company news Developing a mobile cross-platform library – Part 3. JavaScript

All articles

Developing a mobile cross-platform library – Part 3. JavaScript

Developing a mobile cross-platform library in JavaScript.

Here, I am including the experience I had while exploring solutions for developing a mobile cross-platform library, i.e. a single codebase that could be part of mobile apps running under different platforms.

It covers my journey from mobile cross-platform developments tools (PhoneGap, Titanium, and the likes), code porting tools, and WebViews that weren’t up to the task, to C++ and JavaScript engines that did work. There aren’t many resources out there explaining how to approach this problem, so we thought it would be helpful if we shared this experience.

This is the third and final part which describes how a cross-platform library was implemented in JavaScript. In the first part, you can find out about the other candidate solutions that were investigated. The second part describes the steps taken while developing the library in C++.

We were particularly interested in developing our cross-platform library in JavaScript, as we all love JavaScript here in Skyscanner. Our first attempt at having JavaScript code running in an Android WebView and iOS UIWebView didn’t work (check the first part), as their APIs are very limiting. Naturally, our next thought was “What about a pure JavaScript engine?”.

The answer is that it is possible, but it can be challenging. One of our library’s requirements was to perform HTTP requests. However, standalone JavaScript engines do not provide networking functionality, so if we want to use such an engine, we’ll have to delegate the missing functionality to the respective platform (increasing the amount of glue code).

Hence, the minimum functionality we want from JavaScript is:

  • Executing scripts
  • Calling functions
  • Evaluating global variables and returned results
  • Performing callbacks (from JavaScript to Java/Objective-C)

Cross-platform library

One way of delegating missing functionality to the native code is through dependency injection. JavaScript stub methods are used as placeholders for the expected functionality, and they get overridden by appropriate callback functions when the library is loaded at run-time. In our sample library, we used this approach for logging and HTTP requests.

01: _// skyscanner-sdk.js_
02: 
03: _// Logging function._
04: _// This function is the first one to be overridden by _
05: _// a logging callback function at the glue-code level_
06: **var** log = **function**(text) {
07:     ;
08: };
09: 
10: _// Performing an HTTP request to a Skyscanner API._
11: _// This function also gets overridden once this script is loaded_
12: **var** performHttpRequest = **function**(fromAirport, toAirport, outboundDate) {
13:     **log**(""performHttpRequest()" function is a stub and should be overridden");
14: };
15: 
16: _// ..._

Once we have the stubs in our code, we can continue developing the cross-platform JavaScript library, as if the functionality is there. Our minimal library consisted of just one API method, getItineraries(). This method,

  1. calls performHttpRequest() with the appropriate arguments (which in turn, calls platform-specific code),
  2. parses the returned value,
  3. creates the necessary Itinerary and ItineraryLeg JavaScript objects,
  4. puts them inside a JavaScript Array,
  5. sorts them by price, and
  6. returns the Array.

    01: // skyscanner-sdk.js 02: 03: // … 04: 05: var getItineraries = function(fromAirport, toAirport, outboundDate) { 06: // perform HTTP request using the overridden performHttpRequest() callback 07: var jsonString = performHttpRequest(fromAirport, toAirport, outboundDate); 08: 09: // parse response and populate an array of JavaScript Itinerary objects 10: var itineraryArray = parseJson(jsonString); 11: 12: // sort it based on price 13: itineraryArray.sort(function(a, b) { 14: // sorting here 15: }); 16:
    17: return itineraryArray; 18: }; 19: 20: // …

As in the C++ library, the logic is minimal, but it’s enough to prove that the cross-platform library can be built.

After the library, we can continue with integrating the JavaScript engine and developing glue code for each platform.

iOS

One of the new impressive features in iOS 7 is the JavaScriptCore framework, which wraps the functionality of the JavaScriptCore engine. It supports executing JavaScript code, evaluating JavaScript variables and function calls, making callbacks, object transformations, and in general, it is a very powerful and easy-to-use framework.

iOS app architecture

In order to run JavaScript code, we need a JavaScript context with a global scope, which is wrapped in a JSContext instance. A JSContext runs inside a JavaScript Virtual Machine (JSVirtualMachine). We can have several VMs in our app, and several contexts inside each VM. Contexts inside a VM can exchange data, but this is not allowed across VMs.

1: _// initializing a JavaScript context inside a JavaScript Virtual Machine_
2: JSContext context = [[JSContext **alloc**]
3:                       **initWithVirtualMachine**:[[JSVirtualMachine **alloc**] **init**]];

Callbacks from JavaScript to Objective-C are possible through assigning JavaScript function names to Objective-C blocks. Argument conversion is performed automatically for all common types (BOOL, double, int32_t , uint32_t, NSNumbe r, NSString, NSDate, NSArray, NSDictionary), but this can also be extended to other custom types, given that they conform to the JSExport protocol.

1: _// overriding log JavaScript function with an appropriate callback Objective-C block_
2: context[@"log"] = ^(NSString *text) {
3:     **NSLog**(@"JSLog: %@", text);
4: };

Since we didn’t have much JavaScript logic, all the JavaScript handling was put inside a single class; SkyscannerSdkWrapper. The constructor initializes a JSContext instance where all the JavaScript code runs. Upon initialization, the library is loaded to the context, and the JavaScript stubs are overridden by appropriate callback Objective-C blocks.

