The outline of this paper:

  1. What is the JSI
  2. Inject methods and variables in V8. 1. V8 runs the JS code in step 2. Inject methods into JS. 3. Inject variables into JS
  3. React-native js and NATIVE JS communication 1. Js to native communication 2
  4. Briefly describe the implementation of JSI

This article strongly suggests opening the React-Native source code for comparison, because I have not posted all the code in many places, and due to the frequent update of the warehouse, this article was written on 2020-11-17, the React-Native version is V0.63.3.

What is the JSI

JSI is commonly translated into javascript interface, and its function is to establish an adaptation layer between JS engines (such as V8) and Native. With JSI, two improvements are mentioned in React-Native:

  • React-native’s default JS engine is JSC, which can be easily changed to V8, Hermes (Facebook’s own JS engine), or even Jere-Script, etc.
  • 2. Javascript can directly reference and call the methods injected by C++ into the js engine, which makes native and js layers “aware of each other”, instead of needing to JSON data and then pass it between js and native via bridge.

1The improvement in is well understood,2A deeper explanation of the content in “React-Native” is that it is in the previous architecture, as shown in the figure belowThe communication is carried out through the bridge of the middle layer. When the method of native layer needs to be called in JS, the message needs to be serialized by JSON and then sent to Native. Since the data is sent asynchronously, it may cause blocking and some optimization problems (like microTask and MacroTask in our JS asynchronism), and at the same time, because native and JS layers cannot sense each other (there is no reference to Native in JS), Before we call native methods (such as Bluetooth) from the JS side, we need to initialize the Bluetooth module, even though you may not use the module in your entire app. The new architecture allows native modules to be loaded on demand, i.e. loaded when needed, and the ability to have references to modules in JS means no need to communicate via JSON, which greatly improves startup efficiency. Now the new architecture of React-Native is as follows: Fabric on the lower left is the native rendering module, while Turbo Modules on the right is the native method module. It can be seen that JSI now connects the native and JS layers.The relationship between JSI and JS engines is as follows:

Inject methods and variables in V8

We all know that there are some methods such as the console, log, setInterval and setTimeout method is, in fact, the browser (chrome) or node injection methods for us, js engine itself is not the way, That is, many methods are injected outside of the JS engine. So it’s worth taking a look at how methods and variables are injected in V8:

  • Start by compiling V8 to generate a static/dynamic library, which you can include in your C++ fileshereThis is the official v8 tutorial, which will guide you from compiling v8 to running a snippet of js code that prints “Hello world”, sort of like in c++eval("'Hello ' + 'World'")
  • After the previous step, we can simply see how to run js code from the V8 library:

Run the JS code step

The first step is to convert the JS String to a V8 ::String of Local type using v8’s NewFromUtf8Literal method, where ISOLATE is a V8 instance and Local is used for garbage collection.

  v8::Local<v8::String> source =
     v8::String::NewFromUtf8Literal(isolate, "'Hello' + 'World'");
Copy the code

The second step is to compile the js code, where the Context is the js execution Context, source is the code in 1

  v8::Local<v8::Script> script =
          v8::Script::Compile(context, source).ToLocalChecked(a);Copy the code

— Step 3. Step 3 Run the JS code.

  v8::Local<v8::Value> result = script->Run(context).ToLocalChecked(a);Copy the code

There are three steps: 1. String type conversion 2. Compile 3

Inject methods into JS

  • Emmm.. However, if we are injecting methods and variables into JS, of course we need to have aStep 2 aboveInsert a print method into the javascript context. The C++ implementation of print is as follows. We don’t care about the implementation.
   // this code is not important, but the C++ implementation of the print method
  void Print(const v8::FunctionCallbackInfo<v8::Value>& args) {
    bool first = true;
    for (int i = 0; i < args.Length(a); i++) {v8::HandleScope handle_scope(args.GetIsolate());
       if (first) {
         first = false;
       } else {
         printf("");
       }
       v8::String::Utf8Value str(args.GetIsolate(), args[i]);
       const char* cstr = ToCString(str);
       printf("%s", cstr);
    }
  printf("\n");
  fflush(stdout);
}
Copy the code
  • The Print method has been created. Now we need to add the method to the js execution context (global).
// Create an object with type ObjectTemplate and name global based on v8 instance ISOLATE
v8::Local<v8::ObjectTemplate> global=v8::ObjectTemplate::New(isolate);

