Objective-C has its own syntax, it cannot be written directly with js-ctypes. This guide explains how to convert Objective-C code into js-ctypes code.
A simple example is also in Standard OS Libraries page.
To convert Objective-C code to js-ctypes, we need to convert it to C code first. We can then convert it straight to js-ctypes code.
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; }
Save this file as test.m
, and run with the following command, inside the same directory as the saved file (needs XCode).
$ clang -framework AppKit test.m && ./a.out
Our task at hand is to convert Objective-C syntax to C syntax. Let's look at the following codelet:
[NSSpeechSynthesizer alloc]
It passes an alloc
message to the NSSpeechSynthesizer
class, in Objective-C syntax. It performs the following through this Objective-C syntax:
NSSpeechSynthesizer
class definition.alloc
selector for the message.Class definitions are retrieved with the objc_getClass
function, 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);
In /usr/include/objc/objc.h
, Class
is defined as an opaque type by the following:
typedef struct objc_class *Class;
In this example, we need the classNSSpeechSynthesizer
, which is retrieved with the following code:
Class NSSpeechSynthesizer = objc_getClass("NSSpeechSynthesizer");
Selectors can be registered and retrieved with sel_registerName
function, also declared in /usr/include/objc/runtime.h
. sel_registerName
receives the name of the selector, and returns 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 example, we need to send alloc
, its selector can be retrieved with the following code:
SEL alloc = sel_registerName("alloc");
Once target class and selector are ready, you can send a message. This 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, returning the returned value from the method.
id objc_msgSend(id self, SEL op, ...);
id
is defined as the 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 example, we send an alloc
message without any arguments using the following code. This code returns an allocated NSSpeechSynthesizer
instance that has not yet been initialized.
id tmp = objc_msgSend((id)NSSpeechSynthesizer, alloc);
Here, Class
is always cast into id
, which is an opaque type. We could choose to use id
instead, reducing casting and making our code more efficient in future.
id NSSpeechSynthesizer = (id)objc_getClass("NSSpeechSynthesizer"); id tmp = objc_msgSend(NSSpeechSynthesizer, alloc);
In this case, [NSSpeechSynthesizer initWithVoice:]
takes one argument; the selector name with a trailing colon.
SEL initWithVoice = sel_registerName("initWithVoice:");
If a method takes two or more arguments, the selector name becomes a concatenation of each name.
// [NSString getBytes:maxLength:usedLength:encoding:options:range:remainingRange:] SEL foo = sel_registerName("getBytes:maxLength:usedLength:encoding:options:range:remainingRange:");
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, the following functions can be used, depending on return type and architecture.
objc_msgSend_stret
objc_msgSend_fpret
/ objc_msgSend_fp2ret
objc_msgSend
For example, [NSSpeechSynthesizer isSpeaking]
returns BOOL
. In this case, BOOL
can be passed through a register, and we can use objc_msgSend
. As [NSObject release]
returns nothing, we can also use objc_msgSend
.
Another Objective-C syntax used is the @"..."
literal, which creates NSString instance. This could be converted by the following Objective-C code (may not be exactly the same).
NSString* text = [NSString initWithCString: "Hello, Firefox!" encoding: NSUTF8StringEncoding];
This will be converted into the 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 this allocated NSString
instance.
Now we can translate our 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 this code, save it as test.c
, and run the following command in the same directory.
$ clang -lobjc -framework AppKit test.c && ./a.out
Now we have working C code, it can be converted into js-ctypes in a relatively straightforward manner.
In addition to the above code, we need to declare function and types.
Types can be readily declared. 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;
All functions in our example are exported by /usr/lib/libobjc.dylib
.
let lib = ctypes.open(ctypes.libraryName("objc"));
Function definition is the more tricky part. In this example, objc_msgSend
is used in 3 ways. We need to declare three different FunctionType
CData
s:
id
or compatible type.BOOL
.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 first two cases are both integers (including pointer), so we can cast them after receiving the value in pointer type. The third case is void
, but we're going to use the same function internally, the only difference is if we need to ignore the returned value or not. In fact, here we can use the same definition in all cases, as a minimal case.
let objc_msgSend = lib.declare("objc_msgSend", ctypes.default_abi, id, id, SEL, "...");
Declaring a dedicated function for BOOL
might be more efficient, directly getting the primitive value.
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 fluently, 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);
objc_msgSend
is a variadic function, so we should always pass it a CData
instance, other than this first and second argument, to declare each argument type.
For example, let's take the following function call:
id text = objc_msgSend(tmp2, initWithCString_encoding, "Hello, Firefox!", NSUTF8StringEncoding);
[NSString initWithCString:encoding:]
is defined as:
- (instancetype)initWithCString:(const char *)nullTerminatedCString encoding:(NSStringEncoding)encoding
And NSStringEncoding
is defined as:
typedef unsigned long NSUInteger; typedef NSUInteger NSStringEncoding;
So, our function call can be converted into the following js-ctypes code:
let text = objc_msgSend(tmp2, initWithCString_encoding, ctypes.char.array()("Hello, Firefox!"), ctypes.unsigned_long(NSUTF8StringEncoding));
Finally, we have our converted code. This can run with a copy-and-paste into a JavaScript shell.
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();
Objective-C API calls sometimes require you to pass in a block. Reading the Apple Developer :: Programming with Objective-C - Working with Blocks you can learn more about blocks. To create a block with js-ctypes, use the 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: http://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: http://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 this function in use can be seen here: _ff-addon-snippet-objc_monitorEvents - Shows how to monitor and block mouse and key events on Mac OS X