01: _//  SkyscannerSdkWrapper.m_
02: 
03: **#import** "SkyscannerSdkWrapper.h"
04: 
05: **#import** <JavaScriptCore/JavaScriptCore.h>
06: 
07: _// ..._
08: 
09: @interface SkyscannerSdkWrapper ()
10: 
11: _// the JavaScript context with the global object_
12: @property JSContext *context;
13: 
14: @end
15: 
16: @implementation SkyscannerSdkWrapper
17: 
18: - (**id**)**init** {
19:     **if** ((**self** = [super **init**]) != **nil**) {
20:         _// the path to the script_
21:         NSString *scriptFilePath = 
22:             [[NSBundle **mainBundle**] **pathForResource**:@"skyscanner-sdk" **ofType**:@"js"];
23:         _// the contents of the script_
24:         NSString *scriptFileContents = 
25:             [NSString **stringWithContentsOfFile**:scriptFilePath
26:                                       **encoding**:NSUTF8StringEncoding
27:                                          **error**:**nil**];
28: 
29:         _// initializing a JavaScript context inside a JavaScript Virtual Machine        _
30:         _context = [[JSContext **alloc**] 
31:                        **initWithVirtualMachine**:[[JSVirtualMachine **alloc**] **init**]];
32:         _// loading the script to the context_
33:         [_context **evaluateScript**:scriptFileContents];
34:         
35:         _// overriding log JavaScript function with an appropriate callback_
36:         _// Objective-C block_
37:         _context[@"log"] = ^(NSString *text) {
38:             **NSLog**(@"JSLog: %@", text);
39:         };
40: 
41:         _// overriding the HTTP request_
42:         _context[@"performHttpRequest"] = ^(NSString *fromAirport, 
43:                                             NSString *toAirport,
44:                                             NSDate *outboundDate) {
45:             _// perform request in Objective-C and return results to JavaScript_
46:             **return** [SkyscannerSdkWrapper **performObjCHttpRequestFrom**:fromAirport
47:                                                                  **to**:toAirport
48:                                                                **date**:outboundDate];
49:         };        
50:     }
51:     
52:     **return** **self**;
53: }
54: 
55: _// performs HTTP request and returns the results_
56: + (NSString *) **performObjCHttpRequestFrom**:(NSString *)fromAirport 
57:                                        **to**:(NSString *)toAirport
58:                                      **date**:(NSDate *)outboundDate {
59:     _// ..._
60: }
61: 
62: @end

JavaScript values and functions are wrapped in JSValue instances. There are many helper methods to make value conversions between Objective-C and JavaScript. The following code shows how JSValues are used for function calling and conversions between Objective-C and JavaScript. This method calls the getItineraries() JavaScript function, transforms its results (JavaScript Array of JavaScript objects) into an NSArray of Objective-C wrapper objects, and returns it.

01: _//  SkyscannerSdkWrapper.m_
02: 
03: _// ..._
04: 
05: **#import** "ItineraryWrapper.h"
06: 
07: _// ..._
08: 
09: @implementation SkyscannerSdkWrapper 
10: 
11: _// ..._
12: 
13: _// returns an NSArray of itineraries (Objective-C ItineraryWrapper objects)_
14: _// for the specified origin and destination airports and outbound date_
15: - (NSArray *)**getItineraryArrayFrom**:(NSString *)fromAirport **to**:(NSString *)toAirport
16:                               **date**:(NSDate *)outboundDate {
17:     _// find the getItineraries JavaScript function_
18:     JSValue *getItinerariesJsFunction = **self**.context[@"getItineraries"];
19:     _// call it and get the JavaScript Array of JavaScript Itinerary objects_
20:     JSValue *jsItineraryJsArray = [getItinerariesJsFunction
21:                                     **callWithArguments**:@[fromAirport, toAirport,
22:                                                         outboundDate]];
23: 
24:     _// NSArray to hold the Objective-C ItineraryWrapper objects to be returned_
25:     NSMutableArray *itineraryWrapperNsArray = [NSMutableArray **array**];
26:     _// length of the JavaScript Array_
27:     int length = [jsItineraryJsArray[@"length"] **toInt32**];
28:     **for** (int i = 0; i < length; i++) {
29:         _// get the JavaScript object_
30:         JSValue *jsItinerary = jsItineraryJsArray[i];
31:         _// create the wrapper object_
32:         ItineraryWrapper *wrapper = [[ItineraryWrapper **alloc**]
33:                                       **initWithJsItinerary**:jsItinerary];
34:         [itineraryWrapperNsArray **addObject**:wrapper];
35:     }
36:     
37:     **return** itineraryWrapperNsArray;
38: }
39: 
40: _// ..._
41: 
42: @end

In our approach, the Objective-C wrapper object contains the original JavaScript object as a JSValue. Consequently, when a wrapper object’s method is called, execution is delegated to the original object through the JSValue. Another approach would be to copy the JavaScript objects into Objective-C ones, but this would increase our glue code (we would have to rewrite all methods in Objective-C), and reduce the objects’ flexibility (they would no longer be accessible from JavaScript). Here’s a wrapper class implementation:

01: _//  ItineraryLegWrapper.m_
02: 
03: **#import** "ItineraryLegWrapper.h"
04: 
05: @interface ItineraryLegWrapper
06: 
07: _// the original JavaScript ItineraryLeg object handler_
08: @property JSValue *jsItineraryLeg;
09: 
10: @end
11: 
12: @implementation ItineraryLegWrapper
13: 
14: - (**id**)**initWithJsItineraryLeg**:(JSValue *)jsItineraryLeg {
15:     **if** ((**self **= [super **init**]) != nil) {
16:         _jsItineraryLeg = jsItineraryLeg;
17:     }
18:     **return** **self**;
19: }
20: 
21: - (int)**duration **{
22:     _// the duration represented by a JavaScript Number_
23:     JSValue *jsDuration = **self**.jsItineraryLeg[@"duration"];
24:     _// tranforming the JavaScript Number to int32_
25:     **return** [jsDuration **toInt32**];
26: }
27: 
28: _// ..._
29: 
30: @end

And that was it! We managed to implement all necessary functionality very easily (unlike the Android solutions that follow), thanks to the JavaScriptCore framework.

There isn’t much documentation about the framework, but useful information can be found in the header files, and WWDC 2013 (Session 615).

Android

Unfortunately, in Android, there is no such cool framework like JavaScriptCore, so we have to embed a JavaScript engine ourselves. The alternatives are:

  • Rhino – The most popular JavaScript Engine developed in Java, originally by Netscape, but now maintained by Mozilla. Unfortunately, its development is not very active, documentation at some points is inadequate and outdated, and the API is not very elegant.
  • Nashorn – Another JavaScript engine developed in Java by Oracle. It was released along with Java 8 a few days ago. Oracle has open-sourced the project, but unfortunately, it cannot run on Android’s Dalvik VM.
  • V8 – The JavaScript engine developed in C++ by Google, which powers the Chrome and Chromium web-browsers, and other projects like node.js. Open-source project.
  • SpiderMonkey – The first-ever JavaScript engine developed in C/C++, originally by Netscape, and now maintained by Mozilla. It powers the Firefox web-browser. Open-source project.
  • JavaScriptCore (the JavaScript engine, not the iOS framework) – The JavaScript engine developed in C/C++ by Apple, Adobe and others, which powers the Safari web-browser (among others). It’s part of the WebKit framework. Open-source project.

