What is NativeScript anyways?


NativeScript is a runtime that lets you build native iOS, Android, and (soon) Windows Universal apps using JavaScript code. NativeScript has a lot of cool features, such as two-way data binding between JavaScript objects and native UI components, and a CSS implementation for native apps. But my favorite feature, and the subject of this article, is NativeScript’s mechanism for giving you direct access to native platform APIs.

It’s pretty awesome, but it can mess with your mind a little. For example, check out this code for a NativeScript Android app:

var time = new android.text.format.Time();
time.set( 1, 0, 2015 );
console.log( time.format( "%D" ) );

I’ll give your brain a minute or two to parse this, because yes, thisJavaScript code instantiates a Java android.text.format.Time()object, calls its set() method, and then logs the return value of itsformat() method, which is the string "01/01/15".

 

I’m with you Keanu, but hold on, because the rabbit hole gets deeper. Here’s one more example before we dive into the how this code actually works—this time for iOS:

var alert = new UIAlertView();
alert.message = "Hello world!";
alert.addButtonWithTitle( "OK" );
alert.show();

This JavaScript code instantiates an Objective-C UIAlertView class, sets its message property, and then calls its addButtonWithTitle() andshow() methods. When you run a NativeScript iOS app with this code you’ll see the alert below:

Pretty cool, huh?

One thing I should clarify before we dive into how all of this works: just because you can access native iOS and Android APIs, doesn’t mean NativeScript apps contain only JavaScript-ified Objective-C and Java code. NativeScript includes a number of cross-platform modules for common tasks, such as making HTTP requests, building UI components, and so forth. But that being said, most apps have some need to access native APIs occasionally, and the NativeScript runtime makes that access simple when you need it. Let’s look at how it works.

The NativeScript Runtime

The NativeScript runtime may seem like magic, but believe it or not, the architecture isn’t all that complex. Everything starts with JavaScript virtual machines, as they’re what NativeScript uses to execute JavaScript commands. Specifically, NativeScript uses V8 on Android andJavaScriptCore on iOS. Because NativeScript uses JavaScript VMs, all native-API-accessing code you write, including the code in the examples above, still needs to use JavaScript constructs and syntaxes that V8 and JavaScript Core understand.

Generally speaking, NativeScript tries to use the latest stable releases of both V8 and JavaScriptCore; therefore the ECMAScript language support in NativeScript for iOS is nearly identical to the support in desktop Safari, and the support in NativeScript for Android is nearly identical to the support in desktop Chrome. You can get an idea of what specific ES6 features that includes here.

Knowing that that NativeScript uses JavaScript VMs is important, but it’s only the first part of the puzzle. Let’s return to the first line of code in this article:

var time = new android.text.format.Time();

In the NativeScript Android runtime, this code is compiled (JIT compiled, technically) and executed by V8. How this works is pretty easy to understand for simple statements like var x = 1 + 2;, but in this case, the next question becomes… how does V8 know whatandroid.text.format.Time() is?

The next few sections focus on V8 and Android for simplicity, but the same basic architectural patterns apply to JavaScriptCore and iOS. Where there are notable differences they will be noted.

I do not discuss NativeScript’s Windows Universal runtime in detail because it’s subject to change, however, the current Windows implementation uses JavaScriptCore much like the iOS runtime does.

How NativeScript manages JavaScript VMs

V8 knows what android is because the NativeScript runtime injects it, because as it turns out, V8 has a whole slew of APIs that let you configure a bunch of things about its JavaScript environment. You can insert custom C++ code to profile JavaScript CPU usage, manage JavaScript garbage collection, change how the environment’s internals work, and a whole lot more:

654dcuD
V8 has a ton of APIs. Who knew?

Amidst these APIs are a few “Context” classes that let you manipulate the global scope, making it possible for NativeScript to inject a globalandroid object. This is actually the same mechanism Node.js uses to make its global APIs available – e.g. require() – and NativeScript uses it to inject APIs that let you access native code. JavaScriptCore has a similar mechanism that makes the same technique possible for iOS. Cool, right?

Let’s go back to our code:

var time = new android.text.format.Time();

You now know that this code runs on V8, and that V8 knows whatandroid.text.format.Time() is because NativeScript injected the necessary objects into the global scope. But there are still some big unanswered questions, like, how does NativeScript know which APIs to inject, and how does NativeScript know what to do when the Time() call is actually made? Let’s start with the first of these questions, and look at how NativeScript builds its list of APIs.

Metadata

NativeScript uses reflection to build the list of the APIs that are available on the platform they run on. If you’re a JavaScript developer you may not be familiar with reflection, as the permissive nature of the JavaScript language makes reflection largely unnecessary. In many other languages, most notably Java, reflection is the only technique you can use to examine the runtime itself. For example, in Java the only way to build a list of methods an arbitrary Object can invoke is with reflection.

For NativeScript’s purposes, reflection is what lets NativeScript build a comprehensive list of APIs for each platform, includingandroid.text.format.Time. Because generating this data is non-trivial from a performance perspective, NativeScript does it ahead of time, and embeds the pre-generated metadata during the Android/iOS build step.

