Site menu Jeneral Jelly: mix JS with native Android and iOS

Jeneral Jelly: mix JS with native Android and iOS

Most platforms can run HTML5 apps and Javascript code. I have been using Javascript and HTML5 to implement core parts of larger, multi-platform mobile apps. The inverse situation is also possible — to reuse a C library in Web and Node.js environments. I have written a bit about Emscripten which compiles C directly to JS (or WebAssembly) and spares us from messing with native interfaces.

Running an HTML5 applet inside a native Android or iOS app has its quirks, but it is generally well-documented and well-supported by both platforms. I don't feel the need of writing yet another piece of text about this. (If you disagree, drop a comment so I may consider amending the text.)

Here I intend to show a "cheat sheet" of how to run pure, non-HTML5 JS code inside an app. For example, an app could implement its Model or business logic in JS, while the UI is fully native.

The Rule of Four

Interfacing native and JS code is a set of four dissimilar problems:

The documentation typically mentions the simplest cases, concentrating on how to pass parameters of primitive types, and glossing over the conversion of compound types and/or how to handle the function result.

Because of this, and because I like to keep things as simple as possible, I tend to avoid returning values in either direction. I avoid to send or receive anything more complex than an array; JSON strings are safer. All native functions callable from JS are bound as properties of a single gateway object.

Another reason I don't return values often, is because the JS code may run in a different thread.

Threading

It may be desirable to run the Javascript interpreter in a separate thread. Even if your JS calls are fast and don't do any intensive processing, the initial script loading may block the UI thread for too long.

On the other hand, every call from UI thread to the JS thread and vice-versa must be mediated by some sort of a queue. It is never a good idea to let different threads run the same code, and (generally) a non-UI thread cannot update the UI. Moreover, queued function calls are run asynchronously and cannot return the results (at least not trivially).

There are many ways to use threads. In iOS, I do it indirectly, using a user-initiated DispatchQueue. Every call from native to JS is posted in this queue. And every call from JS to native is posted in DispatchQueue.main, which is conveniently served by the UI thread.

In Android, I employ a subclass of Thread to encapsulate the JS interpreter, and Handlers are employed as queues. In Android, the handler queue is served by the thread context in which the handler was created, so the JS-to-UI handler has to come from outside the Thread class.

Available APIs

Most non-HTML5 Javascript interpreters deliver a "clean slate" environment. Save for the primitive types and general APIs like Math and JSON, nothing else is available. There is no window, no document, and notably the setTimeout() family is absent. If you need timeouts, you have to implement them as native functions callable by JS.

Implementing an asynchronous call like setTimeout() seems to imply passing closures from JS to native side, which is possible, but messy and poorly documented. I skip this problem by keeping part of the implementation at JS side; only integer handlers are exchanged between JS and native.

iOS

The iOS platform has this wonderful thing: JavaScriptCore. This is a standard component that exposes the WebKit/Safari interpreter. This means first-class support for non-HTML5 Javascript execution. It is well-documented, save for a couple dusty corners.

let jvm = JSVirtualMachine()!
// many contexts may stem from a single jvm
let x = JSContext(virtualMachine: self.jvm)!

// log any JS exception
x.exceptionHandler = { context, exception in
    if let e = exception!.toString() {
        print("\n\n#### engine exception: " + e + "\n\n")
    }
}

// load script       
x.evaluateScript(script_as_string)

// the 'gateway' or trampoline object must have been instantiated
// by the JS script evaluated above
let gateway = x.objectForKeyedSubscript("gateway" as NSString)

// export a native function to JS       
let a: @convention(block) (Int, [Double], String) -> () = { a, b, c in
    return native_function(a, b, c)
}
// native function is added to the 'gateway' object
gateway!.setObject(q, forKeyedSubscript: "native_fun" as NSString)
// can be called as "gateway.native_fun(...)" at JS side
 
// one way to call a JS function from native
// (not good when passing string parameters)
let cmd = String(format: "js_function(%d);", int_value)
x.evaluateScript(cmd)

// another, more civilized way to call a JS function
x.objectForKeyedSubscript("js_function2")
    .call(withArguments: [int_value, string_value])

Android

Android does not offer a standard API equivalent to JavaScriptCore, so the first step is to select some third-party library. This excellent article lists some available options.

A common trick is to use an invisible WebView as Javascript interpreter. It works, it leverages a standard component, but it is a bit too kludgey for my taste. Moreover, the article linked above cites a couple problems with this approach (noting they may have been resolved since 2016).

I still use the Rhino interpreter. Since it is written in Java, you just need to drop a JAR in your project, no need to mess with NDK. Rhino just works. It is slow, which is not a big problem for me, but may be for you.

Rhino does not see much development these days, and does not implement the latest Javascript features. The latter is the main reason that made me look for a replacement. There are some other quirks as well e.g. passing arrays from JS needs Rhino-specific code at JS side. So, I won't write about Rhino interfacing. (If you feel I should, drop a comment.)

The best option so far seems to be the J2V8 project. As the name suggests, it is an adapter of V8 interpreter (used in Node and Google Chrome) to Java, and Android. Adding it to your Android project takes a single line in build.gradle:

implementation "com.eclipsesource.j2v8:j2v8:6.2.0@aar"

Using a C/C++ library in Android means the dreaded NDK and JNI. But, if you use the AAR package supplied by J2V8 and distribute your app using bundles, you can ignore the fact J2V8 is native.

import com.eclipsesource.v8.*
var v8 = V8.createV8Runtime()

// here we create the gateway object from scratch
val gateway = V8Object(v8)

// native function that does not return data
val a = JavaVoidCallback { _, parameters ->
    val p0 = parameters.getString(0)
    val p1 = parameters.getInteger(1)
    ...
}
gateway.registerJavaMethod(a, "aa")

// native function with parameter array of integers
val b = JavaVoidCallback { _, parameters ->
    val p0 = parameters.getArray(0)
    val p0a = p0.getIntegers(0, p0.length())
    p0.close()
    ...
}
gateway.registerJavaMethod(b, "bb")

// Native function that returns string
val c = JavaCallback { sender, parameters ->
    "bla"
}
gateway.registerJavaMethod(c, "cc")

// Add 'gateway' to main scope
v8.add("gateway", gateway)
gateway.close()

// load main script
v8.executeVoidScript(main_script)

// add or change some property from existing object
val o = v8.getObject("someobject")
o.add("some_bool_property", true)
o.close()

// Call jsfunction("aaa", 3)
val params = V8Array(v8).push("aaa").push(3)
try {
    v8.executeVoidFunction("jsfunction", params)
} catch (e: V8ScriptExecutionException) {
    Log.e(TAG, e.message)
    Log.e(TAG, e.jsStackTrace)
}
params.close()

v8.terminateExecution()
try {
    v8.close()
} catch (e: IllegalStateException) {
    // exception raised when there are leaked objects
    e.printStackTrace()
}

One important quirk of J2V8 is the necessity of releasing objects manually — look for close() instances in the code above. The release is necessary even in cases where the object clearly belongs to, or whose ownership has been moved to, the JS side.

The methods executeVoidScript() and executeVoidFunction() naturally don't return objects. If you use the similar methods executeScript() and executeFunction(), they may return objects which must be released.

Besides the increased memory usage, the only visible effect of leaking objects is raising an exception when the interpreter itself is released, which can (and probably should) be caught. The exception reports the number of leaked items and looks more like a debugging/lint tool.