Using Objective-C from js-ctypes

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.

  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 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 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 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.

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 from locking up, this code should be run 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 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: 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 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

Document Tags and Contributors

 Contributors to this page: rolfedh, Noitidart, arai
 Last updated by: rolfedh,