Two of them were tested; Rhino and V8. Significant efforts were made for testing SpiderMonkey and JavaScriptCore as well, but we ran into building errors, and decided not to spend more time on them.

Rhino

The first attempt was with Rhino. The jars were taken from Mozilla’s website. Release 1.7R4 is the latest official release (released in June 2012!!!), but a more recent version can be found at the github repository (last commit in February 2013).

In Android, the ${ANDROID_PROJECT_HOME}/assets/ directory is probably the most appropriate location to put your scripts, as they can be organized in a folder hierarchy, and it’s easy to retrieve them.

In Rhino, in order to run JavaScript code, we need a scope (Scriptable) and a context (Context). The scope holds the standard objects, along with their constructors, and the global object, while the context contains the execution environment, and the call stack. Scopes can be shared across threads, but contexts can be associated only with a single thread.

1: _// enter the Rhino Context_
2: Context rhinoContext = Context.**enter**();
3: _// disable optimizations and run in interpretive mode, otherwise it crashes on Android_
4: rhinoContext.**setOptimizationLevel**(-1); 
5: _// the global object_
6: Scriptable sharedJsScope = rhinoContext.**initStandardObjects**();

Setting callbacks at run-time is not very elegant. In order to override the placeholders, we need to inject a script as a String. Argument conversion from JavaScript to Java is performed automatically for all common types (boolean, double, int, String, Date, Array, etc.). The opposite conversion (Java to JavaScript) is possible for all types, since Rhino uses reflection to access the Java fields and methods from JavaScript.

1: _// overriding the log function and setting the callback to Android's Log._
2: _// we are also providing a fake filename and the number of the first line _
3: _// for debugging purposes (useful messages upon crashing)_
4: rhinoContext.**evaluateString**(sharedJsScope,
5:                             "log = function(text) {n" +
6:                                 "android.util.Log.d("RhinoLog", text);n" +
7:                             "};", 
8:                             "override-log-callback.js", 1, **null**);

Again, in our sample cross-platform library we used a single class to wrap our library and handle Rhino. The constructor takes care of initializing the JavaScript scope, loading the library and overriding stubs with appropriate callbacks.

01: _// SkyscannerSdkWrapper.java_
02: 
03: **package** net.skyscanner.mobilesdk;
04: 
05: _// ..._
06: 
07: **public** **class** SkyscannerSdkWrapper {     
08:         
09:     **private** Scriptable sharedJsScope;
10:         
11:     **private** **SkyscannerSdkWrapper**(Activity activity) {
12:         _// enter the Rhino Context_
13:         Context rhinoContext = Context.**enter**();
14:         _// Disable optimizations and run in interpretive mode, _
15:         _// otherwise it crashes on Android_
16:         rhinoContext.**setOptimizationLevel**(-1); 
17:         _// the global object_
18:         sharedJsScope = rhinoContext.**initStandardObjects**();
19: 
20:         _// load the library_
21:         **loadScript**(activity.**getAssets**(), rhinoContext, sharedJsScope,
22:                    "skyscanner-sdk.js");
23: 
24:         _// overriding the log function and setting the callback to_
25:         _// Android's Log. We are also providing a fake filename and_
26:         _// the number of the first line for debugging purposes_
27:         _// (useful messages upon crashing)_
28:         rhinoContext.**evaluateString**(sharedJsScope,
29:             "logCallback = function(text) {n" +
30:                 "android.util.Log.d("RhinoLog", text);n" +
31:             "};", 
32:             "override-log-callback.js", 1, **null**);
33: 
34:         _// similarly for the performHttpRequest callback_
35:         rhinoContext.**evaluateString**(sharedJsScope, 
36:             "performHttpRequest = function(fromAirport, toAirport, " +
37:                                                 "outboundDate) {n" +
38:                 "return net.skyscanner.mobilesdk.SkyscannerSdkWrapper." +
39:                     "performJavaHttpRequest(fromAirport, toAirport, outboundDate);n" +
40:             "};", 
41:             "override-api-request.js", 1, **null**);
42:                 
43:         _// exiting the Context_
44:         Context.**exit**();
45:     }
46:         
47:     **private** **static** void **loadScript**(AssetManager assetManager,
48:                                    Context rhinoContext,
49:                                    Scriptable scope, String filename) {
50:         _// InputStream for the script's contents_
51:         InputStream stream = **null**;
52:                 
53:         **try** {
54:             _// initialize the stream and a reader_
55:             stream = assetManager.**open**(filename);
56:             InputStreamReader reader = **new** **InputStreamReader**(stream);
57:                         
58:             _// load the script to the scope_
59:             rhinoContext.**evaluateReader**(scope, reader, filename, 0, **null**);
60:         } **catch** (IOException e) {
61:             e.**printStackTrace**();
62:         } **finally** {
63:             _// cleaning up_
64:             **try** {
65:                 **if** (stream != **null**) {
66:                     stream.**close**();
67:                 }
68:             } **catch** (IOException e) {
69:                 e.**printStackTrace**();
70:             }
71:         }
72:     }
73:         
74:     _// performs the HTTP request and returns the results  _
75:     **public** **static** String **performJavaHttpRequest**(String fromAirport, String toAirport,
76:                                                 Date outboundDate) {
77:         _// ..._
78:     }
79: 
80:     _// ..._
81:  
82: }
83: 

Similarly to JavaScriptCore framework’s JSValue, all JavaScript objects in Rhino implement the Scriptable interface. Object transformations are performed through explicit type casting or Rhino helper functions (Context.toString()). Here’s the respective getItineraries() method for Rhino.

