Interacting with Go from React Native through JSI

Jun 27, 2019

Introduction

There are 3 parts that let JS talk to Go:

  1. The C++ binding
  2. Installing the binding
  3. Calling Go

Not all the code is shown, check out the source code for specifics.

Part 1 - The C++ Binding

The binding is the C++ glue code that will hook up your Go code to the JS runtime. The binding itself is composed of two main parts.

Part 1.1 - The C++ Binding

The binding is a c++ class that implements the jsi::HostObject interface. At the very least it's useful for it to have a get method defined. The type of the get method is:

jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name) override;

It returns a jsi::Value (a value that is safe for JS). It's given the JS runtime and the prop string used by JS when it gets the field. e.g. global.nativeTest.foo will call this method with PropNameID === "foo".

Part 1.2 - The C++ Binding's install

Now that we've defined our HostObject, we need to install it into the runtime. We use a static member function that we'll call later to set this up. It looks like this:

void TestBinding::install(jsi::Runtime &runtime,
                          std::shared_ptr<TestBinding> testBinding) {
  // What is the name that js will use when it reaches for this?
  // i.e. `global.nativeTest` in JS
  auto testModuleName = "nativeTest";
  // Create a JS object version of our binding
  auto object = jsi::Object::createFromHostObject(runtime, testBinding);
  // set the "nativeTest" propert
  runtime.global().setProperty(runtime, testModuleName, std::move(object));
}

Part 2. Installing the binding (on Android)

Since we have a reference to the runtime in Java land, we'll have to create a JNI method to pass the runtime ptr to the native C++ land. We can add this JNI method to our TestBinding file with a guard.

#if ANDROID
extern "C" {
JNIEXPORT void JNICALL Java_com_testmodule_MainActivity_install(
    JNIEnv *env, jobject thiz, jlong runtimePtr) {
  auto testBinding = std::make_shared<example::TestBinding>();
  jsi::Runtime *runtime = (jsi::Runtime *)runtimePtr;

  example::TestBinding::install(*runtime, testBinding);
}
}
#endif

Then on the Java side (after we compile this into a shared library), we register this native function and call it when we're ready.

// In MainActivity

public class MainActivity extends ReactActivity implements ReactInstanceManager.ReactInstanceEventListener {
    static {
        // Load our jni
        System.loadLibrary("test_module_jni");
    }

    //... ellided ...

    @Override
    public void onReactContextInitialized(ReactContext context) {
        // Call our native function with the runtime pointer
        install(context.getJavaScriptContextHolder().get());
    }

    //  declare our native function
    public native void install(long jsContextNativePointer);
}

Part 3. Calling Go

Now that our binding is installed in the runtime, we can make it do something useful.

jsi::Value TestBinding::get(jsi::Runtime &runtime,
                            const jsi::PropNameID &name) {
  auto methodName = name.utf8(runtime);

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

Here we return a jsi::Function when JS calls global.nativeTest.runTest. When JS calls it (as in global.nativeTest.runTest()) we execute the code inside the closure, which just returns TestNum(). TestNum is a Go function that's exported through cgo so that it is available to c/c++. Our Go code looks like this:

package main

import "C"

// TestNum returns a test number to be used in JSI
//export TestNum
func TestNum() int {
	return int(9001)
}
func main() {
}

cgo builds a header and creates a shared library that is used by our binding.

Building

  • Look at the CMakeLists.txt for specifics on building the C++ code.
  • Look at from-go/build.sh for specifics on building the go code.

A Go Shared Library for C + Java

It's possible to build the Go code as a shared library for both C and Java, but you'll have to define your own JNI methods. It would be nice if gomobile bind also generated C headers for android, but it doesn't seem possible right now. Instead you'll have to run go build -buildmode=c-shared directly and define your jni methods yourself. Take a look at from-go/build.sh and testnum.go for specifics.

Further Reading

JSI Challenge #1

JSI Challenge #2

RN Glossary of Terms

GO JNI

GO Cross Compilation