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.
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.
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 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.
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.
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 does not offer a standard API equivalent to JavaScriptCore, so the first step is to select some third-party library, or use WebView.
Currently, using an invisible WebView as Javascript interpreter is probably the best solution, and it leverages a standard component. It is one less dependency to fatten up your APK.
Interfacing between a visible WebView and native code has always been well-supported. Using invisible WebViews used to be kludgey, there were a couple problems with it. Was not a good solution in 2016 when I first considered it, but it is fine in 2026.
Interfacing between Kotlin and Javascript is fairly easy. To call a JS function from Kotlin, or to load JS source, or to set a variable, there is only one way: call WebView.evaluateJavascript(js_code). It is a blunt interface, and not the fastest if you need to send a lot of data from native to JS. But it works, and even has an optional resultCallback.
To call a Kotlin method from JS, there are two steps. Add some object to Javascript context by calling e.addJavascriptInterface(object, "name"). Then, mark the exported methods with the decorator @JavascriptInterface. Then you can call the Kotlin method from JS like name.method().
Make sure to use only primitive types as arguments or return values; compound types like lists or dictionaries must be encoded as JSON strings. (Some people argue they manage to pass compound types directly, but it is undocumented and obscure. Why bother when JSON codecs are so fast these days?)
NOTE: I no longer recommend using J2V8, since the project is abandoned and you would have to use a fork to make it run with recent Android versions. But FWIW I will keep the original text here.
For a long time, J2V8 project was possibly the best option to run JS in Android. 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 bringing the dreaded NDK to the table. But if you use the AAR package supplied by J2V8, and distribute your app using bundles, you will be spared of the native complications.
Snippets of code to interface with J2V8:
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.
For a long time, I used the Rhino interpreter. Since it is written in Java, you just need to drop a JAR in your project. 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.)