(Not) Dynamically Exporting Objective-C Classes To JavaScriptCore

It’s not possible, but it’s interesting to know why!

One of the first things I wanted to try when the Objective-C API was added to JavaScriptCore in iOS 7 was dynamically exporting Objective-C classes to utilize them in JS. Part of the motivation behind this was to avoid the verbose requirement of extending and implementing the empty JSExport protocol that normally exposes certain properties and methods of a native class. Nigel Brooke at Steamclock also suggested that this was a possibility, so I started on my way.

What we want to do is, given a class name we want to expose to JS, traverse the method and property information to build a new protocol (extending JSExport) with the same methods and properties. This will export all of the methods and properties to JS. Then, we tell the runtime that this class implements this protocol and we should be good to go. If you haven’t played with the Objective-C runtime before, it isn’t too difficult to deal with, although you will be working with C and manual memory management. I’ll go through the code necessary to do all of this in small chunks.

First let’s make a new protocol that extends JSExport, assuming that we have a Class class variable that we want to export:

const char *protocolName = class_getName(class);
Protocol *protocol = objc_allocateProtocol(protocolName);
protocol_addProtocol(protocol, objc_getProtocol("JSExport"));

Now lets build up a list of methods from the class that we’ll expose to JS. Note that instance and class methods are done separately. First we get a list of methods and how many there are, and then we enumerate that list and use the information about the method to add it to the protocol:

NSUInteger methodCount, classMethodCount;
Method *methods, *classMethods;
methods = class_copyMethodList(class, &methodCount);
for (NSUInteger methodIndex = 0; methodIndex < methodCount; ++methodIndex) {
    Method method = methods[methodIndex];
    protocol_addMethodDescription(protocol, method_getName(method), method_getTypeEncoding(method), YES, YES);
}

Here we’re doing the same for class methods:

classMethods = class_copyMethodList(object_getClass(class), &classMethodCount);
for (NSUInteger methodIndex = 0; methodIndex < classMethodCount; ++methodIndex) {
    Method method = classMethods[methodIndex];
    protocol_addMethodDescription(protocol, method_getName(method), method_getTypeEncoding(method), YES, NO);
}

Now let’s do it for properties. They’re almost exactly the same except that we also need to fetch and use each property’s attributes (the name and type encoding) to add it to the protocol.

NSUInteger propertyCount;
objc_property_t *properties;
properties = class_copyPropertyList(class, &propertyCount);
for (NSUInteger propertyIndex = 0; propertyIndex < propertyCount; ++propertyIndex) {
    objc_property_t property = properties[propertyIndex];
    NSUInteger attributeCount;
    objc_property_attribute_t *attributes = property_copyAttributeList(property, &attributeCount);
    protocol_addProperty(protocol, property_getName(property), attributes, attributeCount, YES, YES);
    free(attributes);
}

And the most important part, adding the new protocol to the class:

objc_registerProtocol(protocol);
class_addProtocol(class, protocol);

You can find the full implementation I made to do this in a gist in the -exportClass:toContext: method. (I skipped a bit here.) But! This doesn’t work. In fact it used to crash. At this point I was disappointed, but determined to find out why. First we’ll need the JavaScriptCore source.

Diving into the source far enough will bring you to objCCallbackFunctionForMethod, the function that creates a JSObjectRef pointing to the native method of an Objective-C class. All it really does, though, is pass some method-specific information to objCCallbackFunctionForInvocation which handles the real work. But there’s something peculiar about this function call:

objCCallbackFunctionForInvocation(context, invocation, isInstanceMethod ? CallbackInstanceMethod : CallbackClassMethod, isInstanceMethod ? cls : nil, _protocol_getMethodTypeEncoding(protocol, sel, YES, isInstanceMethod))

What’s _protocol_getMethodTypeEncoding? Visiting the definition gives us:

// Forward declare some Objective-C runtime internal methods that are not API.
const char *_protocol_getMethodTypeEncoding(Protocol *, SEL, BOOL isRequiredMethod, BOOL isInstanceMethod);

Interesting! Here JavaScriptCore is relying on a private Objective-C runtime function in order to properly expose Objective-C methods. Let’s jump to the runtime implementation, which is also open source, to see how this might be different from the public protocol_getMethodDescription and why JavaScriptCore needs to use it. I’ll note that iOS 7 isn’t available on Apple Open Source yet, but 10.9 is, so that’s where we’ll go. In objc-runtime-new.mm we find the implementation of _protocol_getMethodTypeEncoding with a comment that reads: “Returns nil if the compiler did not emit any extended @encode data.” Even more interesting! So the compiler will emit extended @encode data that we don’t normally get when we use protocol_getMethodDescription.

In fact, when we use both of these we can see exactly how the returned values are different:

// Given this definition:
@protocol Tester
@required
- (BOOL)testDictionary:(NSDictionary *)dictionary error:(NSError **)error;
@end

struct objc_method_description description = protocol_getMethodDescription(@protocol(Tester), @selector(testDictionary:error:), YES, YES);
NSLog(@%s, description.types);
// Outputs c32@0:8@16^@24

const char *descriptionString = _protocol_getMethodTypeEncoding(@protocol(Tester), @selector(testDictionary:error:), YES, YES);
NSLog(@%s, descriptionString);
// Outputs c32@0:8@”NSDictionary”16^@24

There’s a full gist you can run here.

So this is why we need to have our JSExport protocols defined at compile time, and if you try to use _protocol_getMethodTypeEncoding on a protocol that was created dynamically you still won’t get the extended type info you need. I’d like to say that knowing this allows us to work around the lack of this extra type information in order to export native classes like we first wanted to. Unfortunately, that extra type information is crucial to how JavaScriptCore marshals values into and out of the virtual machine, and there are obvious downsides to trying to get JavaScriptCore to ignore the missing information (I tried, just in case).

If we look back at the JSC source we can now see how this extra type information is used to marshal values. objCCallbackFunctionForInvocation loops over this extra information in order to properly specify how JS types should be marshalled to Objective-C types as well as how many arguments there are for a given method.

You may have noticed that objCCallbackFunctionForInvocation is also called to create JSObjectRefs for Objective-C blocks. In that case it uses the also-private _Block_signature function declared here to return the type encoding of the block’s signature. Although this function is private, the ABI is public and it’s possible to access the signature directly, something that I first learned from Rob Rix’s Obstruct project.

I hope that, despite this disappointing ending, you’ve learned a bit more about the internals of JavaScriptCore and the Objective-C runtime.