01: _// SkyscannerSdkWrapper.java_
02: 
03: **package** net.skyscanner.mobilesdk;
04: 
05: _// ..._
06: 
07: **public** **class** SkyscannerSdkWrapper {
08:         
09:         _// ..._
10: 
11:         _// calls the getItineraries() JavaScript function, gets the returned_
12:         _// JavaScript Array of JavaScript Itinerary objects, transforms it into_
13:         _// a Java Array of ItineraryWrapper objects and returns it_
14:         **public** ItineraryWrapper[] **getItineraryArray**(String fromAirport,
15:                                                     String toAirport, 
16:                                                     Date outboundDate) {
17:                 Context rhinoContext = Context.**enter**();
18: 
19:                 _// find the getItineraries() JavaScript method_
20:                 Function getItinerariesJsFunction =
21:                     (Function) sharedJsScope.**get**("getItineraries", sharedJsScope);
22: 
23:                 _// the arguments to be passed_
24:                 Object[] arguments = **new** Object[] {fromAirportUpper,
25:                                                    toAirportUpper,
26:                                                    outboundDate};
27:                 _// call the JavaScript function and get the results_
28:                 Object resultJs = getItinerariesJsFunction.**call**(rhinoContext,
29:                                                                 sharedJsScope, 
30:                                                                 sharedJsScope,
31:                                                                 arguments);
32:                 
33:                 _// casting it to a JavaScript Array_
34:                 NativeArray jsArray = (NativeArray) resultJs;
35:                 _// the Java Array to be returned _
36:                 ItineraryWrapper [] javaArray =
37:                     **new** ItineraryWrapper[(int) jsArray.**getLength**()];
38: 
39:                 _// getIds() returns the object's property names._
40:                 _// in case of an array, these are the indices_
41:                 **for** (Object o : jsArray.**getIds**()) {
42:                     int index = (Integer) o;
43:                     javaArray[index] =
44:                         **new** **ItineraryWrapper**((Scriptable) jsArray.**get**(index, **null**));
45:                 }
46:                         
47:                 Context.**exit**();
48: 
49:                 **return** javaArray;
50:         }
51: }

Similarly to the JavaScriptCore framework, Rhino passes objects by reference, so we can hold the JavaScript references (handles) inside Java wrapper objects. Consequently, when some Java wrapper method is called, the JavaScript object is found through the handle, and functionality is delegated to the original object.

01: _// ItineraryLegWrapper.java_
02: 
03: **package** net.skyscanner.mobilesdk;
04: 
05: _// ..._
06: 
07: **public** **class** ItineraryLegWrapper {
08: 
09:         _// the handle to the original JavaScript object_
10:         **private** Scriptable jsItineraryLeg;
11:         
12:         **public** **ItineraryLegWrapper**(Scriptable jsItineraryLeg) {
13:                 **this**.jsItineraryLeg = jsItineraryLeg;
14:         }
15:         
16:         **public** int **getDuration**() {
17:                 int result = (Integer) jsItineraryLeg.**get**("duration",
18:                                                           jsItineraryLeg);
19:                 **return** result;
20:         }
21: 
22:         _// ..._
23: 
24: }

Since we are embedding a JavaScript Engine, we need to check the resulting app size. Thankfully, it was only increased by 2.6MB, which should be acceptable for most apps, given that a HelloWorld app needs approximately 1MB.

This is how we interfaced our library with Rhino! Rhino is written Java, so it can be easily embedded in Android, and the app size increase is insignificant. However, the API is not very elegant, and its development has been inactive for quite a long time, so we probably need to look for alternatives.

Documentation (incomplete and outdated at some points) about the API can be found in the Javadocs included along with the source, and some examples can also be found in the MDN website.

V8

(In this section, some of the Android NDK and JNI concepts mentioned in the C++ part are revisited, so some details about them might not be covered. In case you need any more information, please refer to the C++ part, Android NDK documentation inside ${NDK_HOME}/docs/, and Oracle’s website.)

Since Rhino is not actively developed, we decided to continue exploring other engines for Android. The next engine to be tested was V8. As V8 is developed in C++, once more, we have to take the dark path of building with the Android NDK and interfacing through JNI.. In addition, there is one more level of translations. In Rhino, we had object translations and callback functions only between Java and JavaScript, while in V8, they have to be performed both between Java and C++ and between C++ and JavaScript. As a result, this solution is quite complex.

Building V8

Thankfully, V8 has Android as one of its build targets, so we just need to provide it with the Android toolchain paths. Here’s the script used for generating the Android NDK toolchains:

01: _# buildToolchain.sh_
02: 
03: _#!/bin/sh_
04: 
05: _# the NDK directory_
06: NDK_HOME=$1
07: _# the directory to store the toolchains_
08: BASE_TOOLCHAIN_DIR=$2
09: _# the platform (target version) for which the toolchain _
10: _# should be generated. android-9 used here_
11: PLATFORM=$3
12: _# target architecture: armeabi, armeabi-v7a, x86, or mips_
13: ARCH=$4
14: _# the host system, e.g. linux-x86_64, darwin-x86_64_
15: SYSTEM=$5
16: 
17: **export** ANDROID_NDK_ROOT=${NDK}
18: 
19: TOOLCHAIN_DIR=${BASE_TOOLCHAIN_DIR}/${ARCH}
20: 
21: mkdir -p ${TOOLCHAIN_DIR}
22: 
23: **if** [ "${ARCH}" = "mips" ] || [ "${ARCH}" = "x86" ]; **then**
24:     ARCH_SWITCH=${ARCH}
25: **fi**
26: 
27: ${NDK_HOME}/build/tools/make-standalone-toolchain.sh 
28:     --platform=${PLATFORM} 
29:     --install-dir=${TOOLCHAIN_DIR} 
30:     --arch=${ARCH_SWITCH} 
31:     --system=${SYSTEM}

Afterwards, V8 can be built using the respective generated toolchains. Here’s how you can build it for the armeabi Android ABI target:

01: _# buildV8Armeabi.sh_
02: 
03: _#!/bin/sh_
04: 
05: _# the path to the generated armeabi toolchain_
06: **export** ANDROID_TOOLCHAIN=$1
07: 
08: _# target architecture is android_arm ("android_ia32" and "android_mipsel"_
09: _# are also supported)_
10: _# target mode is debug (could also be set to "release")_
11: _# set number of concurrent jobs to 16_
12: _# remove internationalization support for outputting smaller binary_
13: make android_arm.debug -j16 i18nsupport=off

In addition, the following line has to be added to the build/android.gypi file, so that it outputs normal archives, and not just thin ones.

  _# ..._

          ['_type=="static_library"', {
            'ldflags': [
              _# Don't export symbols from statically linked libraries._
              '-Wl,--exclude-libs=ALL',
            ],
**+     'standalone_static_library': 1,**
          }],

  _# ..._

Now, there are 2 interesting binaries in ${V8_HOME}/out/build/android_arm.release/obj.target/tools/gyp/; libv8_base.arm.a and libv8_nosnapshot.arm.a. In our example, we put them inside ${ANDROID_PROJECT_HOME}/jni/v8/, along with their header files and an Android.mk Makefile which contains instructions about how to include the library in the final app bundle. Alternatively, you can put these files in any other directory, but you have to make sure to add that directory to the ${NDK_MODULE_PATH} environment variable.

