How to create your own native bridge
Chapter 2: JSVM and the first adventure
Previous article: “How to create your own native bridge. Chapter 1: Designing an architecture”.
What do you know about JavaScript engines? Did you ever try to embed one? In this chapter I’m going to guide you through the dark spooky forest of hosted objects, virtual machines, interpreters and other evil spirits that we call JavaScript engines.
I know that it may look scary, but don’t forget that a journey of a thousand miles starts with a single step. In our case it’ll be a step into code parsing. At this stage our JavaScript source is getting converted to a structure called abstract syntax tree (AST). There are many different techniques to parse your code (LL(k), LR(k) etc) and convert it to AST, but for the sake of simplicity I want to keep it out of this article.
Although, for those who are interested, I got all my knowledge about parsers (and compiler theory in general) from the Dragon Book. I just can’t recommend it enough.
However, I will tell you more about the abstract syntax tree concept. An AST is a structural representation of your code in a tree format where every node represents a language construct (e.g. expressions, statements, variables, literals etc). You can play with it using ESPrima praser demo page or ASTExplorer.
First of all, a JavaScript engine has to parse (tokenize) a source code to produce an array of tokens. These tokens are supplied to a syntactic analysis tool that builds an AST based on a given language grammar. Once an AST is built, JavaScript engine will compile it either to machine code directly (V8 behaves this way) or to intermediate representation, which is an another level of abstraction over machine code.
In this experiment I committed to use ChakraCore which uses a bytecode as it’s intermediate language. But it can’t be executed straight away: our target machine doesn’t know how to process it.
In order to bridge the gap, ChakraCore includes a bytecode interpreter. On ChakraCore’s bytecode each instruction starts with a 1-byte bytecode that represents which operation should be executed (a.k.a. opcode), and therefore the interpreter may have up to 256 instructions. Some bytecodes may take multiple bytes, and may be arbitrarily complicated
That was a very short overview of the JS execution flow. Probably you noticed that in this article we don’t talk about inner code optimizations (like JIT or AoT). Although it’s a very interesting topic, I decided to omit it in order to make this article easier to grasp.
Embedding ChakraCore
Now, when we have some knowledge about ChakraCore, we can start embedding it into our application. So first of all we need to install ChakraCore dependencies:
$ xcode-select --install
$ brew install cmake icu4c
And ChakraCore itself. I will show how to include it as a submodule:
$ mkdir modules && $_
$ git submodule add https://github.com/Microsoft/ChakraCore
Then build it from the source:
$ cd ChakraCore
$ ./build.sh --static --icu=/usr/local/Cellar/icu4c/<version>/include --test-build -j=2
Once these steps are done, we can include it into our application:
- Open the project we created in the previous chapter
- Select ExampleBridge project in the Project navigator and switch to the target:
Link your compiled ChakraCore files:
- libChakra.Pal.a
- libChakra.Common.Core.a
- libChakra.Jsrt.a
And your icu4u files (from /usr/local/Cellar/icu4c/<version>/include):
- libicudata.a
- libicui18n.a
- libicuuc.a
Your result should look like this:
Note: order of these dependencies is very important!
Getting started with ChakraCore
We come to the very interesting part of our journey. To the place where we need all our knowledge about JavaScript engines and the way they work. Yes, dear reader, you’re right! We’re about to start using ChakraCore!
Bootstrapping ChakraCore
First of all, open your ChakraProxy.m file and find the NSLog statement that we added in the previous chapter. Let’s replace it by something that makes more sense:
I don’t expect you to be familiar with Objective C, so let me guide you through this code:
- Line 6–8: Read a content of main.js file from the bundle. More about bundles here.
- Line 14: ChakraCore is an engine, written in C++, so it doesn’t understand
NSString
format. However, we used Objective C approach to read a file content and now we have to deal withNSString
toconst char*
conversion. So, that’s how we do it. - Line 24: That’s my custom C++ function. I use it in order to reduce a cognitive payload of
run
method and move a function description to the different section of this article.
Hope it doesn’t look very complex to you. Anyway, there are still some unclear places in this code:
Runtime
Runtime (line 17) represents a complete JavaScript execution environment. Each runtime that is created has its own isolated garbage-collected heap and, by default, its own just-in-time (JIT) compiler thread and garbage collector (GC) thread. (see ChakraCore JSRT overview)
Execution Context
Context (line 20–21) is an execution environment that allows separate, unrelated, JavaScript applications to run in a single instance of runtime. You must explicitly specify the context in which you want any JavaScript code to be run. (see V8 Embedder’s Guide)
Extending global scope
Now, when ChakraCore is set up, it’s time to build a bridge. In the previous chapter I briefly mentioned that we’re going to use hosted objects to expose C++ functions to JavaScript. So let’s write a function that will do it for us:
If you rewrite the code above to JavaScript, it’ll look like this:
As you can see from the snippet above, we use a special ChakraCore function JsGetGlobalObject
to get context’s “global” object. Once it is there, we extend it by a custom hosted object called “bridge” to expose our C++ “render” function to JS. This approach is similar to the one we used in our web applications back in a day. I’m talking about namespaces, when you move all your application modules under window.app
or a similar object in order to scope them by an organic global variable. In this code we do the same, but instead of modules we expose a custom C++ function. You are probably wondering why in the code above I use ChakraUtils
. It’s a self-written wrapper over a standard ChakraCore API. I won’t go though the code, but you can find my implementation on the github.
However, the “render” function is still has to be defined. It should fit a JS function interface and perform an async dispatch to the main thread. The simplest implementation will look like this:
Try to not to be overwhelmed by the amount of function parameters, in this article we’re about to use only one — arguments
. The code above will read the first two parameters passed to the function from JavaScript and invoke an Objective-C AppDelegate method called renderElementOfType
. No callbacks, no return values. Let’s keep it simple for now.
One thing, that may make you feel confused is a dispatch_async
call. We use this function in order to schedule a block (statement inside ^{}
) to be executed in the main dispatch queue (see GCD documentation for details).
Now, once “render” function is invoked, it sends a block to the main thread. Inside the block we have a renderElementOfType
call, which is responsible for a final element creation:
You may notice, that we hardcode window
and don’t even use a name
property. We’ll get back to this part in the third chapter. Other than that, this code should be pretty straight-forward: we create a CGWindow
instance and call openWithSize
method with a given params.
But when you call this function, you don’t see a window. Why? Because window
is a local variable and it will be deallocated once you leave the function scope. So in order to see a window, we have to store a reference to this window
somewhere outside of the function. Let’s create a UIManager class that will manage our UI references:
You’re probably wondering what does a sharedInstance
mean? It’s one of the ways to create a singleton in Objective C. It’s not necessary to make UIManager a singleton, but to me it feels like a right way to do it.
This class has the only one public API method: addValue
. In the next chapter I’m going to add some more (like deleteValue
), but let’s keep it as simple as possible for now.
Let’s update our renderElementOfType
function to start using our storage:
So once we get a UIManager instance, we generate a uuid for our window and put it in the storage by addValue
. Inside the manager we generate a strong reference to the given object which prevents it from being deallocated (see ARC).
And you know what? That’s it! If you create a main.js file, add it to the bundle and type something like bridge.render('Window', 400, 400);
, you’ll see a 400x400 window at the application startup!
Buy hey, it has nothing about React yet! What do we need to provide for a React-like interface to our platform? How to return references from Objective-C to JS? All these questions will be answered in the Chapter 3!
In the meanwhile, you can play around with the code from this article.