Comments:"Steamclock Software - Apple’s new Objective-C to Javascript Bridge"
URL:http://www.steamclock.com/blog/2013/05/apple-objective-c-javascript-bridge/
Nigel Brooke | May 14, 2013
A few month back, Apple quietly slipped a very nice Objective-C to Javascript bridge into WebKit. Since the first commit while we were busy celebrating New Year’s Eve, it has been fairly actively developed and improved. This new API supports straightforward embedding of the JavaScriptCore interpreter into native Objective-C projects, including reading and writing variables and object members with appropriate type coercion, calling methods on JavaScript objects, and directly binding Objective-C objects into JavaScript.
It seems likely that this API is going to become public in Mac OS X 10.9 (where JavaScriptCore is already a public framework), and it might be a hint of an eventual public API on iOS. Either way, a new option for building hybrid JavaScript apps is here.
Siracusa County is on the left. Image © Apple.
Getting the code and building
Building JavaScriptCore with this new Objective-C API isn’t hard, but does require some tweaks to the project file and source. The latest source is available from the WebKit project’s site or the unofficial GitHub mirror, but you can just grab our test project which has a pre-modified version of WebKit along with our test code. A full checkout of WebKit is 2.7GB, whereas the necessary code is only about 60MB.
The Objective-C to JavaScript API is hidden behind an #ifdef JSC_OBJC_API_ENABLED
so we need to add a definition of that to both the JavaScriptCore project and any project that is going to use it. Most of the interesting classes have a NS_CLASS_AVAILABLE(10_9, NA)
to suppress export unless you are building on 10.9. Kind of a giveaway, eh? Luckily, swapping in an ‘8’ in the appropriate places will make it build on 10.8.
Building on iOS is presumably possible, but I haven’t proven this yet.
Embedding the JS interpreter and calling native code
Once we’ve got the source building, running a little JavaScript code is easy. Include JavascriptCore/API/JSContext.h
, create a JSContext
object, and give it some JavaScript:
JSContext* context = [[JSContext alloc] init];
[context evaluateScript:@"console.log(\"Hello JavaScript\")"];
Which will print… absolutely nothing. This is pretty much just a raw JavaScript interpreter that we have, even something as fundamental as console.log
isn’t there yet. Fortunately, since the whole point of this API is to bridge between JavaScript and Objective-C, getting some Objective-C code exposed to the interpreter is equally simple.
This API uses Objective-C protocols as its binding mechanism. Any protocol that you define which includes the (empty) JSExport
protocol from JavascriptCore/API/JSExport.h
will have any methods defined in the protocol exported to JavaScript. If you try and bind a class that has no JSExport
protocols, it asserts inside JavaScriptCore - presumably that will behave better by the time it’s released. That header contains some documentation on how selector names are mangled into names for the JavaScript functions that are exposed, and how to control that behaviour.
Let’s define a protocol and an actual class for our native object that we want to bind into JavaScript:
@protocol NativeObjectExport <JSExport>
-(void)log:(NSString*)string;
@end
@interface NativeObject : NSObject <NativeObjectExport>
@end
@implementation NativeObject
-(void)log:(NSString*)string {
NSLog(@"js: %@", string);
}
@end
Once this is created, we can expose an instance of the class to Javascript like so:
context[@"nativeObject"] = [[NativeObject alloc] init];
and then run some JavaScript that references that object:
[context evaluateScript:@"nativeObject.log(\"Hello Javascript\")"];
Which does do what you would expect: the log appears in the console.
Exposing existing classes
Another thing that we might want to do is to expose JavaScript to some methods from an existing class you don’t own. Fortunately the dark arts of the Objective-C runtime have us covered. We can build a protocol on some existing class:
@protocol NSButtonExport <JSExport>
-(void)setTitle:(NSString*)title;
-(NSString*)title;
@end
Then we can use class_addProtocol
to attach it at runtime:
class_addProtocol([NSButton class], @protocol(NSButtonExport));
Once that’s done, the existing class will bind in fine:
NSButton* button = [[NSButton alloc] initWithFrame:NSRectFromCGRect(CGRectMake(0, 0, 200, 50))];
button.title = @"Hello Objective-C";
[window.contentView addSubview:button];
context[@"button"] = button;
[context evaluateScript:@"button.setTitle(\"Hello JavaScript\")"];
It’s also theoretically possible to build protocols on the fly using the Objective-C runtime functions. This should enable automatically exposing all the methods of an object to JavaScript without having to list them, via walking the existing method table. This would simplify the process of binding large sets of objects into the runtime.
Calling into JavaScript
Calling Objective-C from JavaScript is probably the most likely use case in an app. Stilll, there are lots of situations where we might want to read variables from or call into JavaScript code from Objective-C. That can be done through a few methods on JSContext
, as well as another key class in the API: JSValue
(from JavascriptCore/API/JSValue.h
). JSContext
has functions for accessing global variables, and JSValue
provides an Objective-C wrapper around a JavaScript object, giving you access to the members of the object, including properties and methods.
To get a JSValue
global variable out of the context, we can either use subscript access or use the globalObject property of JSContext
. This can then be converted to an appropriate Objective-C type, or you can modify the variable using the JSValue
reference.
JSValue* jsValue = context[@"globalVariable"];
JSValue* jsValue2 = context[@"globalObject"];
NSString* globalVariableString = [jsValue toString];
NSDictionary* globalObject = [jsValue2 toDictionary];
[jsValue2 setValue:@"foo" forProperty:@"bar"];
You can call methods on JavaScript objects via JSValue
as well:
JSValue* result = [scriptObject invokeMethod:@"someFunction" withArguments:@[]];
If we are willing to spend a bit of time with methodSignatureForSelector
and forwardInvocation
, it’s also possible to make a wrapper object that will accept Objective-C selector calls and automatically forward them to the appropriate JavaScript functions, you can see an example of that in my test project.
Uses and future directions
It seems likely that this API will be part of the public API of JavaScriptCore, at least on the Mac, in OS X 10.9 at WWDC. It’s also possible that this is the start of Apple committing to a public API for the JavaScriptCore framework on iOS, something that developers have been asking for. Availability in iOS 7 would be a pleasant surprise, but even if an official iOS version isn’t ready yet, the API becoming official on the Mac would probably make it worthwhile to invest time in building it ourselves to use on iOS.
These APIs becoming public could be a huge boon to those of us that are interested in using JavaScript in their apps in various ways. It would be an official way to do some of the things enabled by third-party platforms (like Cordova, Appcelerator and Impact) or other binding libraries (such as JSBindings).
The most interesting possibility would be that this is the start of Apple’s evolution away from Objective-C into promoting a higher-level language for their platform. It’s too early to say at this point, but if JavaScriptCore is going to one day displace the Objective-C runtime, this would be a reasonable starting point.
In the meantime, if you are keen to try it, you can always build JavaScriptCore yourself from source. The ease of using the binding methods in particular makes embedding JavaScriptCore an attractive option even before we know Apple’s long-term plans.
Update: Francisco Tolmasky pointed out on Hacker News that this API is not entirely new, but is an evolution of some existing parts of WebKit.framework: WebScripting and WebScriptObject. This doesn’t change much about the technical details, but it does make me even less certain than I already was of whether Apple has any specific future plans for this API.