Intro to JavaScriptCore

I last wrote about the JavaScriptCore REPL that every Mac user has available on their computer. Today I want to discuss a far more useful way to interact with JavaScriptCore: leveraging it directly within your native apps.

JavaScriptCore can benefit your native apps in a few ways. Primarily it opens the door for using the many excellent JavaScript libraries out there. Additionally it is highly configurable and hackable, allowing native apps to share their values and even behavior within the JavaScript context.

JavaScriptCore can be leveraged with a few different strategies.

  • Directly in code via the JavaScriptCore framework.
  • By creating a WKWebView to run and display html & js.
  • By creating a WKWebView and borrowing the internal JavaScript context to execute a script asynchronously.

Each comes with its own distinct benefits and tradeoffs. This article will explore the first option, which has the advantages of deep integration with your Swift code and tighter control over the execution of your js. I’ll discuss the tradeoffs later in this article.

The Setup

It is easiest to explore JavaScriptCore using Xcode’s Playground feature. If you want to follow along open Xcode and create a new playground. The default will look something like this:

//: Playground - noun: a place where people can play

import UIKit

var str = "Hello, playground"

Go ahead and delete all of that and enter the following so we have an environment ready to use JavaScriptCore.

import Foundation
import JavaScriptCore

Creating the JavaScript environment is as simple as:

let context = JSContext()

Now we have a context within which we can inject JavaScript libraries, code, and even native Swift values, objects, & functions.

Invoking JavaScript

The primary point of entry into a JSContext is evaluateScript. It takes in a string of JavaScript, runs it, and returns a JSValue object. But there is one little gotcha that may feel odd as we interoperate with native code.

Consider this example that attempts to return the value 1.

let jsv = context.evaluateScript(
    "return 1;"
)

>> undefined

You might expect the above snippet to set jsv to the value of 1. However jsv is undefined.

The method evaluateScript returns the last value generated by the script. This is because the string passed into evaluateScript is like a JavaScript document loaded into a browser. You don’t return values at the end of a browser script. The return statement only has meaning within the context of a function. Instead, simply stating a 1 will result in the value 1 being returned.

let jsv = context.evaluateScript(
    "1;"
)

>> 1

The behavior is a little more obvious when calling a function within evaluateScript, for example map.

let jsv = context.evaluateScript(
    "[1,2,3,4,5].map(function(n){return n*n})"
)

>> 1,4,9,16,25

The last value generated was the result of calling map, which returns an array that our value jsv now points to.

As you can see evaluating JavaScript is easy. Now I’ll explain how to get JavaScript files into the context.

Third Party Scripts

Since we are discussing JavaScript within a native app, you probably wont be writing the bulk of your app in JavaScript, but rather in Swift. Where JavaScriptCore really shines is in offloading some work to existing libraries or tools that are already written in js. Here I will show you a simple example of reading a resource from within an Xcode Playground.

Lets say you want to convert some markdown using the pagedown library. Import this into your Playground by dragging it into the Resources folder. Your folder structure will look like this.

- Playground
  └ Resources
    └ pagedown
      └ Markdown.Converter.js

The following snippet shows how to read the contents of the file Markdown.Converter.js and inject it into your JSContext.

// get path to the pagedown source file
let path = NSBundle.mainBundle().pathForResource("Markdown.Converter", ofType: "js", inDirectory: "pagedown")

// get the contentData for the file
let contentData = NSFileManager.defaultManager().contentsAtPath(path!)

// get the string from the data
let content = NSString(data: contentData!, encoding: NSUTF8StringEncoding) as? String

// finally inject it into the js context
context.evaluateScript(content)

Now pagedown is ready to invoke within your JSContext. You can accomplish this with a simple snippet of JavaScript similar to this.

let script = "var converter = new Markdown.Converter();" +
    "var markdownText = \"# Hello World\";" +
    "converter.makeHtml(markdownText);"

let result = context.evaluateScript(script)

>> <h1>Hello World</h1>

Thus far the examples I’ve shown have data hard-coded into the JavaScript string. For simple tasks this might be fine. But often we want to inject some dynamic values, or even behavior, from the native context into the JavaScript context. Let’s try that next.

Injecting Native Objects

The simplest solution to injecting Swift values into your JSContext can be string interpolation. For example you can rewrite the map example from above using a native array instead. Here the native Swift array is rendered into the JavaScript string.

let array = [1,2,3,4,5]

let jsv = context.evaluateScript(
    "\(array).map(function(n){return n*n})"
)

>> 1,4,9,16,25

While this works, it is not truly injecting a native object. It’s string interpolation. JSContext has a better way using setObject:forKeyedSubscript:. Here is the example rewritten to first inject the array into the JSContext by setting a variable on the context’s global scope.

// Create a Swift array.
let array = [1,2,3,4,5]

// Set the swift array on the js context.
// It will be named `array`.
context.setObject(array, forKeyedSubscript: "array")

// Evaluate js that references the `array` variable we just set.
let jsv = context.evaluateScript(
    "array.map(function(n){return n*n})"
)