Wrapping

Since we have the V8 binaries and their header files, we can continue with the implementation of the C++ and Java JNI wrappers, which are connecting V8 (and our library) with Java, and handle all object transformations. The C++ wrappers are located in ${ANDROID_PROJECT_HOME}/jni/, while the Java ones can be found inside ${ANDROID_PROJECT_HOME}/src/, along with the rest of the Java source code.

In order to avoid too many transformations between Java and C++, it was decided to push as much glue-code as possible to the C++ wrappers, away from the Java ones. As a result, the only functionality that was left in the Java wrappers was performing HTTP requests (actually, this could also be done in C++ if we embedded libcurl, but it would require too much work and the app size would increase even more).

V8’s API is much more sophisticated compared to Rhino’s. Firstly, we have local and persistent handles. On the one hand, local handles are held on a stack and their lifetime is determined by a handle scope, created at the beginning of a function call. When the handle scope is deleted, all the objects referenced by the local handles are deleted too. Local handles have the class Local<SomeType>. On the other hand, persistent handles are not held on a stack and can only be deleted explicitly. They are used when an object has to be used in more than one function calls. Persistent handles have the class Persistent<SomeType>.

In order to run JavaScript code, we need an isolated instance of V8 (Isolate), a handle scope (HandleScope), and an execution context (Context). Once we have these objects, our script can be compiled and run.

_// get an isolated V8 instance_
Isolate *isolate = v8::Isolate::**GetCurrent**();

_// stack-allocated class that governs a number of local handles._
HandleScope **handle_scope**(isolate);

_// sandboxed execution context with its own set of built-in objects and functions._
Handle<Context> context = Context::**New**(isolate);

_// stack-allocated class which sets the execution context for all operations_
_// executed within a local scope. _
Context::Scope **context_scope**(context);

_// Converting C strings to JavaScript ones is performed with_
_// String::NewFromUtf8(isolate, cstring)_
Handle<String> script_v8string = String::**NewFromUtf8**(isolate, script_cstring);
Handle<String> filename_v8string = String::**NewFromUtf8**(isolate, "skyscanner-sdk.js")

_// compile the script and provide a filename for debugging purposes_
Handle<Script> script = Script::**Compile**(script_v8string, filename_v8string);

_// run the script_
script->**Run**();

In order to override the JavaScript stubs, a reference to the global object is obtained, and the stubs are overridden with C++ functions wrapped in a v8::FunctionTemplate object. These C++ functions are void functions which take a single const FunctionCallbackInfo<Value> argument, which encapsulates the actual JavaScript arguments, the value to be returned and also contains some information about the JavaScript function call.

01: _// the C++ function which overrides logCallback in JavaScript_
02: void **logCallback**(**const** v8::FunctionCallbackInfo<v8::Value>& args) {
03:     _// create a HandleScope for the variables here_
04:     HandleScope **scope**(args.**GetIsolate**());
05: 
06:     _// get the first argument_
07:     Handle<Value> arg = args[0];
08: 
09:     _// conversion to get the C string_
10:     String::Utf8Value **value**(arg);
11: 
12:     _// call the Android NDK logging function_
13:     **__android_log_print**(ANDROID_LOG_DEBUG, "V8Log", "%s", *value);
14: }
15: 
16: _// ..._
17: 
18: _// get the global object_
19: Handle<Object> global_object = context->**Global**();
20: 
21: _// the JavaScript string name of the callback (stub) function_
22: Handle<String> log_function_name = String::**NewFromUtf8**(isolate, "log");
23: _// the FunctionTemplate _
24: Handle<FunctionTemplate> log_function_template = FunctionTemplate::**New**(isolate,
25:                                                                        logCallback);
26: 
27: _// override the JavaScript log() function with the C++ one_
28: global_object->**Set**(log_function_name, log_function_template->**GetFunction**());
29: 
30: _// ..._

C++ wrappers have access to only a small part of the Android framework (logging, asset management, etc.). Consequently, if we want to access other parts of it from JavaScript (e.g. HTTP requests), the C++ callback functions will need to call Java methods.

01: _// the Java VM is stored for looking up the JNIEnv_
02: _// (initialized at the initLibrary() function)_
03: **static** JavaVM *vm = 0;
04: 
05: _// method for instantiating a java.util.Date object from milliseconds_
06: jobject **jdate**(JNIEnv *env, long time_in_millis) {
07:     _// ..._
08: }
09: 
10: _// the callback from JavaScript to Java (through C++)_
11: void **performCppHttpRequest**(**const** v8::FunctionCallbackInfo<Value>& info) {
12:     _// first argument is the origin airport_
13:     String::Utf8Value **from_airport_jsstring**(info[0]);
14:     _// second argument is the destination airport_
15:     String::Utf8Value **to_airport_jsstring**(info[1]);
16:     _// third argument is the outbound date. Notice how V8 handles_
17:     _// date conversions_
18:     Handle<Date> outbound_jsdate = Handle<Date>::**Cast**(info[2]);
19: 
20:     _// get the JNIEnv, necessary for calling JNI methods_ 
21:     JNIEnv *env = 0;
22:     vm->**AttachCurrentThread**(&env, 0);
23: 
24:     _// convert JavaScript arguments to Java objects_
25:     jstring from_airport_jstring = env->**NewStringUTF**(*from_airport_jsstring);
26:     jstring to_airport_jstring = env->**NewStringUTF**(*to_airport_jsstring);
27:     jobject outbound_jdate = **jdate**(env, outbound_jsdate->**ValueOf**());
28: 
29:     _// find the Java class where the performJavaHttpRequest method is_
30:     jclass sdk_wrapper_jclass =
31:         env->**FindClass**("net/skyscanner/mobilesdk/SkyscannerSdkWrapper");
32:     _// the method's signature according to JNI specifications_
33:     **const** char *signature =
34:         "(Ljava/lang/String;Ljava/lang/String;Ljava/util/Date;)Ljava/lang/String;";
35:     _// get the method's id in order to call it_
36:     jmethodID perform_post_jmethodID = env->**GetStaticMethodID**(sdk_wrapper_jclass,
37:                                                               "performJavaHttpRequest",
38:                                                               signature);
39: 
40:     _// call the Java method with the Java arguments_
41:     _// the method is a synchronous HTTP request with returns the response_
42:     _// as a Java String_
43:     jstring json_response_jstring =
44:         (jstring) env->**CallStaticObjectMethod**(pricing_api_request_class,
45:                                               perform_post_jmethodID,
46:                                               from_airport_jstring,
47:                                               to_airport_jstring,
48:                                               outbound_jdate);
49: 
50:     _// convert to a C string_
51:     **const** char *json_response_cstring = env->**GetStringUTFChars**(json_response_jstring,
52:                                                                0);
53: 
54:     _// convert to a JavaScript string_
55:     Handle<String> json_response = String::**NewFromUtf8**(info.**GetIsolate**(),
56:                                                        json_response_cstring);
57: 
58:     _// return the response_
59:     info.**GetReturnValue**().**Set**(json_response);
60: 
61:     _// need to clean up the C string returned by GetStringUTFChars_
62:     env->**ReleaseStringUTFChars**(json_response_jstring, json_response_cstring);
63: }