With that in mind let’s again return to our line of code:

var time = new android.text.format.Time();

You now know that this code runs on V8, that NativeScript injects theandroid.text.format.Time JavaScript object, that NativeScript knows each API to inject from a separate metadata process, and that NativeScript embeds that metadata during its Android and iOS builds. On to the next question: how does NativeScript turn a JavaScript Time() call into a native android.text.format.Time() object?

Invoking native code

The answer to how NativeScript invokes native code again lies in the JavaScript VM APIs. When we last looked at V8’s APIs, we saw how NativeScript used them to inject global variables. This time we’ll look at a series of callbacks that let you execute C++ code at given points during JavaScript execution.

For example, the code new android.text.format.Time() invokes a JavaScript function, which V8 has a callback for. That is, V8 has a callback that lets NativeScript intercept the function call, take some action with custom C++ code, and provide a new result.

In the case of Android, the NativeScript runtime’s C++ code cannot directly access Java APIs such as android.text.format.Time. However,Android’s JNI, or Java Native Interface, provides the ability to bridge between C++ and Java, so NativeScript uses JNI to make the jump. On iOS this extra bridge is unnecessary as C++ code can directly invoke Objective-C APIs.

With all of this in mind, let’s return to our line of code:

var time = new android.text.format.Time();

We already know that this code runs on V8; that it knows whatandroid.text.format.Time is because NativeScript injects such an object; and that NativeScript has a metadata-generating process for obtaining these APIs. We now know that when Time() executes, the following things happen:

  • 1) The V8 function callback runs.
  • 2) The NativeScript runtime uses its metadata to know that Time()means it needs to instantiate an android.text.format.Timeobject.
  • 3) The NativeScript runtime uses the JNI to instantiate aandroid.text.format.Time object and keeps a reference to it.
  • 4) The NativeScript runtime returns a JavaScript object that proxies the Java Time object.
  • 5) Control returns to JavaScript where the proxy object gets stored as a local time variable.

The proxy object is how NativeScript maintains a mapping of JavaScript objects to native ones. For example, let’s look at the next line of code from our earlier example:

var time = new android.text.format.Time();
time.set( 1, 0, 2015 );

Because of the generated metadata, NativeScript knows all the methods to put on the proxy object. In this case the code invokes the Time object’sset() method. When this method runs, V8 again invokes its function callback; NativeScript detects that this is a method call; and then NativeScript uses the Android JNI to make the corresponding method call on the Java Time object.

And that’s really most of how NativeScript works. Cool, right?

Now, I did leave out some of the really complex parts, because converting Objective-C and Java objects into JavaScript objects can get tricky, especially when you consider the different inheritance models each language uses.

However, we’re not going to dig into those type conversion details here because they’re not a very common thing you need to know when building a NativeScript app. In fact, even though this article has focused on how native access in NativeScript works, another feature of NativeScript keeps you from having to dive into native code very often: TNS modules.

TNS Modules

I like to think of TNS modules, or Telerik NativeScript modules, as Node modules that depend on the NativeScript runtime. TNS modules follow the same CommonJS conventions as Node modules, so if you already know how require() and the exports object work, then you already know how TNS modules work.

TNS modules allow you to abstract platform-specific code into a platform-agnostic API, and NativeScript itself provides several dozens of these modules for you out of the box. As an example, suppose you need to create a file in your iOS/Android app. You could write the following code for Android:

new java.io.File( path );

As well as the following code on iOS:

NSFileManager.defaultManager();
fileManager.createFileAtPathContentsAttributes( path );

But you’re better off just using the TNS file-system module, as it lets you write your code once, without having to worry about the iOS/Android internals:

var fs = require( "file-system" );
var file = new fs.File( path );

What’s cool is that all TNS modules are written using the same NativeScript runtime conventions discussed in this article, which means that it’s really easy to browse any module’s source code, and that it’s really easy to create your own distributable TNS modules. For example, here’s a TNS module that retrieves a device’s OS version:

// device.ios.js
module.exports = {
    version: UIDevice.currentDevice().systemVersion
}

// device.android.js
module.exports = {
    version: android.os.Build.VERSION.RELEASE
}

This code only retrieves one property, but it gives you an idea of much you can accomplish in a small amount of code. Using custom TNS modules is also trivial, as you use the same require() call you use to retrieve npm modules. Here’s how you use the device module shown above:

var device = require( "./device" );
console.log( device.version );

NativeScript modules are surprisingly easy to write, distribute, and use, especially if you’re already familiar with npm’s conventions. Personally, as a web developer, native iOS and Android code scares me, but even I can reference the Java/Objective-C API documentation and throw together something functional if you give me a few hours. It’s exciting stuff, and it lowers the barrier for web and Node developers that want to build on native platforms.

 

 

The only question that I have in mind is that can it be a Xamarin killer? Well.. I don’t think so.