JavaScriptCore – The Holy Grail of Cross Platform

JavaScriptCore - The Holy Grail of Cross Platform thumbnail

The perfect cross-platform solution. The Holy Grail. The El Dorado of app development. The dream that multiple languages have promised, but none have delivered. As we speak, there are likely a dozen cross-platform projects in the R&D phase—all with a couple years worth of funding and all being built with the hopes of revolutionizing the way we develop software.

But why wait for them?

Cross platform is already here, and it’s not coming from a startup hoping to make their name with one “big bang” of a breakthrough. Today’s solution is already broadly adopted, and it’s backed by the largest companies in the industry.

No, it’s not snake oil or a silver bullet. It’s JavaScript. Wait! Hear me out. I’m not talking about a webpage wrapped in a native bundle masquerading as an app. I’m also not advocating that you adopt a crazy new framework that adds 200 files to your project, mapping every UI element to a JavaScript component. It’s better than that.

Lucid is one of hundreds of companies that all have the same exact need: an app that feels fast, responsive, and native to its platform, while at the same time sharing logic with apps on other platforms. In our case, that shared code is a data model with associated business logic that absolutely needs to be consistent across platforms.

So what’s the secret?

The secret is JavaScriptCore. Introduced back in 2013, JavaScriptCore exposes the complete JavaScript engine that backs your favorite WebKit-powered browser to non-browser developers.

The following walkthrough is going to be in Swift and JavaScript, but a similar setup is available for other platforms (including Android) as well.

The first step is to import the Apple provided framework into your project.

  1. In the project browser, click on your root level project file.
  2. Click on your main target > Build Phases > Link Binary with Libraries > click the + at the bottom and select JavaScriptCore.framework > Click Add.
  3. Go to your Swift file that you’d like to work in and add `import JavaScriptCore` to the top of your file.

That’s it! You’re ready to run some JavaScript inside your Swift or Objective-C app.

The class that we’re going to be spending all our time with is the `JSContext` class. The `JSContext` class represents a JavaScript execution context within a virtual machine. It is within this context that everything exciting will happen. Let’s start by initializing an instance.

private let context: JSContext = JSContext()

Next, there is one important property that we’d like to set before we get moving. These properties make it a little easier to debug any issues we might encounter.

context.exceptionHandler = { context, value in
    print("JSError: \(value!)")
}

Whenever a JavaScript exception would be surfaced in our console, this exception handler will be called. Don’t worry, JavaScriptCore is safe by default, so these exceptions will not crash your app, though they can end execution of your current script.

Note: Some JavaScript exceptions may surface as EXC_BAD_ACCESS but only when running with a debugger attached to your context.

Speaking of debugging, let’s go ahead and attach our Safari debugger to our JavaScriptContext. Start by building and running your app in Xcode. After the simulator is running, open Safari.

  1. Click Develop > Simulator > JSContext.
  2. Click Develop > Simulator > Automatically Show Web Inspector for JSContexts.

Step one opens a console so that you can debug your JavaScriptContext. Step two enables a setting that will automatically open a console whenever debugging a JavaScriptContext is available in your simulator.

From here, you can run any JavaScript code you want in your Safari console and it will execute within your app, just as if you were inspecting a web page. That’s not all though. Any JavaScript provided within your native code will also be available in the debugger. To illustrate this, go back to Xcode and create a variable in your JavaScript context.

self.context.evaluateScript("var x = 'Hello World’;”)

After building and running in Xcode, you can go back to your Safari console and simply type:

x;

The result will be Hello World printed to your console.

Now for a production app with a large amount of code, you definitely don’t want to add your JavaScript like this. At Lucid, we use TypeScript for almost everything on the web. That same TypeScript code is what we will be sharing with our native apps. We use various build targets that compile the code needed for each platform. In the case of our example today, we’re building the business logic that syncs our documents with remote changes—a large chunk of business logic that needs to be exactly the same across platforms.

Your process will likely be different from ours, but once you have your JavaScript written and ready to bring into your apps, you start by adding it to your project as a resource. Remember that our project for today was shared logic for syncing documents.

Note: You could also download your JavaScript from a remote source—the code for doing that is only slightly different, but I won’t go over it in this walkthrough.

Back in Xcode, add the JavaScript file as a resource:

  • File > Add files to Your Project Name > documentSyncer.js

Once you have your JavaScript file added to your project, you evaluate the JavaScript like this:

if let path = Bundle.main.path(forResource: "documentSyncer", ofType: "js"), 
    let documentSyncerJS = try? String(contentsOfFile: path) {
    var result = self.context.evaluateScript(documentSyncerJS)!
    print(result)
}

In our case, result is going to be undefined, but in your case, it will be whatever value your JavaScript returns, wrapped up in a JSValue object.

From here, the world of JavaScript is your oyster. In our case, documentSyncer.js defines a function called syncDocument where the parameters are a list of changes you’ve made to a document as well as a URL to identify the document remotely. What that means is that anytime we’d like to sync a document, we can do this:

self.context.evaluateScript("syncDocument(url, arrayOfChanges, completionHandler);”)

The result is an asynchronous task that synchronizes a document and its changes with the remote copy on the server. It can be reused for multiple documents, is thread safe, and—most importantly—is cross platform. We can use the same code for syncing Lucidchart documents on the web, Android, and iOS.

Before we wrap up, there are two things you need to know about JavaScriptCore. First, anything that you’re used to using that is NOT part of the JavaScript language won’t be available to you. For us, that meant we implemented our own versions of setInterval, setTimeout, and fetch. All of these are implemented by the browser, and since JavaScriptCore is a barebones engine, none of these are provided. If you’d like to see our implementations, they’re available here on GitHub.

The second thing you need to know is that the JavaScriptCore implementation of Promise is buggy. We never uncovered what the core issue is, other than that it can be unreliable. That’s why the above GitHub project contains an implementation of Promise provided by Bluebird. It was a drop-in replacement for us and immediately remedied all of the strange behaviors we were seeing when using the implementation of Promise that comes with JavaScriptCore.

If you’d like to go deeper into JavaScriptCore, NSHipster as well as Ray Wenderlich have very detailed write-ups that further explain how to use the other features of JavaScriptCore, as well as the small tweaks that need to be made when going from Objective-C to Swift. From there, I would also recommend watching the WWDC video provided by Apple. It discusses memory management for objects shared between languages.

If you have a suite of apps and need a cross-platform solution, JavaScriptCore can be a lifesaver. If you’re like us and one of those apps is a web app, JavaScriptCore is likely your only option. Hopefully this article helps you avoid some of the road blocks we discovered and puts you on the path to a seamless integration between your products.

2 Comments

  1. This is interesting stuff. I personally have just been writing cross-platform apps with Electron but I may have to look more into this. Why dont you add this post to Ciphly.com so that everyone can see it. It is a new Topsites Website geared towards helping developers find the newest, coolest stuff that they may not have heard of or thought of yet.

Your email address will not be published.