This article needs a technical review. How you can help.
This article needs an editorial review. How you can help.
Objective-C has its own syntax, and it cannot be written directly with js-ctypes. This documentation explains how to convert the Objective-C code into js-ctypes code.
A simple example is also in Standard OS Libraries page.
Converting Objective-C code to C code
To convert Objective-C code to js-ctypes, we need to convert it to C code first. Then, we can convert it straight to js-ctypes code.
Speech Synthesis Example
Let's start with the following Objective-C code, which invokes the Speech Synthesis API to say "Hello, Firefox!". It uses the default system voice and waits until the speaking is done.
#import <AppKit/AppKit.h> int main(void) { NSSpeechSynthesizer* synth = [[NSSpeechSynthesizer alloc] initWithVoice: nil]; [synth startSpeakingString: @"Hello, Firefox!"]; // Wait until start speaking. while (![synth isSpeaking]) {} // Wait while speaking. while ([synth isSpeaking]) {} [synth release]; return 0; }
To run the code, save it as test.m
. Then, run following command in the directory (needs XCode).
$ clang -framework AppKit test.m && ./a.out
Class, Message, and Selector
So, we need to convert Objective-C syntax to C syntax. Let's take following codelet. It passes an alloc
message to the NSSpeechSynthesizer
class, in Objective-C syntax.
[NSSpeechSynthesizer alloc]
It does the following things behind the Objective-C syntax.
- Get the
NSSpeechSynthesizer
class definition. - Register the
alloc
selector for the message. - Send a message to the class, with the selector.
Get a reference to a class
Class definitions can be retrieved with the objc_getClass
function, which is declared in /usr/include/objc/runtime.h
. The objc_getClass
function receives the name of the class, looks up the definition, and returns it.
Class objc_getClass(const char *name);
Class
is defined as following, in /usr/include/objc/objc.h
, it's an opaque type.
typedef struct objc_class *Class;
In this case, we need the classNSSpeechSynthesizer
, so it can be retrieved with the following code.
Class NSSpeechSynthesizer = objc_getClass("NSSpeechSynthesizer");
Registering a selector
Selectors can be registered and retrieved with sel_registerName
function, which is also declared in /usr/include/objc/runtime.h
. sel_registerName
receives the name of the selector, and return the selector.
SEL sel_registerName(const char *str);
SEL
is defined as follows, in /usr/include/objc/objc.h
. it's also an opaque type.
typedef struct objc_selector *SEL;
In this case, we need to send alloc
, the selector can be retrieved with the following code.
SEL alloc = sel_registerName("alloc");
Sending a message
Once target class and selector are ready, you can send a message. The message can be sent using the objc_msgSend
function and its variants, which are declared in /usr/include/objc/message.h
. objc_msgSend
function, receives the instance which receives the message, the selector, and variable argument list for the message, and returns the returned value of the method.
id objc_msgSend(id self, SEL op, ...);
id
is defined as following, in /usr/include/objc/objc.h
, it's also an opaque type. Class
can be cast into id
, so we can pass Class
returned by objc_getClass
.
typedef struct objc_object *id;
In this case, send an alloc
message without any arguments using the following code. This code returns an allocated NSSpeechSynthesizer
instance that has not been initialized.
id tmp = objc_msgSend((id)NSSpeechSynthesizer, alloc);
Here, Class
is always cast into id
, which is an opaque type. We could use id
instead, to reduce casting in future.
id NSSpeechSynthesizer = (id)objc_getClass("NSSpeechSynthesizer"); id tmp = objc_msgSend(NSSpeechSynthesizer, alloc);
Selector for a method with arguments
[NSSpeechSynthesizer initWithVoice:]
takes one argument, in that case, selector name has a trailing colon.
SEL initWithVoice = sel_registerName("initWithVoice:");
If a method takes two or more arguments, the selector name is the concatenation of each name.
// [NSString getBytes:maxLength:usedLength:encoding:options:range:remainingRange:] SEL foo = sel_registerName("getBytes:maxLength:usedLength:encoding:options:range:remainingRange:");
Method which returns non-id type
If a method returns a type which is compatible with id
, we can cast it, or just use it as id
type (since we don't need to use a different type for each instance, in terms of C).
Otherwise, following functions can be used, depending on the return type and architecture.
objc_msgSend_stret
- For the method which returns structs on the stack.
objc_msgSend_fpret
/objc_msgSend_fp2ret
- For the method which returns floating-point values on the stack.
objc_msgSend
- For the method which returns the value in a register, or returns nothing.
For example, [NSSpeechSynthesizer isSpeaking]
returns BOOL
. In this case, BOOL
can be passed through a register, we can use objc_msgSend
. Also, [NSObject release]
returns nothing, also, in this case, we can use objc_msgSend
too.
NSString literals
One more Objective-C syntax used in the @"..."
literal. Which creates NSString instance. It could be converted into following Objective-C code (may not be exactly same).
NSString* text = [NSString initWithCString: "Hello, Firefox!" encoding: NSUTF8StringEncoding];
So, it will be converted into following C code. NSUTF8StringEncoding
is defined as 4
.
id NSString = (id)objc_getClass("NSString"); SEL initWithCString_encoding = sel_registerName("initWithCString:encoding:"); int NSUTF8StringEncoding = 4; id tmp = objc_msgSend(NSString, alloc); id text = objc_msgSend(tmp, initWithCString_encoding, "Hello, Firefox!", NSUTF8StringEncoding);
Note that you need to release the allocated NSString
instance.
Converted C code
Now we can translate whole code into C syntax.
#include <objc/objc.h> #include <objc/runtime.h> #include <objc/message.h> int main(void) { // NSSpeechSynthesizer* synth = [[NSSpeechSynthesizer alloc] initWithVoice: nil]; id NSSpeechSynthesizer = (id)objc_getClass("NSSpeechSynthesizer"); SEL alloc = sel_registerName("alloc"); SEL initWithVoice = sel_registerName("initWithVoice:"); id tmp = objc_msgSend(NSSpeechSynthesizer, alloc); id synth = objc_msgSend(tmp, initWithVoice, NULL); // @"Hello, Firefox!" id NSString = (id)objc_getClass("NSString"); SEL initWithCString_encoding = sel_registerName("initWithCString:encoding:"); int NSUTF8StringEncoding = 4; id tmp2 = objc_msgSend(NSString, alloc); id text = objc_msgSend(tmp2, initWithCString_encoding, "Hello, Firefox!", NSUTF8StringEncoding); // [synth startSpeakingString: @"Hello, Firefox!"]; SEL startSpeakingString = sel_registerName("startSpeakingString:"); objc_msgSend(synth, startSpeakingString, text); SEL isSpeaking = sel_registerName("isSpeaking"); // Wait until start speaking. // [synth isSpeaking] while (!objc_msgSend(synth, isSpeaking)) {} // Wait while speaking. // [synth isSpeaking] while (objc_msgSend(synth, isSpeaking)) {} SEL release = sel_registerName("release"); // [synth release]; objc_msgSend(synth, release); // [text release]; objc_msgSend(text, release); return 0; }
To run the code, save it as test.c
, and run following command in the directory.
$ clang -lobjc -framework AppKit test.c && ./a.out
Converting C code to js-ctypes code
Now we have working C code. It could be converted into js-ctypes in an almost straightforward way.
Types and Functions
In addition to above code, we need to declare function and types.
Types
Types can be declared in a straightforward manner. BOOL
is defined in /usr/include/objc/objc.h
.
let id = ctypes.StructType("objc_object").ptr; let SEL = ctypes.StructType("objc_selector").ptr; let BOOL = ctypes.signed_char;
Functions
All functions in the example are exported by /usr/lib/libobjc.dylib
.
let lib = ctypes.open(ctypes.libraryName("objc"));
The only tricky part is the function definition. In this example, objc_msgSend
is used in 3 ways. So, we need to declare three different FunctionType
CData
s.
- Returns
id
or compatible type. - Returns
BOOL
. - Returns nothing.
let objc_msgSend_id = lib.declare("objc_msgSend", ctypes.default_abi, id, id, SEL, "..."); let objc_msgSend_BOOL = lib.declare("objc_msgSend", ctypes.default_abi, BOOL, id, SEL, "..."); let objc_msgSend_void = lib.declare("objc_msgSend", ctypes.default_abi, ctypes.void_t, id, SEL, "...");
The former two cases are both integers (including pointer), so we can cast it after receiving the value in pointer type. The third case is void
, but we're going to use the same function internally, and the only difference is that we need to ignore the returned value or not. So, in fact, we can use the same definition in all cases here, as a minimal case.
let objc_msgSend = lib.declare("objc_msgSend", ctypes.default_abi, id, id, SEL, "...");
But declaring a dedicated function for BOOL
might be better, to get the primitive value directly.
let objc_msgSend = lib.declare("objc_msgSend", ctypes.default_abi, id, id, SEL, "..."); let objc_msgSend_BOOL = lib.declare("objc_msgSend", ctypes.default_abi, BOOL, id, SEL, "...");
Other functions can be declared in almost straightforward manner, except using id
instead of Class
as the return type of objc_getClass
.
let objc_getClass = lib.declare("objc_getClass", ctypes.default_abi, id, ctypes.char.ptr); let sel_registerName = lib.declare("sel_registerName", ctypes.default_abi, SEL, ctypes.char.ptr);
Calling variadic function
objc_msgSend
is a variadic function, so we should always pass a CData
instance, except the first and second argument, to tell the type of each argument.
For example, let's take following function call.
id text = objc_msgSend(tmp2, initWithCString_encoding, "Hello, Firefox!", NSUTF8StringEncoding);
[NSString initWithCString:encoding:]
is defined as following.
- (instancetype)initWithCString:(const char *)nullTerminatedCString encoding:(NSStringEncoding)encoding
And NSStringEncoding
is defined as following.
typedef unsigned long NSUInteger; typedef NSUInteger NSStringEncoding;
So, the function call can be converted into following js-ctypes code.
let text = objc_msgSend(tmp2, initWithCString_encoding, ctypes.char.array()("Hello, Firefox!"), ctypes.unsigned_long(NSUTF8StringEncoding));
Converted js-ctypes code
Here's converted code, which works with copy-n-paste into Scratchpad, with Browser Environment.
let { ctypes } = Components.utils.import("resource://gre/modules/ctypes.jsm", {}); let id = ctypes.StructType("objc_object").ptr; let SEL = ctypes.StructType("objc_selector").ptr; let BOOL = ctypes.signed_char; let lib = ctypes.open(ctypes.libraryName("objc")); let objc_getClass = lib.declare("objc_getClass", ctypes.default_abi, id, ctypes.char.ptr); let sel_registerName = lib.declare("sel_registerName", ctypes.default_abi, SEL, ctypes.char.ptr); let objc_msgSend = lib.declare("objc_msgSend", ctypes.default_abi, id, id, SEL, "..."); let objc_msgSend_BOOL = lib.declare("objc_msgSend", ctypes.default_abi, BOOL, id, SEL, "..."); let NSSpeechSynthesizer = objc_getClass("NSSpeechSynthesizer"); let alloc = sel_registerName("alloc"); let initWithVoice = sel_registerName("initWithVoice:"); let tmp = objc_msgSend(NSSpeechSynthesizer, alloc); let synth = objc_msgSend(tmp, initWithVoice, ctypes.voidptr_t(null)); let NSString = objc_getClass("NSString"); let initWithCString_encoding = sel_registerName("initWithCString:encoding:"); let NSUTF8StringEncoding = 4; let tmp2 = objc_msgSend(NSString, alloc); let text = objc_msgSend(tmp2, initWithCString_encoding, ctypes.char.array()("Hello, Firefox!"), ctypes.unsigned_long(NSUTF8StringEncoding)); let startSpeakingString = sel_registerName("startSpeakingString:"); objc_msgSend(synth, startSpeakingString, text); let isSpeaking = sel_registerName("isSpeaking"); // Wait until start speaking. while (!objc_msgSend_BOOL(synth, isSpeaking)) {} // Wait while speaking. while (objc_msgSend_BOOL(synth, isSpeaking)) {} let release = sel_registerName("release"); objc_msgSend(synth, release); objc_msgSend(text, release); lib.close();
Creating Objective-C Blocks
Objective-C API calls sometimes require you to pass in a block. Reading the Apple Developer :: Programming with Objective-C - Working with Blocks we can learn about blocks. To create a block with js-ctypes use this function:
function createBlock(aFuncTypePtr) { /** * Creates a C block instance from a JS Function. * Blocks are regular Objective-C objects in Obj-C, and can be sent messages; * thus Block instances need are creted using the core.wrapId() function. */ // Apple Docs :: Working with blocks - https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithBlocks/WorkingwithBlocks.html var _NSConcreteGlobalBlock = ctypes.open(ctypes.libraryName('objc')).declare('_NSConcreteGlobalBlock', ctypes.voidptr_t); // https://dxr.mozilla.org/mozilla-central/source/js/src/ctypes/Library.cpp?offset=0#271 /** * The "block descriptor" is a static singleton struct. Probably used in more * complex Block scenarios involving actual closure variables needing storage * (in `NodObjC`, JavaScript closures are leveraged instead). */ // struct is seen here in docs: https://clang.llvm.org/docs/Block-ABI-Apple.html var Block_descriptor_1 = ctypes.StructType('Block_descriptor_1', [ { reserved: ctypes.unsigned_long_long }, { size: ctypes.unsigned_long_long } ]); /** * We have to simulate what the llvm compiler does when it encounters a Block * literal expression (see `Block-ABI-Apple.txt` above). * The "block literal" is the struct type for each Block instance. */ // struct is seen here in docs: https://clang.llvm.org/docs/Block-ABI-Apple.html var Block_literal_1 = ctypes.StructType('Block_literal_1', [ { isa: ctypes.voidptr_t }, { flags: ctypes.int32_t }, { reserved: ctypes.int32_t }, { invoke: ctypes.voidptr_t }, { descriptor: Block_descriptor_1.ptr } ]); var BLOCK_CONST = { BLOCK_HAS_COPY_DISPOSE: 1 << 25, BLOCK_HAS_CTOR: 1 << 26, BLOCK_IS_GLOBAL: 1 << 28, BLOCK_HAS_STRET: 1 << 29, BLOCK_HAS_SIGNATURE: 1 << 30 }; // based on work from here: https://github.com/trueinteractions/tint2/blob/f6ce18b16ada165b98b07869314dad1d7bee0252/modules/Bridge/core.js#L370-L394 var bl = Block_literal_1(); // Set the class of the instance bl.isa = _NSConcreteGlobalBlock; // Global flags bl.flags = BLOCK_CONST.BLOCK_HAS_STRET; bl.reserved = 0; bl.invoke = aFuncTypePtr; // create descriptor var desc = Block_descriptor_1(); desc.reserved = 0; desc.size = Block_literal_1.size; // set descriptor into block literal bl.descriptor = desc.address(); return bl; }
An example of using this can be seen here: _ff-addon-snippet-objc_monitorEvents - Shows how to monitor and block mouse and key events on Mac OS X