Again, the script is probably best to be located in ${ANDROID_PROJECT_HOME}/assets/. Thankfully, there is an Android NDK API for asset management, and it can be read in the C++ wrappers. The only object needed from Java is an AssetManager instance.

01: **#include** <android/asset_manager_jni.h>
02:     
03: _// taken from http://stackoverflow.com/a/10428200_
04: _// takes a JNIEnv, an AssetManager Java object and the filename_
05: _// as arguments and returns a char array that needs to be explicitly deleted_
06: **const** char ***readAssetFile**(JNIEnv *env, jobject assetManager,
07:                           **const** char *filename) {
08:     _// get the C++ AssetManager_
09:     AAssetManager *mgr = **AAssetManager_fromJava**(env, assetManager);
10:     _// open the file_
11:     AAsset *asset = **AAssetManager_open**(mgr, filename, AASSET_MODE_UNKNOWN);
12:     **if** (asset == NULL) {
13:       **return** NULL;       
14:     }
15:
16:     _// the size of the buffer to create_
17:     long size = **AAsset_getLength**(asset);
18:     char *buffer = **new** char[size];
19: 
20:     _// copy the file contents in the buffer and close it_
21:     **AAsset_read**(asset, buffer, size);
22:     **AAsset_close**(asset);
23:         
24:     **return** buffer;
25: }

In our example, we had an initLibrary() function that takes care of all the above and is called when the app loads. In this function, we are storing two persistent JavaScript references; one for the global object, so that we can recreate the same context; and one for the getItineraries() method, so that we don’t have to look it up again in subsequent calls.

001: _// skyscanner_sdk.cpp_
002: 
003: **#include** <android/asset_manager_jni.h>
004: **#include** <jni.h>
005: **#include** <string>
006: **#include** <time.h>
007: **#include** <v8.h>
008: **#include** <vector>
009: 
010: **using** **namespace** v8;
011: 
012: _// preventing C++ name mangling in order to work with JNI_
013: **#ifdef** __cplusplus
014: **extern** "C" {
015: **#endif**
016: 
017:     _// the Java VM is stored for looking up the JNIEnv_
018:     **static** JavaVM *vm = 0;
019: 
020:     _// persistent handle to the JavaScript global object_
021:     **static** v8::Persistent<v8::Object> global_object_persistent;
022:     _// persistent handle to the JavaScript getItineraries method_
023:     **static** v8::Persistent<v8::Function> get_itineraries_function_persistent;
024:     
025:     void
026:     **Java_net_skyscanner_mobilesdk_SkyscannerSdkWrapper_initLibrary**(JNIEnv *env,
027:                                              jobject obj, jobject assetManager) {
028:         _// initialize vm_
029:         env->**GetJavaVM**(&vm);
030: 
031:         _// get an isolated V8 instance_
032:         Isolate *isolate = v8::Isolate::**GetCurrent**();
033:         _// if it is null, create a new one, and enter it_
034:         **if** (isolate == NULL) { 
035:             isolate = Isolate::**New**();
036:             isolate->**Enter**();
037:         }
038: 
039:         _// stack-allocated class that governs a number of local handles._
040:         HandleScope **handle_scope**(isolate);
041: 
042:         _// sandboxed execution context with its own set of built-in objects_
043:         _// and functions._
044:         Handle<Context> context = Context::**New**(isolate);
045:         _// stack-allocated class which sets the execution context for all operations_
046:         _// executed within a local scope. _
047:         Context::Scope **context_scope**(context);
048: 
049:         _// read the script from assets/_
050:         **const** char *script_cstring = **readAssetFile**(env, assetManager,
051:                                                    "skyscanner-sdk.js");
052: 
053:         _// Converting C strings to JavaScript ones is performed with_
054:         _// String::NewFromUtf8(isolate, cstring)_
055:         Handle<String> script_v8string = String::**NewFromUtf8**(isolate,
056:                                                              script_cstring);
057:         Handle<String> filename_v8string =
058:             String::**NewFromUtf8**(isolate, "skyscanner-sdk.js");
059: 
060:         _// compile the script and run it_
061:         Handle<Script> script = Script::**Compile**(script_v8string, filename_v8string);
062:         script->**Run**();
063: 
064:         _// get the global object_
065:         Handle<Object> global_object = context->**Global**();
066:         
067:         _// the JavaScript string name of the log() (stub) function_
068:         Handle<String> log_function_name = String::**NewFromUtf8**(isolate,
069:                                                                "log");
070:         _// the logCallback FunctionTemplate _
071:         Handle<FunctionTemplate> log_function_template =
072:             FunctionTemplate::**New**(isolate, logCallback);
073:         _// override the logCallback JavaScript function with the C++ one_
074:         global_object->**Set**(log_function_name, log_function_template->**GetFunction**());
075: 
076:         _// similarly for performHttpRequest_
077:         Handle<String> http_function_name =
078:             String::**NewFromUtf8**(isolate, "performHttpRequest");
079:         Handle<FunctionTemplate> http_function_template =
080:             FunctionTemplate::**New**(isolate, performCppHttpRequest);
081:         global_object->**Set**(http_function_name, http_function_template);
082:         
083:         _// store the JavaScript getItineraries() function as a persistent handle_
084:         _// for easier retrievals when the Java getItineraries method is called_
085:         Handle<String> get_itineraries_name = String::**NewFromUtf8**(isolate,
086:                                                                   "getItineraries");
087:         Handle<Value> get_itineraries_value =
088:             global_object->**Get**(get_itineraries_name);
089:         Handle<Function> get_itineraries_function =
090:             Handle<Function>::**Cast**(get_itineraries_value);
091:         get_itineraries_function_persistent.**Reset**(isolate, get_itineraries_function);
092: 
093:         _// set the global object's persistent handle_
094:         global_object_persistent.**Reset**(isolate, global_object);
095:     }
096: 
097:     _// ..._
098:     
099: **#ifdef** __cplusplus
100: }
101: **#endif**`

