An updated ranking of programming language popularity is out this week, showing Swift leaping upward through the ranks from 68th to 22nd, while Objective-C holds a strong lead up ahead at #10. Both, however, are blown away by the only other language allowed to run natively on iOS: the current champion, JavaScript.
Introduced with OS X Mavericks and iOS 7, the JavaScriptCore framework puts an Objective-C wrapper around WebKit’s JavaScript engine, providing easy, fast, and safe access to the world’s most prevalent language. Love it or hate it, JavaScript’s ubiquity has led to an explosion of developers, tools, and resources along with ultra-fast virtual machines like the one built into OS X and iOS.
So come, lay aside bitter debates about dynamism and type safety, and join me for a tour of JavaScriptCore.
JSContext
/ JSValue
JSContext
is an environment for running JavaScript code. A JSContext
instance represents the global object in the environment—if you’ve written JavaScript that runs in a browser, JSContext
is analogous to window
. After creating a JSContext
, it’s easy to run JavaScript code that creates variables, does calculations, or even defines functions:
letcontext=JSContext()!context.evaluateScript("var num = 5 + 5")context.evaluateScript("var names = ['Grace', 'Ada', 'Margaret']")context.evaluateScript("var triple = function(value) { return value * 3 }")lettripleNum=context.evaluateScript("triple(num)")
JSContext*context=[[JSContextalloc]init];[contextevaluateScript:@"var num = 5 + 5"];[contextevaluateScript:@"var names = ['Grace', 'Ada', 'Margaret']"];[contextevaluateScript:@"var triple = function(value) { return value * 3 }"];JSValue*tripleNum=[contextevaluateScript:@"triple(num)"];
As that last line shows, any value that comes out of a JSContext
is wrapped in a JSValue
object. A language as dynamic as JavaScript requires a dynamic type, so JSValue
wraps every possible kind of JavaScript value: strings and numbers; arrays, objects, and functions; even errors and the special JavaScript values null
and undefined
.
JSValue
includes a host of methods for accessing its underlying value as the correct Foundation type, including:
JavaScript Type | JSValue method | Objective-C Type | Swift Type |
---|---|---|---|
string | toString | NSString | String! |
boolean | toBool | BOOL | Bool |
number | toNumber toDouble toInt32 toUInt32 | NSNumber double int32_t uint32_t | NSNumber! Double Int32 UInt32 |
Date | toDate | NSDate | Date! |
Array | toArray | NSArray | [Any]! |
Object | toDictionary | NSDictionary | [AnyHashable : Any]! |
Object | toObject toObjectOfClass: | custom type | custom typeas Any! |
To retrieve the value of tripleNum
from the above example, simply use the appropriate method:
print("Tripled: \(tripleNum!.toInt32())")// Tripled: 30
NSLog(@"Tripled: %d",[tripleNumtoInt32]);// Tripled: 30
Subscripting Values
We can easily access any values we’ve created in our context
using subscript notation on both JSContext
and JSValue
instances. JSContext
requires a string subscript, while JSValue
allows either string or integer subscripts for delving down into objects and arrays:
letnames=context.objectForKeyedSubscript("names")letinitialName=names?.objectAtIndexedSubscript(0)print("The first name: \(initialName?.toString() ?? "none")")// The first name: Grace
JSValue*names=context[@"names"];JSValue*initialName=names[0];NSLog(@"The first name: %@",[initialNametoString]);// The first name: Grace
Swift shows its youth, here—while Objective-C code can take advantage of subscript notation, Swift currently only exposes the raw methods that should make such subscripting possible:
objectForKeyedSubscript()
andobjectAtIndexedSubscript()
.
Calling Functions
With a JSValue
that wraps a JavaScript function, we can call that function directly from our Objective-C/Swift code using Foundation types as parameters. Once again, JavaScriptCore handles the bridging without any trouble:
lettripleFunction=context.objectForKeyedSubscript("triple")letresult=tripleFunction.callWithArguments([5])print("Five tripled: \(result.toInt32())")
JSValue*tripleFunction=context[@"triple"];JSValue*result=[tripleFunctioncallWithArguments:@[@5]];NSLog(@"Five tripled: %d",[resulttoInt32]);
Exception Handling
JSContext
has another useful trick up its sleeve: by setting the context’s exceptionHandler
property, you can observe and log syntax, type, and runtime errors as they happen. exceptionHandler
is a callback handler that receives a reference to the JSContext
and the exception itself:
context.exceptionHandler={context,exceptioninprint("JS Error: \(exception?.description ?? "unknownerror")")}context.evaluateScript("function multiply(value1, value2) { return value1 * value2 ")// JS Error: SyntaxError: Unexpected end of script
context.exceptionHandler=^(JSContext*context,JSValue*exception){NSLog(@"JS Error: %@",exception);};[contextevaluateScript:@"function multiply(value1, value2) { return value1 * value2 "];// JS Error: SyntaxError: Unexpected end of script
JavaScript Calling
Now we know how to extract values from a JavaScript environment and call functions defined therein. What about the reverse? How can we get access to our custom objects and methods, defined in Objective-C or Swift, from within the JavaScript realm?
There are two main ways of giving a JSContext
access to our native client code: blocks and the JSExport
protocol.
Blocks
When an Objective-C block is assigned to an identifier in a JSContext
, JavaScriptCore automatically wraps the block in a JavaScript function. This makes it simple to use Foundation and Cocoa classes from within JavaScript—again, all the bridging happens for you. Witness the full power of Foundation string transformations, now accessible to JavaScript:
letsimplifyString:@convention(block)(String)->String={inputinletresult=input.applyingTransform(.toLatin,reverse:false)returnresult?.applyingTransform(.stripCombiningMarks,reverse:false)??""}context.setObject(simplifyString,forKeyedSubscript:"simplifyString"asNSString)print(context.evaluateScript("simplifyString('안녕하새요!')"))// annyeonghasaeyo!
context[@"simplifyString"]=^(NSString*input){NSMutableString*mutableString=[inputmutableCopy];CFStringTransform((__bridgeCFMutableStringRef)mutableString,NULL,kCFStringTransformToLatin,NO);CFStringTransform((__bridgeCFMutableStringRef)mutableString,NULL,kCFStringTransformStripCombiningMarks,NO);returnmutableString;};NSLog(@"%@",[contextevaluateScript:@"simplifyString('안녕하세요!')"]);
There’s another speedbump for Swift here—note that this only works for Objective-C blocks, not Swift closures. To use a Swift closure in a
JSContext
, declare it with the@convention(block)
attribute.
Memory Management
Since blocks can capture references to variables and JSContext
s maintain strong references to all their variables, some care needs to be taken to avoid strong reference cycles. Avoid capturing your JSContext
or any JSValue
s inside a block. Instead, use [JSContext currentContext]
to get the current context and pass any values you need as parameters.
JSExport
Protocol
Another way to use our custom objects from within JavaScript code is to add conformance to the JSExport
protocol. Whatever properties, instance methods, and class methods we declare in our JSExport
-inherited protocol will automatically be available to any JavaScript code. We’ll see how in the following section.
JavaScriptCore in Practice
Let’s build out an example that will use all these different techniques—we’ll define a Person
model that conforms to the JSExport
sub-protocol PersonJSExports
, then use JavaScript to create and populate instances from a JSON file. Who needs NSJSONSerialization
when there’s an entire JavaScript VM lying around?
1) PersonJSExports
and Person
Our Person
class implements the PersonJSExports
protocol, which specifies what properties should be available in JavaScript.
The
create...
class method is necessary because JavaScriptCore does not bridge initializers—we can’t simply sayvar person = new Person()
the way we would with a native JavaScript type.
// Custom protocol must be declared with `@objc`@objcprotocolPersonJSExports:JSExport{varfirstName:String{getset}varlastName:String{getset}varbirthYear:NSNumber?{getset}funcgetFullName()->String/// create and return a new Person instance with `firstName` and `lastName`staticfunccreateWithFirstName(firstName:String,lastName:String)->Person}// Custom class must inherit from `NSObject`@objcclassPerson:NSObject,PersonJSExports{// properties must be declared as `dynamic`dynamicvarfirstName:StringdynamicvarlastName:StringdynamicvarbirthYear:NSNumber?init(firstName:String,lastName:String){self.firstName=firstNameself.lastName=lastName}classfunccreateWithFirstName(firstName:String,lastName:String)->Person{returnPerson(firstName:firstName,lastName:lastName)}funcgetFullName()->String{return"\(firstName) \(lastName)"}}
// in Person.h -----------------@classPerson;
@protocolPersonJSExports<JSExport>@property(nonatomic,copy)NSString*firstName;@property(nonatomic,copy)NSString*lastName;@propertyNSIntegerageToday;-(NSString*)getFullName;// create and return a new Person instance with `firstName` and `lastName`+(instancetype)createWithFirstName:(NSString*)firstNamelastName:(NSString*)lastName;@end@interfacePerson : NSObject<PersonJSExports>@property(nonatomic,copy)NSString*firstName;@property(nonatomic,copy)NSString*lastName;@propertyNSIntegerageToday;@end// in Person.m -----------------@implementationPerson-(NSString*)getFullName{return[NSStringstringWithFormat:@"%@ %@",self.firstName,self.lastName];}+(instancetype)createWithFirstName:(NSString*)firstNamelastName:(NSString*)lastName{Person*person=[[Personalloc]init];person.firstName=firstName;person.lastName=lastName;returnperson;}@end
2) JSContext
Configuration
Before we can use the Person
class we’ve created, we need to export it to the JavaScript environment. We’ll also take this moment to import the Mustache JS library, which we’ll use to apply templates to our Person
objects later.
// export Person classcontext.setObject(Person.self,forKeyedSubscript:"Person")// load Mustache.jsifletmustacheJSString=String(contentsOfFile:...,encoding:NSUTF8StringEncoding,error:nil){context.evaluateScript(mustacheJSString)}
// export Person classcontext[@"Person"]=[Personclass];// load Mustache.jsNSString*mustacheJSString=[NSStringstringWithContentsOfFile:...encoding:NSUTF8StringEncodingerror:nil];[contextevaluateScript:mustacheJSString];
3) JavaScript Data & Processing
Here’s a look at our simple JSON example and the code that will process it to create new Person
instances.
Note: JavaScriptCore translates Objective-C/Swift method names to be JavaScript-compatible. Since JavaScript doesn’t have named parameters, any external parameter names are converted to camel-case and appended to the function name. In this example, the Objective-C method
createWithFirstName:lastName:
becomescreateWithFirstNameLastName()
in JavaScript.
varloadPeopleFromJSON=function(jsonString){vardata=JSON.parse(jsonString);varpeople=[];for(i=0;i<data.length;i++){varperson=Person.createWithFirstNameLastName(data[i].first,data[i].last);person.birthYear=data[i].year;people.push(person);}returnpeople;}
[{"first":"Grace","last":"Hopper","year":1906},{"first":"Ada","last":"Lovelace","year":1815},{"first":"Margaret","last":"Hamilton","year":1936}]
4) Tying It All Together
All that remains is to load the JSON data, call into the JSContext
to parse the data into an array of Person
objects, and render each Person
using a Mustache template:
// get JSON stringletpeopleJSON=try?String(contentsOfFile:...,encoding:.utf8)// get load functionletload=context.objectForKeyedSubscript("loadPeopleFromJSON")!// call with JSON and convert to an Arrayifletpeople=load.call(withArguments:[peopleJSON]).toArray()as?[Person]{// get rendering function and create templateletmustacheRender=context.objectForKeyedSubscript("Mustache").objectForKeyedSubscript("render")!lettemplate="{{getFullName}}, born {{birthYear}}"// loop through people and render Person object as stringforpersoninpeople{print(mustacheRender.call(withArguments:[template,person]))}}// Output:// Grace Hopper, born 1906// Ada Lovelace, born 1815// Margaret Hamilton, born 1936
// get JSON stringNSString*peopleJSON=[NSStringstringWithContentsOfFile:...encoding:NSUTF8StringEncodingerror:nil];// get load functionJSValue*load=context[@"loadPeopleFromJSON"];// call with JSON and convert to an NSArrayJSValue*loadResult=[loadcallWithArguments:@[peopleJSON]];NSArray*people=[loadResulttoArray];// get rendering function and create templateJSValue*mustacheRender=context[@"Mustache"][@"render"];NSString*template=@"{{getFullName}}, born {{birthYear}}";// loop through people and render Person object as stringfor(Person*personinpeople){NSLog(@"%@",[mustacheRendercallWithArguments:@[template,person]]);}// Output:// Grace Hopper, born 1906// Ada Lovelace, born 1815// Margaret Hamilton, born 1936
How can you use JavaScript in your apps? JavaScript snippets could be the basis for user-defined plugins that ship alongside yours. If your product started out on the web, you may have existing infrastructure that can be used with only minor changes. Or if you started out as a programmer on the web, you might relish the chance to get back to your scripty roots. Whatever the case, JavaScriptCore is too well-built and powerful to ignore.