// Add a method named print to the global created above. Global.print = print
global->Set(v8::String::NewFromUtf8(isolate, "print", v8::NewStringType::kNormal).ToLocalChecked(),v8::FunctionTemplate::New(isolate, Print));

// Create the corresponding context according to the global, i.e. js execution context, and use this context to perform steps 1, 2, 3 above.
v8::Local<v8::Context> context = v8::Context::New(isolate, NULL,global);
Copy the code

If the command is executed again

     v8::Local<v8::String> source =
     v8::String::NewFromUtf8Literal(isolate, "print('Hello World')");
     // Compoile.....
     // Run....
Copy the code

And you can see in Terminal Hello World.

Inject variables into JS

Like the injection method, you need to inject variables into the context (js execution context), but you need to convert the C++ “Object” into the js “Object”. Type conversion, front-end developers forever pain.

    // Create the Context as you did when you injected the method
   v8::Local<v8::ObjectTemplate> global=v8::ObjectTemplate::New(isolate);
   v8::Local<v8::Context> context = v8::Context::New(isolate, NULL,global);
   // Create the corresponding ObjectTemplate named temp1
   Local<v8::ObjectTemplate> templ1 = v8::ObjectTemplate::New(isolate, fun);
   // add an x attribute to temp1
   templ1->Set(isolate, "x", v8::Number::New(isolate, 12));
   // add y to temp1
   templ1->Set(isolate, "y",v8::Number::New(isolate, 10));
   // Create instance instance1 of ObjectTemplate
   Local<v8::Object> instance1 =
     templ1->NewInstance(context).ToLocalChecked(a);// Add the contents of instance1 to global.options
   context->Global() - >Set(context, String::NewFromUtf8Literal(isolate, "options"),instance1).FromJust(a);Copy the code

If the command is executed again

v8::Local<v8::String> source = v8::String::NewFromUtf8Literal(isolate, "options.x");
// Compoile.....
// Run....
Copy the code

And you can see the output 12 in Terminal.

React-native js and Native communication

Now that We know what JSI is and know the basics of injecting methods and variables into the JS engine, We need to dig deeper.

Js to native communication

  • The react-Native startup process is shown hereA great god,Since we are only concerned with the JSI part, we come directly to itJSIExecutor::initializeRuntimeMethods. (RN will come here to initialize the Runtime after it starts.) We’ll omit the other implementations except for the first onenativeModuleProxyThe implementation of the.
  void JSIExecutor::initializeRuntime(a) {
  runtime_->global().setProperty(
      *runtime_,
      "nativeModuleProxy",
      Object::createFromHostObject(
          *runtime_, std::make_shared<NativeModuleProxy>(nativeModules_)));

  runtime_->global().setProperty(
      *runtime_,
      "nativeFlushQueueImmediate",
      Function::createFromHostFunction(
         // The code is omitted
         }));

  runtime_->global().setProperty(
      *runtime_,
      "nativeCallSyncHook",
      Function::createFromHostFunction(
          *runtime_,
          PropNameID::forAscii(*runtime_, "nativeCallSyncHook"),
          1.// The code is omitted
           ));

  runtime_->global().setProperty(
      *runtime_,
      "globalEvalWithSourceUrl".// The code is omitted
      );
}
Copy the code

The code is easy to read, which is to set several modules on the runtime using global().setProperty. For example, the first module, Add a module called nativeModuleProxy to the runtime js context using the global setProperty method. NativeModuleProxy module is an Object of type nativeModuleProxy. There are get and set methods in it, just like our front-end proxy. And all calls from JS to Native require it as an intermediate proxy.

    class JSIExecutor::NativeModuleProxy : public jsi::HostObject {
  public:
  NativeModuleProxy(std::shared_ptr<JSINativeModules> nativeModules)
      : weakNativeModules_(nativeModules) {}

  Value get(Runtime &rt, const PropNameID &name) override {
    if (name.utf8(rt) == "name") {
      return jsi::String::createFromAscii(rt, "NativeModules");
    }

    auto nativeModules = weakNativeModules_.lock(a);if(! nativeModules) {return nullptr;
    }
    / / call getModule
    return nativeModules->getModule(rt, name);
  }

  void set(Runtime &, const PropNameID &, const Value &) override {
    throw std::runtime_error(
        "Unable to put on NativeModules: Operation unsupported");
  }

 private:
  std::weak_ptr<JSINativeModules> weakNativeModules_;
};
Copy the code

