Using Objective-C from js-ctypes

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.

Converting Objective-C code to C code

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.

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;
}

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

Class, Message, and Selector

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:

  1. Get the NSSpeechSynthesizer class definition.
  2. Register the alloc selector for the message.
  3. Send a message to the class, with the selector.

Get a reference to a class

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");

Registering a selector

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");

Sending a message

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);

Selector for a method with arguments

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:");

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, the following functions can be used, depending on 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, and we can use objc_msgSend. As [NSObject release] returns nothing, we can also use objc_msgSend.

NSString literals

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.

Converted C code

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

Converting C code to js-ctypes code

Now we have working C code, it can be converted into js-ctypes in a relatively straightforward manner.

Types and Functions

In addition to the above code, we need to declare function and types.

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;

Functions

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 CDatas:

  • 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 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);

Calling variadic function

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));

Converted js-ctypes code

Finally, we have our converted code. This can run with a copy-and-paste into a JavaScript shell.

This example uses a busy loop, and thus, Firefox won't respond until the speaking is done. If this code were to be used in a production add-on, then to avoid Firefox locking up, run this code from a ChromeWorker.
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 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); // //github.com/RealityRipple/UXP/blob/master/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