>> 1,4,9,16,25

Now you have seen how to successfully inject a native object into the JavaScript context. But this example still relies on the returned value. Next let’s explore how to read out a value from JavaScript into Swift.

Retrieving JavaScript Objects

Retrieving values is similar to setting them. We can easily rewrite the above to access the result as a variable on the JavaScript context instead of having it returned from the evaluateScript call.

// Create a Swift array.
let array = [1,2,3,4,5]

// Set the swift array on the js context.
// It will be named `array`.
context.setObject(array, forKeyedSubscript: "array")

// Evaluate js that references the `array` variable we just set,
// overwriting the array variable with the new value.
context.evaluateScript(
    "array = array.map(function(n){return n*n})"
)

// Pull the array back into native context
let mappedArray = context.objectForKeyedSubscript("array")

>> 1,4,9,16,25

It is worth noting here that the result of both evaluateScript and objectForKeyedSubscript is a JSValue object. JSValues maintain a strong reference to the JSContext they came from. The source says it like this:

All instances of JSValue originate from a JSContext and hold a strong reference to this JSContext. As long as any value associated with a particular JSContext is retained, that JSContext will remain alive.

To avoid memory leaks it is best to not retain these JSValues, but instead convert them into their native Swift types.

Casting to Native Types

Swift is a strongly typed language. It is best to inform the Swift compiler about the types we expect out of JavaScript to avoid problems later when we use those values.

The result of evaluateScript is a JSValue object. As the documentation describes, “a JSValue is a reference to a value within the JavaScript object space”. So even though we have a reference to it within Swift, it is not a Swift object. We must cast it to a native Swift object manually. JSValue has many to* methods, such as toArray, that help us with this.

let jsv = ...

let nativeArray = jsv.toArray()

>> [1, 4, 9, 16, 25]

toArray actually returns a typed array [AnyObject], where the values within the array are allowed to be anything. However, we often want Swift to know about the specific value types the array is allowed to contain. This is accomplish with the as operator. Here I have rewritten the previous example into an array containing only integers.

let jsv = ...

let nativeArray:[Int] = jsv.toArray() as! [Int]

>> [1, 4, 9, 16, 25]

You must be careful with this since you have no guarantee that your JSValue will actually contain the type you want. Here Swift provides some convenient guard patterns to protect your code. You can easily use if let… to protect against errors when the JSValue fails to cast, safely trapping the error.

let jsv = ...

// Our array contains ints, not strings.
// This will fail to cast.
if let nativeArray:[String] = jsv.toArray() as? [String] {
    print(nativeArray)
}
else {
    print("error casting array")
}

>> "error casting array\n"

Ideally, though, you will be encapsulating your code in small functions. Right? In this case you can use the new Swift 2.0 guard statement (which is better than if) to cleanly guard your code from improper value types.

let jsv = ...

func handleJavaScriptArray(value:JSValue) -> Void {
    guard let array:[String] = value.toArray() as? [String] else {
        print("error casting array")
        return
    }

    // Proceed knowing than `array` is the value type you expected
}

handleJavaScriptArray(jsv)

The JavaScriptCore Tradeoff

Using JavaScript within Swift is awesome. But it does come with a tradeoff — speed. The JavaScriptCore project has an incredible four-tier JIT compiler that can run js at near-native speeds. It actually uses the same LLVM compiler that Swift uses. However, because JIT compilers must create executable memory, only special white-listed apps are allowed to use it. Thus the JSContext objects you create can only run interpreted js, the slowest of the four compilation tiers available.

You can find this called out in Apple’s iOS Security Guide on page 18 (emphasis mine.)

Further protection is provided by iOS using ARM’s Execute Never (XN) feature, which marks memory pages as non-executable. Memory pages marked as both writable and executable can be used only by apps under tightly controlled conditions: The kernel checks for the presence of the Apple-only dynamic code-signing entitlement. Even then, only a single mmap call can be made to request an executable and writable page, which is given a randomized address. Safari uses this functionality for its JavaScript JIT compiler.

Thus Safari, and WKWebView objects in iOS 8+, are allowed to run JavaScript at full-speed. But your apps and mine are not.

I have not yet been able to confirm the same is true of OS X apps, other than anecdotal evidence. My suspicion is sandboxed apps from the OS X App Store are held to similar restrictions as iOS apps.

How bad is the speed hit? Opinions vary. Andy Matuschak of the Kahn Academy ran some benchmarks and described the performance as “poor”. However Telerik, who makes NativeScript, cites the performance hit to be about 10%.

In my own work on EveryDollar I have found the performance of JavaScriptCore to be quite acceptable. We are careful to use it for small, discrete tasks where the benefit of leveraging an existing JavaScript library far outweighs any minor performance hit we might encounter.

Wrap Up

Hopefully this gives you a good taste of how you can begin to leverage JavaScriptCore within your native Swift apps. At the moment there is no really solid documentation on JavaScriptCore so the best place to go is the source itself. You can either access it via opensource.apple.com or by browsing the WebKit trak repo.