Quantcast
Channel: NSHipster
Viewing all articles
Browse latest Browse all 382

JavaScriptCore

$
0
0

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 TypeJSValue methodObjective-C TypeSwift Type
stringtoStringNSStringString!
booleantoBoolBOOLBool
numbertoNumber
toDouble
toInt32
toUInt32
NSNumber
double
int32_t
uint32_t
NSNumber!
Double
Int32
UInt32
DatetoDateNSDateDate!
ArraytoArrayNSArray[Any]!
ObjecttoDictionaryNSDictionary[AnyHashable : Any]!
ObjecttoObject
toObjectOfClass:
custom typecustom 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() and objectAtIndexedSubscript().

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 JSContexts maintain strong references to all their variables, some care needs to be taken to avoid strong reference cycles. Avoid capturing your JSContext or any JSValues 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 say var 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: becomes createWithFirstNameLastName() 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.


Viewing all articles
Browse latest Browse all 382

Trending Articles