Since we have persistent handles, we can store JavaScript object references in the respective Java wrappers, like we did for Rhino and the C++ solution. However, this would result in:

  1. Garbage collection issues, as we cannot automatically deallocate the JavaScript object when the Java wrapper object gets garbage collected. Hence, if we kept JavaScript references, they would have to be explicitly deleted, which is not very Java-like. You can check the C++ solution for more info.
  2. Increased complexity, as all calls from Java to JavaScript have to go through the JNI wrappers.

As a result, in our app, the JavaScript objects are transformed (copied) into Java ones, before they are passed to Java.
Apart from initLibrary(), the second function that is exposed to Java is getJavaItineraries(). It takes care of translating the Java arguments into C++ and JavaScript, call the JavaScript getItineraries() function, get the JavaScript results, translate them into C++ and Java, and finally, return them to Java.

001: _// skyscanner_sdk.cpp_
002: 
003: _// ..._
004: 
005: **#ifdef** __cplusplus
006: **extern** "C" {
007: **#endif**
008:     
009:     _// ..._
010: 
011:     _// translates Java arguments to JavaScript, calls the JavaScript_
012:     _// getItineraries() method with the JavaScript arguments,_
013:     _// translates the returned JavaScript Itineraries to Java_
014:     _// ItineraryWrappers and returns them_
015:     jobjectArray
016:     **Java_net_skyscanner_mobilesdk_SkyscannerSdkWrapper_getItineraries**(JNIEnv *env, 
017:         jobject obj, jstring from_airport_jstring, jstring to_airport_jstring,
018:         jobject outbound_jdate) {
019: 
020:         _// translate the Java arguments into C++ ones_
021:         **const** char *from_airport_cstring =
022:             env->**GetStringUTFChars**(from_airport_jstring, 0);
023:         **const** char *to_airport_cstring =
024:             env->**GetStringUTFChars**(to_airport_jstring, 0);
025:         
026:         jclass date_jclass = env->**FindClass**("java/util/Date");
027:         jmethodID get_time_jmethodid = env->**GetMethodID**(date_jclass, "getTime",
028:                                                         "()J");
029:         jlong date_millis = env->**CallLongMethod**(date_jclass, get_time_jmethodid);
030: 
031:         _// get the isolated V8 instance associated with this thread_
032:         Isolate *isolate = Isolate::**GetCurrent**();
033:         
034:         HandleScope **handle_scope**(isolate);
035: 
036:         _// extract the global object from the persistent handle and initialize_
037:         _// a context_
038:         Handle<Object> global_object = Handle<Object>::**New**(isolate,
039:                                                            global_object_persistent);
040:         Handle<Context> context = Context::**New**(isolate, NULL,
041:                                                Handle<ObjectTemplate>(),
042:                                                global_object);
043:         Context::Scope **context_scope**(context);
044: 
045:         _// extract the getItineraries function from the persistent handle_
046:         Handle<Function> get_itineraries_jsfunction =
047:             Handle<Function>::**New**(isolate, get_itineraries_function_persistent);
048:         _// the arguments to be passed to getItineraries_
049:         Handle<Value> argv[] = {String::**NewFromUtf8**(isolate, from_airport_cstring),
050:                                 String::**NewFromUtf8**(isolate, to_airport_cstring),
051:                                 Date::**New**((double) date_millis)};
052: 
053:         _// call getItineraries with the arguments and cast it to a JavaScript Array _
054:         Handle<Value> itinerary_jsvalue =
055:             get_itineraries_jsfunction->**Call**(global_object, 3, argv);
056:         Handle<Array> itinerary_jsarray = Handle<Array>::**Cast**(itinerary_jsvalue);
057: 
058:         _// clean up the JNI C strings_
059:         env->**ReleaseStringUTFChars**(from_airport_jstring, from_airport_cstring);
060:         env->**ReleaseStringUTFChars**(to_airport_jstring, to_airport_cstring);
061: 
062:         _// convert the JavaScript Itinerary array to a Java ItineraryWrapper array_
063: 
064:         _// the JavaScript string representation of the JavaScript Itinerary's_
065:         _// outboundLeg field (which is an ItineraryLeg JavaScript object)_
066:         Handle<String> outbound_leg_field_jsname =
067:             String::**NewFromUtf8**(isolate, "outboundLeg");
068:         
069:         _// ..._
070:         _// more JavaScript strings of the JavaScript Itinerary's and_
071:         _// ItineraryLeg's fields_
072:         _// ... _
073: 
074:         _// find the Java ItineraryWrapper class, its constructor, and create an array_
075:         _// of ItineraryWrappers with length equal to the returned JavaScript Array_
076:         jclass itinerary_jclass =
077:             env->**FindClass**("net/skyscanner/mobilesdk/ItineraryWrapper");
078:         _// the constructor takes a Java ItineraryWrapper object as an agument_
079:         jmethodID itinerary_jconstructor = env->**GetMethodID**(itinerary_jclass,
080:                     "<init>", "(Lnet/skyscanner/mobilesdk/ItineraryLegWrapper;)V");
081:         jobjectArray itinerary_jarray =
082:             env->**NewObjectArray**((jsize) itinerary_jsarray->**Length**(),
083:                                                              itinerary_jclass, 0);
084: 
085:         _// ..._
086:         _// find the ItineraryLegWrapper constructor too_
087:         _// ..._
088: 
089:         _// construct the Java array_
090:         **for** (int i = 0; i < itinerary_jsarray->**Length**(); i++) {
091:             _// get the JavaScript Itinerary_
092:             Handle<Object> itinerary_js =
093:                 Handle<Object>::**Cast**(itinerary_jsarray->**Get**(i));
094:             _// get its outboundLeg field (an ItineraryLeg JavaScript object)_
095:             Handle<Object> outbound_jsleg =
096:                 Handle<Object>::**Cast**(itinerary_js->**Get**(outbound_leg_field_jsname));
097:             
098:             _// ..._
099:             _// get the outboundLeg's fields and create the ItineraryLegWrapper_
100:             _// object, outbound_jleg_
101:             _// ..._
102: 
103:             _// create the ItineraryWrapper object and put it in the array_
104:             jobject itinerary_j = env->**NewObject**(itinerary_jclass,
105:                                                  itinerary_jconstructor,
106:                                                  outbound_jleg);
107:             env->**SetObjectArrayElement**(itinerary_jarray, i, itinerary_j);
108:         }
109: 
110:         _// return the Java Array to Java_
111:         **return** itinerary_jarray;
112:     }
113: 
114: **#ifdef** __cplusplus
115: }
116: **#endif**