In the get method, there is getModule. If you jump to getModule, you can see createModule:

 Value JSINativeModules::createModule(Runtime &rt, const PropNameID &name) {
 	// This method leaves out a lot. Leave only one key statement, get __fbGenNativeModule from Runtime. global
 	rt.global().getPropertyAsFunction(rt, "__fbGenNativeModule");
 }
Copy the code

In the createModule, return the globally-defined __fbGenNativeModule. We can search globally to find the globally-defined __fbGenNativeModule in nativeModules.

global.__fbGenNativeModule = genModule;
Copy the code

Next, look at genMethod in genModule

    function genMethod(moduleID: number, methodID: number, type: MethodType) {
    // This method is omitted to return only
      return new Promise((resolve, reject) => {
        BatchedBridge.enqueueNativeCall(
          moduleID,
          methodID,
          args,
          data => resolve(data),
          errorData =>
            reject(
              updateErrorWithErrorData(
                (errorData: $FlowFixMe),
                enqueueingFrameError,
              ),
            ),
        );
      });
}
Copy the code

EnqueueNativeCall () : enqueueNativeCall ();

enqueueNativeCall(xxx) { const now = Date.now(); // MIN_TIME_BETWEEN_FLUSHES_MS = 5 if ( global.nativeFlushQueueImmediate && now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS ) { const queue = this._queue; this._queue = [[], [], [], this._callID]; this._lastFlush = now; global.nativeFlushQueueImmediate(queue); }}Copy the code

Here probably made a throttle, if the last time the difference between native and the execution is more than 5 ms, execute nativeFlushQueueImmediate directly. Then see nativeFlushQueueImmediate

    nativeFlushQueueImmediate() {[this](jsi::Runtime &,
          const jsi::Value &,
          const jsi::Value *args,
          size_t count) {
            if(count ! =1) {
              throw std::invalid_argument(
                  "nativeFlushQueueImmediate arg count must be 1");
            }
            callNativeModules(args[0].false);
            return Value::undefined();
          }
    }
Copy the code

Directly executed are the callnativeModules method, which, as its name suggests, calls native methods.

To sum up, the call chain from JS to native is: InitializeRuntime -> js side setProperty(nativeModuleProxy) -> Trigger the get method in nativeModuleProxy when calling nativeModuleProxy GetModule – > createModule – > genModule – > genMethod – > enqueueNativeCall (control native execution frequency) – > nativeFlushQueueImmediate – > callNativeModules.

Native to JS communication

Before we came to NativeToJsBridge directly: : callFunction method, the startup sequence can be the reference here, the name will know that this is a native to js bridge, All calls from the Native to the JS sent from the NativeToJsBridge interface, see these calls JSCExecutor: : callFunction

        // Executor is a pointer of type JSExecutor, which points to JSIExecutor
       executor->callFunction(module, method, arguments);
Copy the code

To see JSIExecutor: : callFunction:

void JSIExecutor::callFunction(a){
    if(! callFunctionReturnFlushedQueue_) {bindBridge(a); }scopedTimeoutInvoker_(
      [&] {
          ret = callFunctionReturnFlushedQueue_->call(
              *runtime_,
              moduleId,
              methodId,
              valueFromDynamic(*runtime_, arguments));
        },
        std::move(errorProducer));


     callNativeModules(ret, true);
  }
Copy the code

See if there is no callFunctionReturnFlushedQueue_ will go bindBridge, if any went to perform callFunctionReturnFlushedQueue_, Then we could look at the callFunctionReturnFlushedQueue_ bindBridge exactly

    void JSIExecutor::bindBridge(a) {
    // omit most of the code
    Value batchedBridgeValue =
        runtime_->global().getProperty(*runtime_, "__fbBatchedBridge");
    }
Copy the code

__fbBatchedBridge = __fbBatchedBridge

const BatchedBridge: MessageQueue = new MessageQueue(a); Object.defineProperty(global, '__fbBatchedBridge', {
  configurable: true,
  value: BatchedBridge,
});
Copy the code

So __fbBatchedBridge is a MessageQueue, open the MessageQueue. Js file to check the MessageQueue callFunctionReturnFlushedQueue method is as follows

  callFunctionReturnFlushedQueue(
    module: string,
    method: string,
    args: mixed[],
  ): null | [Array<number>, Array<number>, Array<mixed>, number] {
    this.__guard(() = > {
      this.__callFunction(module, method, args);
    });

    return this.flushedQueue();
  }
Copy the code

Then look at the final execution of this.__callfunction, and then look inside the method:

      __callFunction(module: string, method: string, args: mixed[]): void {
            // omit most of the code
            moduleMethods[method].apply(moduleMethods, args);
    }
Copy the code

Important to find where to execute js methods… To sum up, the call chain from native to JS is: NativeToJsBridge::callFunction->JSIExecutor::callFunction -> MessageQueue::callFunctionReturnFlushedQueue -> MessageQueue::__callFunction

Briefly describe the implementation of JSI

Above we summarized the mutual call chain from JS to native side. When checking the source code of call chain, we noticed that many method parameters have an address named “Runtime”, so this runtime actually refers to different JS engines. For example, the Native side needs to call the test method registered in the JS side. Jsi interface only defines the test method, and calls the specific implementation of the test method in different runtime according to the different JS engine. We take one of the most easy to understand the setProperty method, for example: start by opening the react – native/ReactCommon/jsi/jsi/jsi – inl. H file to look at jsi setProperty interfaces defined in the method.

void Object::setProperty(Runtime& runtime, const String& name, T&& value) {
  setPropertyValue(
      runtime, name, detail::toValue(runtime, std::forward<T>(value)));
}
Copy the code

Then look at setPropertyValue, which is implemented as:

   void setPropertyValue(Runtime& runtime, const String& name, const Value& value) {
    return runtime.setPropertyValue(*this, name, value);
  }
Copy the code

As you can see from the above code, the setPropertyValue method of runtime (js engine) is finally called. Then we open the react – native/ReactCommon/jsi/JSCRuntime CPP file, the file is to react – native default JSC engine jsi in the concrete implementation of methods:

    // We will not look at the implementation. Just be aware that you need to implement the setPropertyValue method in JSCRuntime
    void JSCRuntime::setPropertyValue(
    jsi::Object &object,
    const jsi::PropNameID &name,
    const jsi::Value &value) {
      JSValueRef exc = nullptr;
      JSObjectSetProperty(
      ctx_,
      objectRef(object),
      stringRef(name),
      valueRef(value),
      kJSPropertyAttributeNone,
      &exc);
  checkException(exc);
}
Copy the code

Then open the React-Native V8 repository, which is implemented by v8’s React-Native Runtime. We open the file the react – native/react – native – v8 / SRC/v8runtime v8runtime. CPP look down in the concrete implementation of the v8:

    void V8Runtime::setPropertyValue(
    jsi::Object &object,
    const jsi::PropNameID &name,
    const jsi::Value &value) {
    // We will not look at the implementation. Just be aware that in V8runtime you need to implement the setPropertyValue method
      v8::HandleScope scopedIsolate(isolate_);
      v8::Local<v8::Object> v8Object =
          JSIV8ValueConverter::ToV8Object(*this, object);

      if (v8Object
          ->Set(
              isolate_->GetCurrentContext(),
              JSIV8ValueConverter::ToV8String(*this, name),
              JSIV8ValueConverter::ToV8Value(*this, value))
          .IsNothing()) {
      throw jsi::JSError(*this."V8Runtime::setPropertyValue failed."); }}Copy the code

Finally we will open the hermes repo, view the file/hermes hermes/API/hermes hermes. CPP look down in the concrete implementation of the hermes:

     void HermesRuntimeImpl::setPropertyValue(
         // We will not look at the implementation. Just be aware that in Hermes you need to implement the setPropertyValue method
        jsi::Object &obj,
        const jsi::String &name,
        const jsi::Value &value) {
      return maybeRethrow([&] {
        vm::GCScope gcScope(&runtime_);
        auto h = handle(obj);
        checkStatus(h->putComputed_RJS(
                         h,
                         &runtime_,
                         stringHandle(name),
                         vmHandleFromValue(value),
                         vm::PropOpFlags().plusThrowOnError()).getStatus());
      });
    }
Copy the code

Therefore, the setPropertyValue method needs to be implemented on the three engines respectively, and the setProperty method should be declared in the JSI interface.