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,
- calls
performHttpRequest()
with the appropriate arguments (which in turn, calls platform-specific code), - parses the returned value,
- creates the necessary
Itinerary
andItineraryLeg
JavaScript objects, - puts them inside a JavaScript
Array
, - sorts them by price, and
returns the
Array
.01: // skyscanner-sdk.js02:03: // …04:05: var getItineraries = function(fromAirport, toAirport, outboundDate) {06: // perform HTTP request using the overridden performHttpRequest() callback07: var jsonString = performHttpRequest(fromAirport, toAirport, outboundDate);08:09: // parse response and populate an array of JavaScript Itinerary objects10: var itineraryArray = parseJson(jsonString);11:12: // sort it based on price13: itineraryArray.sort(function(a, b) {14: // sorting here15: });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.
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** 06: 07: _// ..._08: 09: @interface SkyscannerSdkWrapper ()10: 11: _// the JavaScript context with the global object_12: @property JSContext *context;13: 14: @end15: 16: @implementation SkyscannerSdkWrapper17: 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**:scriptFilePath26: **encoding**:NSUTF8StringEncoding27: **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**:fromAirport47: **to**:toAirport48: **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 *)toAirport58: **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 JSValue
s 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 *)toAirport16: **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 = [getItinerariesJsFunction21: **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 ItineraryLegWrapper06: 07: _// the original JavaScript ItineraryLeg object handler_08: @property JSValue *jsItineraryLeg;09: 10: @end11: 12: @implementation ItineraryLegWrapper13: 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=$107: _# the directory to store the toolchains_08: BASE_TOOLCHAIN_DIR=$209: _# the platform (target version) for which the toolchain _10: _# should be generated. android-9 used here_11: PLATFORM=$312: _# target architecture: armeabi, armeabi-v7a, x86, or mips_13: ARCH=$414: _# the host system, e.g. linux-x86_64, darwin-x86_64_15: SYSTEM=$516: 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=$107: 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
. 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
.
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::**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 script_v8string = String::**NewFromUtf8**(isolate, script_cstring);Handle filename_v8string = String::**NewFromUtf8**(isolate, "skyscanner-sdk.js")_// compile the script and provide a filename for debugging purposes_Handle