Along with the C++ wrappers, the following Android NDK Makefiles were added to ${ANDROID_PROJECT_HOME}/jni/:

01: _# Application.mk_
02: 
03: _# if APP_ABI is set to all, NDK will build for all possible ABIs_
04: _# (armeabi, armeabi-v7a, x86, and mips), otherwise armeabi is the default_
05: _# APP_ABI := all _
06: _# Targeting 2.3 (Gingerbread)_
07: APP_PLATFORM := android-9
08: _# STL to include_
09: APP_STL := stlport_shared
10: _# C++ compiler flag to add exceptions_
11: APP_CPPFLAGS += -fexceptions

01: _# Android.mk (for the JNI C++ wrappers)_
02: 
03: _# set the local path_
04: LOCAL_PATH := $(call my-dir)
05: 
06: _# clear previously set variables_
07: include $(CLEAR_VARS)
08: 
09: _# name of this module_
10: LOCAL_MODULE := mobile_sdk_js_v8_jni
11: _# source files to include_
12: LOCAL_SRC_FILES := skyscanner_sdk_jni.cpp
13: 
14: _# add logging and Asset support_
15: LOCAL_LDLIBS := -llog -landroid
16: _# this module depends on the v8 modules, imported at the end of this file_
17: LOCAL_STATIC_LIBRARIES := v8_base v8_nosnapshot
18: 
19: _# include the GNU Makefile script that is in charge of collecting all the information_
20: _# defined in LOCAL_XXX and determine what to build, and how to do it exactly_
21: include $(BUILD_SHARED_LIBRARY)
22: 
23: _# import the modules in the v8/ directory_
24: $(call import-module,v8)

This is the Android.mk file in ${ANDROID_PROJECT_HOME}/jni/v8/which was mentioned above and is responsible for the V8 binaries:

01: _# Android.mk (for the v8 modules)_
02: 
03: LOCAL_PATH := $(call my-dir)
04: 
05: _# here we define two modules for the two prebuilt V8 binaries_
06: 
07: include $(CLEAR_VARS)
08: LOCAL_MODULE    := v8_base
09: LOCAL_SRC_FILES := libv8_base.arm.a
10: 
11: _# the header files_
12: LOCAL_C_INCLUDES := include
13: LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
14: include $(PREBUILT_STATIC_LIBRARY)
15: 
16: include $(CLEAR_VARS)
17: LOCAL_MODULE    := v8_nosnapshot
18: LOCAL_SRC_FILES :=  libv8_nosnapshot.arm.a
19: include $(PREBUILT_STATIC_LIBRARY)

As in the C++ solution, appropriate Java wrappers were also developed, in order to bridge the Android app with the C++ JNI wrappers (and in turn, our cross-platform library).

01: _// SkyscannerSdkWrapper.java_
02: 
03: **package** net.skyscanner.mobilesdk;
04: 
05: **import** java.util.Date;
06: 
07: **import** android.app.Activity;
08: **import** android.content.res.AssetManager;
09: 
10: 
11: **public** **class** SkyscannerSdkWrapper {
12: 
13:     _// load the built libraries_
14:     **static** {
15:         System.**loadLibrary**("stlport_shared");
16:         System.**loadLibrary**("mobile_sdk_js_v8_jni");
17:     }
18:         
19:     **public** **SkyscannerSdkWrapper**(Activity activity) {
20:         _// get the assetManager and initialize V8_
21:         AssetManager assetManager = activity.**getAssets**();
22:         **initLibrary**(assetManager);
23:     }
24: 
25:     _// initialize V8 and the cross-platform library_
26:     **public** **native** void **initLibrary**(AssetManager assetManager);
27: 
28:     _// call the JavaScript getItineraries method via the JNI C++ code_
29:     **private** **native** ItineraryWrapper[] **getItineraries**(String fromAirport,
30:                                                      String toAirport,
31:                                                      Date date);
32:         
33:     _// performs a synchronous HTTP request to a Skyscanner API_
34:     _// this gets called from JavaScript via the JNI C++ code_
35:     **public** void **performJavaHttpRequest**(String fromAirport, String toAirport,
36:                                        Date date) {
37:         _// ..._
38:     }
39: 
40: }

Compared to Rhino, the key advantage of embedding V8 is its active development, and hence, stability. However, the app size increases significantly. Even if we disable the internationalization API (with the i18nsupport=off switch) the app still needs 8MB (+7MB) which is much larger than the respective Rhino app which needed 3.6MB (+2.6MB) of disk space. Another downside is the complexity of the architecture and the underlying code shown above.

The most updated documentation about V8 can be found in the header files, and useful example code is included in the github repository. There are also some tutorials by Google at the Google Developers website.

Wrapping up

As I mentioned at the top of this page, developing a cross-platform library in JavaScript can be challenging. Firstly, if we need functionality which is not part of the strict JavaScript (not web browser) specification, such as networking functionality, we have to delegate that functionality to the respective platform, increasing the amount of glue code. Secondly, we have to embed a JavaScript engine in Android, increasing the app size. Thirdly, if we choose Rhino, we have to take into account that it is not actively developed, while V8 needs building with the NDK, writing JNI wrappers, handling an extra level of translations, and increases the app size significantly. Nevertheless, all these downsides can be put aside if we want to develop a large piece of cross-platform logic in JavaScript, or reuse existing JavaScript code.

Hopefully, this will help some of you trying to develop a cross-platform library, or the ones reusing JavaScript code in iOS or Android!

Good luck coding!

PS I just found out about Duktape, an embeddable JavaScript engine, written in C. Apparently, it’s lightweight and easy to embed, so it might be worth a shot as a JavaScript engine for Android. However, you’ll still have to use the Android NDK and JNI.

android, cross-platform, ios, javascript, library, mobile, Technology