React Native JSI Challenge

Christian Falch
Apr 14 · 6 min read

Back in 2018 Parashuram presented React Native’s new architecture Fabric in his talk during the React Amsterdam Conference. He also wrote about it and presented it in more detail in this year’s conference. Lorenzo Sciandra has also written up a nice four-part series about the topic and there has been numerous tweets and discussions about what Fabric means for React Native and the performance gains we should see.

One of the core features of the new architecture is that it avoids serializing data from JavaScript to Native over the bridge (read one of the above links if you don’t know how React Native’s rendering pipeline works).

When I first heard of the new architecture I immediately thought that this would be a game changer and I started looking around in the source code to see how it worked. When React 0.59 was released I could see that a lot of the code in the master branch for these new concepts were already in the released code. I decided to find out how we could start using it!

NOTE: The really interesting part is JSI which is the glue between JS and native. When you read about the new architecture you’ll also stumble upon the concepts of TurboModules, CodeGen and Fabric. TurboModules are responsible for automatic discovery and exposure of modules and Fabric is the new synchronous render engine — both of them using JSI. CodeGen is a tool for automatically generate typed glue code between JS and Native.

After asking some of the experts in the contributor channels and sending out a few tweets I got in touch with Eric Lewis and asked for some help on how to expose native code to Javascript without going through the bridge. Eric was excited and super supportive and stepped up to help.

@ericlewis: Well, you don’t need any hacks to create your own c++ modules that you can plug in to JSI. That is actually hack free!

Awesome! I fired up XCode immediately and started hacking and after I while my long-gone C++ skills gave me a few problems so I sat down with Eric to see if we would make this happen together.

Actually, exposing a native module directly to JS actually was not that hard. You only need a few C++ classes in your project!

We decided on the simplest of all things — a class that exposed a method returning a number. This class is a no-brainer and is written in C++ and included in your native projects.

int Test::runTest() const {
return 1337;
}

I’ll try to make the code samples short so they are all included inline — a link to the resulting repository is included at the end of the article if you need color highlighting, header files and intellisense :)

Next up we need to write the glue that performs function lookup and converts values between JavaScript and native— this is where JSI comes in.

A binding needs a method for installing it and a method to get the functions that should be callable from within JavaScript.

void TestBinding::install(jsi::Runtime &runtime,
std::shared_ptr<TestBinding> testBinding) {
auto testModuleName = “nativeTest”; auto object = jsi::Object::createFromHostObject(
runtime, testBinding);
runtime.global().setProperty(runtime, testModuleName,
std::move(object));
}

This is where I ran into trouble. To be able to use the JSI declarations we need to inlude the file `jsi.h` which is not available out of the box in a new React Native project. Eric knew how to solve this, and after editing the project configuration with a few extra header paths and defines, our code finally compiled (see the repository for the required changes).

The next thing we had to do was to create a function in our binding that exposes our test function to JavaScript. It’s all boilerplate code and looks like this:

jsi::Value TestBinding::get(jsi::Runtime &runtime, 
const jsi::PropNameID &name) {

auto methodName = name.utf8(runtime);
auto &test = *test_;

if (methodName == “runTest”) {
return jsi::Function::createFromHostFunction(runtime, name, 0,
[&test](jsi::Runtime &runtime, const jsi::Value &thisValue,
const jsi::Value *arguments, size_t count) -> jsi::Value {
return test.runTest();
});
}

return jsi::Value::undefined();
}

The `get` method is called when we access the function from within our JavaScript. The code is basically constructing a wrapper for each function we call that converts JavaScript arguments and return values that can be passed back and forth between JavaScript and native code.

Our next task was to call the `install` function with the correct parameters so that React Native got to know it. (This is where TurboModules will help — our solution is a hacky solution).

Looking at the signature of the `install` function we could see that we needed a pointer to a `jsi::Runtime` object. We started to tear our hair out to find a way to find this object from within our Objective-C code. Eric knew some tricks for this and we installed a notification in our code so that we got a callback when a valid runtime would be available:

[[NSNotificationCenter defaultCenter] addObserver:self 
selector:@selector(handleJavaScriptDidLoadNotification:)
name:RCTJavaScriptDidLoadNotification
object:bridge];

In the notification callback we finally got access to the internals of the bridge by importing the `<React/RCTBridge+Private.h>` file which exposes a getter for the runtime object. We were finally ready to hack together a solution to call the install function for our bridge:

- (void)handleJavaScriptDidLoadNotification:(
__unused NSNotification*)notification {
// Get the RCTCxxBridge from bridge
RCTCxxBridge* bridge = notification.userInfo[@”bridge”];

// Get the runtime
facebook::jsi::Runtime* runtime =
(facebook::jsi::Runtime*)bridge.runtime;

// Create the Test object
auto test = std::make_unique<facebook::react::Test>();
// Create the Test binding
std::shared_ptr<facebook::react::TestBinding> testBinding_ =
std::make_shared<facebook::react::TestBinding>(std::move(test));
// Install it!!!
facebook::react::TestBinding::install(
(*runtime), testBinding_);
}

Finally our code was compiling and running and we could move over into our JavaScript code.

Calling our native module is really the easiest part of it all:

console.warn(global.nativeTest.runTest());

We were quite excited when we ran this in the simulator and saw our magic number displayed in the React Native yellow box on the screen:

For references to all we did to get this working, please refer to the included repo. It has commits for all we’ve done and should be easy to follow.

Reloading and debugging does not currently work. Reloading fails because we are installing our module every time we get a notification that our javascript has loaded (can easily be fixed by setting a flag). Debugging is not something we’ve yet investigated why is not working. We’re open for PRs!

Playing around with the new parts of the upcoming React Native architecture has been super fun. Although our code contains a few hacks to get our module installed we’ve proved that the required functionality for writing native modules that are callable directly from JavaScript using JSI is already released and working in React Native 0.59.

I’m really looking forward to see all the possibilities this will give library developers — we should be able to start preparing to write code that is synchronous and so much more performant than today’s code where we pass data through the bridge.

Thanks again to Eric who spent his Saturday hacking together this solution in a fun remote pair programming session.

Thanks to my colleagues in Fram X for supporting my non-billable hours :)

Thanks to Tom Duncalf who submitted a PR to the project showing how to do this in Android!!!

https://github.com/ericlewis/react-native-hostobject-demo

Christian Falch

Written by

Cross Platform Developer — Co-founder of Fram X