react-native-community/discussions-and-proposals

JSI (JavaScript Interface) & JSC (JavaScript Core) Discussion

kelset opened this issue Β· 124 comments

What's the current status of JSI? Read here

Intro

With this issue I'd like to try and create a "one stop" for all the information available around the JavaScript Interface, the unified lightweight general purpose API for (theoretically) any JavaScript virtual machine.

Currently, the default Javascript Virtual Machine used by React Native is JavaScriptCore (JSC) - it is used in WebKit.

Terminology

  • Fabric: just the UI layer re-architecture, to take full advantage of concurrent React architecture. (dedicated issue)
  • TurboModules: re-architecture of NativeModules, also using JSI. (dedicated issue)
  • CodeGen: a tool to "automate" the compatibility between JS and native side. (dedicated issue)

TL;DR

From @axe-fb's blogpost, here's a temporary description of the JSI (please consider that this is not yet finalized, it may change in the future)

Instead of using the bridge for queuing messages, the new architecture allows us to directly "invoke" (think RPC) Java/ObjC methods.
An analogy would be how we call DOM methods from JavaScript in the browser. For example, in the statement var el = document.createElement('div'); the variable el holds a reference not to a JavaScript object, but to an object that was possibly instantiated in C++. When JavaScript calls el.setAttribute('width', 100), we end up synchronously invoking the setWidth method in C++ that changes the actual width of that element.
In React Native, we can similarly use the JavaScript interface to invoke methods on UI Views and Native Modules that are implemented in Java/ObjC.

Available Materials

At ReactConf 2018 @axe-fb did a talk about React Native's New Architecture, which also explains the 3 concepts above: JSI, Fabric, TurboModule.

Recently the JSC was upgraded for Android, you can check the commit here and the related issue in the main repo.

On twitter, @karanjthakkar did a thread detailing how he integrated the new JSC in the Skyscanner app.

In Q1 2019, @kelset wrote a high level explanation in a blogpost: https://formidable.com/blog/2019/jsi-jsc-part-2/ and did a talk about the whole rearchitecture in April 2019 at React Edinburgh.

Over on twitter, @ericlewis published a sample repo with @chrfalch to showcase how to use directly C++ in a RN app (thanks to the JSI): https://github.com/ericlewis/react-native-hostobject-demo

@kelset also did a more in-depth talk at React Advanced London in Oct 2019: youtube recording & slides.

@Jarred-Sumner published a quick benchmark difference between a lib and a reimplementation using JSI twitter.

Libraries implemented using JSI

Blogposts

Q&A

This is also a place for questions related to this effort and its direction.

I have a couple of questions:

Kudo commented

@Eyesonly88

Why doesn't React Native use the same exact JSC for iOS and Android?
AFAIK, Android JSC !== iOS JSC (correct me if I'm wrong)

Even at the same version of JSC, the Android implementation is different to iOS.
So that Android JSC will always not equal to iOS.
Furthermore, different iOS versions have different JSCs.

Is it even possible to use something else other than JSC for iOS? AFAIK, even Chrome doesn't use V8 on iOS: https://groups.google.com/a/chromium.org/forum/#!topic/chromium-dev/zINHc4_dWhk

Yes, JSI brings a benefit for other platforms to use different JavaScript engine easier.
E.g. react-native-windows will use CharkaCore

BTW Chrome does not use v8 on iOS simply because they should follows App Store Review Guidelines

2.5.6 Apps that browse the web must use the appropriate WebKit framework and WebKit Javascript.

@Kudo interesting, thanks for the reply.

Yeah, regarding V8 on iOS, that's what I was wondering about, even if RN provided a way to use other engines, we're stuck with JSC on iOS due to App Store rules.

@Eyesonly88 Please check @Kudo comment 2.5.6 Apps that browse the web must use the appropriate WebKit framework and WebKit Javascript. That focus "browse the web". Chrome is a web browser therefore google has to obey it.

@Pikaurd @Eyesonly88 Yeah I also think @Kudo is right regarding usage of V8 and theoretically it should already be possible to submit V8-powered RN-based apps to app store review process and getting accepted considering JSI efforts on RN side and more importantly the almost new announced JIT-less V8 on V8 side.

But really eager to see a practical example that's accepted to app store.

To me biggest benefit of V8 (In addition to other common reasons) could be V8 snapshots that should help boosting startup times considering parsing javascript is a significant chunk of overall code execution time.

@sJJdGG I'm happy to try it in one of my production apps, any guide on how would I go about doing that? (I'd do anything to improve startup time)

@sJJdGG @Eyesonly88 ReactNative supports another bundle option which is ByteCode Bundle. It is not documented and I guess facebook uses it in their app. It must be be similar to V8 snapshots.

Does this have any implications for WebAssembly interop with React Native?

I still can't figure out how JSI will solve the problem with RN viewport size suspense in case of embedding RN within a native project? As i understand the flow now is like:

  1. init RN native
  2. init RN JS
  3. build JS shadow tree
  4. draw native

During all this time the viewport doesn't know the size of content that leads to UI problems.

So what will be changed to solve exactly this problem?

the viewport doesn't know the size of content that leads to UI problems

my understanding is that actually through JSI & Fabric, the viewport will know. But I may be wrong.

Hi guys, we have been working on a JSI implementation for node.
Idea is to be able to re-use JS<->C++ modules that work in both React Native and Node/Electron. We think JSI has a great potential to become an API for cross-runtime implementations.

We also worked on a C++ template system that allows us to define C++ classes that map easier to a JS object, without any codegen tools.

We would like to share our effort with the community, but would like some guidance on how we could do that, perhaps with some mentoring from people that is involved with JSI implementation.

Thanks

So I have some code using turbomodules but I understandably can't run it in remote debug mode since that runs with the remote chrome js engine. What is the workaround and what does the debug DX look like for JSI HostObjects and turbomodules?

@akshetpandey I as remember, the current workaround is to have both JSI and the bridge and switching between them when you debugging.

When you're using JSI, your module available using global object, but once you've switched to a remote debugger, use NativeModules instead (if you have bindings).

You can check out @chrfalch's reanimated JSI example (might be outdated a bit): software-mansion/react-native-reanimated@master...chrfalch:jsi-methods

When using JSI, we currently recommend using safari to debug the JSC instance running on device, or on Android if you are using Hermes, you can connect Chrome to the instance of Hermes running on device.

Is there any article/example/docs on how to properly use JSI on Android? Maybe there is already module in current master which uses JSI on Android? I would appreciate any link.

@terrysahaidak I think that the closest thing to that atm is: ericlewis/react-native-hostobject-demo#4

This looks cool and I've already seen it, but not quite sure how to trigger Java code from C++. By the way, does CodeGen is usable in any kind already?

I'm not sure about it being usable - afaik it's still under heavy development. But you can check it out here -> https://github.com/facebook/react-native/tree/master/packages/react-native-codegen

@tomduncalf added a pull request to Eric and my repository showing how to add support for JSI on Android, don't know if that can be used?

ericlewis/react-native-hostobject-demo#4

Here is my attempt to get JSI working on android. And yeah, JSI definitely works, but also JNI works as well! So yeah, we can call Java from JS right now:
https://github.com/terrysahaidak/host-object-test/blob/master/libs/android-jsi/test-jsi/src/main/cpp/TestJSIInstaller.cpp

I've been experimenting with JSI and enjoying the results but I can't figure out how one is expected to resolve a promise from a native worker thread. A reference to the runtime is required to invoke resolve or reject but the reference available when the promise is created is not valid for use within the worker thread. Is there an example somewhere of how this should be done?

@sdc395biosite You can take a look at some of the conversions between JSI <-> JavaScript in the React Native repo here: ReactCommon/turbomodule/core/TurboModuleUtils.cpp.

@chrfalch Thanks for the rapid response. I've read through the code but I'm not sure what I'm supposed to see. Is JSCallInvoker the key? Does this mean calls to JS from JSI still use the bridge? I was expecting to be able to use something like JNI's AttachCurrentThread or that the runtime would block my thread, queue the call to JS for the next process tick, and then release my thread with the result.

Is there any reference documentation for JSI? I have been looking for something comparable to the N-API docs and found nothing with that level of reference material in it.

@WillerZ as I understand it, there are no reference docs yet because JSI is still somewhat in flux. For now you just have to look at header files and example code (e.g. RNTester) I’m afraid

ditto what @tomduncalf is saying, the jsi header file is probably the best documentation atm.

I think that the last word around this should be given by someone from the FB on this.

But I feel it's quite stable; according to the commit history of the folder it's been lightly touched in the last few months.

That said, I wouldn't consider it "officially stable" until there is documentation on the main website.

JSI is a requirement to implement Fabric, which is required to get async rendering with suspense and all those new goodies from React to React Native.

I don't think there are any plans for pushing JSI as the new way of building native modules in the near future - tooling problems like debugging under Chrome not working with JSI modules makes JSI a bit developer unfriendly.

Hi, I'm the main author of JSI, and @TheSavior asked me to comment on this thread.

I would say that JSI is mostly stable. We have no plans to make substantive changes, but we're not ready to declare the API perfectly "stable", either. We have some ideas for improvements, but they are not fully baked, and may require some iteration.

The main improvements we've discussed internally are around making HostFunction a more robust API. For example, the arguments are passed as just a pointer and length which is simple and familiar, but we've seen bugs where people forget to do bounds checks. So we may choose to introduce a object to hold arguments in order to make it possible to do validation.

There has also been interest in making an ABI-stable variant of JSI, which would benefit people shipping binary modules. We're still pinning down what the exact requirements are and how this would affect JSI.

I hope this explains what the current maturity level of JSI is.

The question of documentation has come up here, and also in the Hermes github repo. Questions there tend to be about JSI generally or outside of the context of RN, but the answers should still be mostly useful to people writing RN native modules. facebook/hermes#43 (comment) is something I wrote early on, which includes a pointer to the JSI header file (which is still the best detailed documentation).

Another useful file is the JSI tests which you can find at https://github.com/facebook/hermes/blob/master/API/jsi/jsi/test/testlib.cpp. This file uses Runtime, HostObject, and HostFunction extensively, but because it's trying to be comprehensive, not everything in there is actually a practice I would recommend. All that code will run with JSC or Hermes.

@mhorowitz thanks very much for the detailed update!

We have developed quite a tight integration between our C++ audio engine (using the JUCE framework) and React Native UI, with features like automatic state synchronisation, so JSI is definitely of interest to us in terms of both making the integration more performant, and cleaner in implementation. I look forward to trying out JSI in the near future!

If you’re interested to learn a bit more about our C++/RN integration, I go into more detail about it in this talk about how we built the ROLI LUMI app with React Native, JUCE and Unity from last year’s Audio Developers Conference: https://m.youtube.com/watch?index=42&v=bsy0-mHcS4Y&list=LLgksgv5Myug8PARo0Q2znaQ

So I've not made any progress figuring out how I'm meant to make a call back to my JavaScript from a native thread using the JSI. Assuming I'm using JSC, it seems to me that I need to acquire the JSGlobalContextRef held by the JSCRuntime object supplied to my JSI functions (as a jsi::Runtime). The obvious problem being the lack of any way of invoking JSGlobalContextRetain.

I don't mean to pollute this discussion but my problem does seem to be on-topic as the JSI seems to lack an API for safely keeping a reference to the JSC context in which my JavaScript lives.

Before JSI, our project was directly accessing to JSC by getting private JSGlobalContextRef from the bridge. I was defining lots of native functions in Rust and registring them to the global context. We have native functions for filesystem, sqlite, HTTP server, zip/unzip, download files for both iOS and Android.
After JSI, we have only c++ API with multiple js backend, therefore, we can't use it directly anymore. To solve this, I created a "jsi-c" library which mostly mimics JSCore C API on JSI. Now my rust library links to jsi-c with lots of helper rust macros.

A usage example:

jsi_ffi! {
    pub fn fs_exists(path: String) -> bool {
        PathBuf::from(path).exists()
    }
}

#[no_mangle]
pub unsafe extern "C" fn register_env(ctx: jsi::sys::JSIRuntime) {
    let mynative = jsi::Object::new(ctx);
    mynative.set_fn_property("fs_exists", Some(fs_exists));
    jsi::Object::global(ctx).set_property("$mynative", mynative.into_value());
}

then call register_env function on swift/objc at RCTJavaScriptWillStartExecutingNotification.

My current biggest issue with JSI is it does not have the typed array interface. Javascript Core has it on iOS 10+ and also Hermes has it but JSI has only ArrayBuffer interface. Without a typed array interface, we can't reuse a single typed array for performance reasons and constantly convert between Uint8Array and ArrayBuffer.

The Hermes team maintains JSI. You can ask about TypedArray support on the Hermes repo

Can you clarify what TypedArray functionality is missing from JSI? AFAICT, it is possible to do everything.

how about make JSI a separated shared lib for community, RN and other libs can depend on it, we can make contribution to JSI

I made a separation about JSI from react-native and repository is https://github.com/acton393/JSI .
The sample is https://github.com/acton393/JSISample which reference on https://github.com/ericlewis/react-native-hostobject-demo

@acton393 a complication is that any changes in the JSI lib has to be mirrored by changes in the JSI implementations for the different VMs. Facebook is currently maintaining the JSC and Hermes one, and there is at least one for V8 (from Microsoft I believe).

How would the process of adding new functionality work?

@tmikov JSC VM runtime is ready now (copy from Facebook), iOS sample using JSC runtime
next step I will finish V8 runtime for JSI.

I will update here if any changes

@mhorowitz Is it possible to define a JavaScript class with JSI? I looked ever jsi.h and a few existing examples I could find, and while it is quite straight forward to define object instances, I couldn't figure out how to define a class. I thought maybe I could just define a function, and then new that function in JavaScript in the same way I could if implemented directly in JavaScript, but that just resulted in an error ("TypeError function is not a constructor").

@oNaiPs it sounds like you were adapting NAPI to JSI, so I'm guessing you may have already solved this problem. We have some existing code that uses NAPI to interop between C++ and JavaScript (which relies on defining JavaScript classes), and we're trying to adapt it to JSI so we can use it in React Native. Any learnings you can share?

@ryantrem JavaScript classes are just syntactic sugar over functions, prototypes, and properties. You can do all that in JSI. The main thing you can't currently do is create a Function with a [[Construct]] internal method. In practice, the only real limitation that creates is you can't call new Foo() on such a function. JS constructor semantics are a bit weird, so it might be simpler just to create objects using Foo(), and have it return an object with an appropriate prototype.

It's pretty common to see people use HostObject when they should instead just use a vanilla JS object, and add HostFunction properties to it. Depending on what you're trying to do with your C++ interop layer, this might be the best approach.

Another option is to use a small JS shim. Create a HostFunction FooHostFunc, and JS like this:

function Foo() { return FooHostFunc.apply(this, arguments); };
var f = new Foo();

If you're trying to differentiate between Foo() and new Foo() in your HostFunction, that's also not going to be possible currently, but the uses cases for that are rare.

Thanks @mhorowitz, this approach works great. For reference, here is what I came up with based on your suggestion:

auto code = std::make_shared<const jsi::StringBuffer>("(function() { return TestClass.ctor.apply(this, arguments); })");
auto testClass = jsiRuntime.evaluateJavaScript(std::move(code), "some description").asObject(jsiRuntime);

testClass.setProperty(jsiRuntime, "ctor", jsi::Function::createFromHostFunction(jsiRuntime, jsi::PropNameID::forAscii(jsiRuntime, "TestClass.ctor"), 1, [](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value
{
    auto testObj = thisValue.asObject(runtime);
    testObj.setProperty(runtime, "testProperty1", std::move(arguments[0]));
    return nullptr;
}));

auto testClassPrototype = testClass.getProperty(jsiRuntime, "prototype").asObject(jsiRuntime);
testClassPrototype.setProperty(jsiRuntime, "testProperty2", 55);
testClassPrototype.setProperty(jsiRuntime, "testFunction", jsi::Function::createFromHostFunction(jsiRuntime, jsi::PropNameID::forAscii(jsiRuntime, "testFunction"), 0, [](jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* arguments, size_t count) -> jsi::Value
{
    thisValue.asObject(runtime).setProperty(runtime, "testProperty2", 66);
    return jsi::Value(runtime, jsi::String::createFromAscii(runtime, "this is a test function"));
}));

jsiRuntime.global().setProperty(jsiRuntime, "TestClass", std::move(testClass));

Then in JavaScript I can do this:

const testObj = new TestClass(7);
console.log("TestClass.testProperty1: " + testObj.testProperty1); // prints 7
console.log("TestClass.testProperty2: " + testObj.testProperty2); // prints 55
console.log("TestClass.testFunction(): " + testObj.testFunction()); // prints 'this is a test function'
console.log("TestClass.testProperty2: " + testObj.testProperty2); // prints 66

Regarding this comment:

It's pretty common to see people use HostObject when they should instead just use a vanilla JS object, and add HostFunction properties to it.

Looking over jsi.h, I didn't understand the purpose of HostObject for exactly the reason you state above - everything I wanted to do could be done by creating a jsi::Object and just calling setProperty on it (for values, or functions via HostFunction). The only reason I could think to use HostObject would be if you wanted the storage of the underlying properties to live on the C++ side, or if you wanted all property evaluation to be lazy. In what scenarios would you say HostObject should be used?

That code looks about what I had in mind, glad you made it work.

A nit: evaluateJavaScript is pretty expensive, and it's possible (although not well documented) in resource-constrained environments to package Hermes without the parser/compiler at all, and just use bytecode. For prototyping, it's ok, but in a production environment, I'd include that stub in JS somewhere, and not emit one per "class". If you have many classes, you could write a single JS function like this:

function makeNativeClass(initializer) {
  return function() { return initializer.apply(this, arguments); }
}

and then use it from C++:

// I haven't tested this, so consider it pseudocode
auto initializer = Function::createFromHostFunction(...);
runtime.global().setProperty(runtime, "TestClass",
  runtime.global().getPropertyAsFunction(runtime, "makeNativeClass")
    .call(runtime, initializer));

This avoids runtime calls into the compiler, and avoids per-class JS glue. If you did want to have the instance be a HostObject, then you'd need to change the pattern a bit to create one, but it sounds like you don't.

Which leads to your second question, those are the two clear cases: to associate native data with a JS object (although you can do this with a HostFunction, the pattern is different and less usable), and for lazy proprety evaluation. A subcase of this is when you don't know all the property names in advance.

Interesting, thanks for that extra info. Would it be safe to use jsiRuntime.global().getPropertyAsFunction(jsiRuntime, "eval").call(jsiRuntime, code) instead of jsiRuntime.evaluateJavaScript(code, "some description")? I'm asking because we are doing this work in a lower level library below React Native where we don't have an obvious way to include JavaScript other than injecting it via an API like NAPI or JSI.

If you did want to have the instance be a HostObject, then you'd need to change the pattern a bit to create one

While I don't see a need at this point, I'm curious how we could make this work. My understanding is that when we use new, JavaScript creates the object instance, so I'm not sure how you could make this pattern work when we want the object instance to be a HostObject (which presumably needs to be instantiated by the C++ side of things). What would the pattern for this look like?

Would it be safe to use... eval

eval also calls the parser/compiler, so there's not much of a difference. If you really have no other way, then you may not have a choice, and if the code is small, it's probably ok. You could also compile the JS code when building your library, include the bytecode as an asset, and then call evaluateJavaScript on that. That would avoid needing to run the compiler at runtime.

My understanding is that when we use new, JavaScript creates the object instance

As I mentioned, JS constructor semantics are a bit weird. You can see this in plain JS in any engine. JS will create an object which is used as the this of the constructor. If the ctor doesn't return anything, this is the result of new. If the ctor returns an object, new returns that instead:

>> function C() { var o = Object.create({protoprop: 2}); o.prop = 3; return o; }
undefined
>> c = new C()
{ prop: 3 }
>> c.protoprop
2

So if C returned a HostObject, then new C would return it, too. You could do this by changing makeNativeClass to return the result from initializer, for example.

Hi,

Is the new architecture [fabric] is ready to use shape? can any one answer?

We've been trying to use JSI to integrate Babylon Native into React Native but hit a roadblock when we found that the JSI JSCRuntime implementation doesn't support array buffers. The necessary JSC calls are commented out, even though the version of JSC currently used by React Native supports all these functions (JSValueGetTypedArrayType, JSObjectGetArrayBufferBytesPtr, and JSObjectGetArrayBufferByteLength). Are those comments (and associated commented out code) just out of date? Is there a plan/timeline to enable these functions?

Also, it looks like this (or something very similar) may have been under discussion fairly recently but that discussion seems to be awaiting input from @mhorowitz. cc @bghgary

@ryantrem Based on @tmikov's comment facebook/hermes#182 (comment) I think if you submitted a PR to enable these functions and tested that they work, there's no reason not to accept that.

facebook/hermes#182 is about adding additional functionality to JSI to make TypedArray easier to use and faster, which is related, but enabling ArrayBuffer in JSCRuntime could proceed in parallel.

Hello folks, I'd like to ask questions that will clarify my (and hopefully other people's too) mental model around JSI.

  1. JSI allows to JS to call C++ code (and other way round). For that it uses a bunch of JavaScriptCore apis as seen in JSCRuntime.cpp. Where is this implementation for Hermes?
  2. Will JSI/Fabric help avoiding some "state duplication" across JS and native? To illustrate what I mean: consider the Switch component which has its state in React / JS but the "true state" lives in UIKit. It's theoretically possible that the two states can get out of sync. RN handles it here using a combo of forceUpdate and imperative calls in componentDidUpdate. Will there be some way to remove this kind of state duplication? Is this part of the goal or is it non-goal? I guess in this case it's not really possible (?) but perhaps it could help in some cases in more complex components (TextInput comes to my mind).
  3. just to clarify, when HostObject is mentioned, is that the term as defined here or is that a JSI-specific term?

Thanks a lot for answering!

Hi @vonovak. Thanks for asking!

  1. The HermesRuntime implementation is in https://github.com/facebook/hermes/blob/master/API/hermes/hermes.cpp.
  2. I'll let someone on the RN team take this one.
  3. I don't think there's a simple answer to this question, but I'll give you some context. The definition you reference is from the 2011 version of the ECMAScript spec, and does not exist in the 2015 and newer versions. The newer versions introduce the term "exotic object": "object that does not have the default behaviour for one or more of the essential internal methods". In the context of JSI, I would generally expect that HostObject refers to this specific type. The name is originally inspired by the 2011 spec name. In modern spec terminology, a HostObject provides the native implementation, via Object::createFromHostObject(), of a particular kind of exotic object.

@ryantrem Based on @tmikov's comment facebook/hermes#182 (comment) I think if you submitted a PR to enable these functions and tested that they work, there's no reason not to accept that.

Just to close the loop on this (array buffer support in JSCRuntime), the PR for this was completed and merged. Thanks @mhorowitz, @tmikov, and @shergin for helping make that happen!

@vonovak
Re: 2. No JSI by itself does not do anything to help this. - JSI is the basis for a UI re-architecture "fabric" which might be able to help here. But even then there is a thread boundary between the UI state and the JS state, so its unlikely that we can actually get down to a single shared state. -- UIKit's state (nor the ones in the other platform's UI frameworks) are not multithreaded.

@sercand could you possibly share any more examples/info of how to do this? I'm interested in creating a iOS/Android WebSocket server that talks to React Native via JSI (currently I am using two different WS implementations in Java/Swift and it would be an interesting experiment to see if it is possible to have one rust codebase for it)!

@radiosilence React Native Reanimated 2 uses JSI really intensively where you can access synchronously variables stored in C++ on JS thread and also call some functions on JS which are defined in C++ etc.

@radiosilence @terrysahaidak I've just been poring over the Reanimated source code myself, trying to find the answer to the question I asked above. It seems I was right that callbacks to JS go via the bridge, and that something named CallbackInvokerHolder is involved. I've gotten as far as passing the holder into my JNI code but now I'm stuck as I've no idea how I'm supposed to extract the CallbackInvoker from the holder. Could someone join the dots for me? Reanimated seems to call getCallInvoker but, when I try, JNI throws a NoSuchMethodError. Thanks.

@sdc395biosite I'm not entirely sure what you are trying to do here? You are talking about the bridge but want to call via JSI? Remember that JSI replaces the bridge - but you should be able to get a pointer to the current jsi:runtime through the bridge on startup. Just remember that when you are running the remote web executor (debugging) the runtime won't be available.

@sdc395biosite it's not true, no bridge is used in Reanimated 2 sources. In this particular example, CallInvoker is used only to trigger asynchronously some JS function defined on JS Thread Context from the JS Context running on UI Thread (yeah, separate one).

Here is an example of running a function from JSI:

  1. Define a function in JS:
    https://github.com/software-mansion/react-native-reanimated/blob/028f00845558d2a63c15caef9e719dd838dc13a8/src/reanimated2/core.js#L46-L88
  2. Call JSI Native method to pass it to C++ (string representation of it)
    https://github.com/software-mansion/react-native-reanimated/blob/028f00845558d2a63c15caef9e719dd838dc13a8/src/reanimated2/NativeReanimated.js#L13-L15
  3. Define module spec for this native method:
    https://github.com/software-mansion/react-native-reanimated/blob/master/Common/cpp/NativeModules/NativeReanimatedModuleSpec.cpp#L5-L13
  4. Define the native method itself:
    https://github.com/software-mansion/react-native-reanimated/blob/master/Common/cpp/NativeModules/NativeReanimatedModule.cpp#L96-L99
  5. Here is a part which stores the function in C++:
    https://github.com/software-mansion/react-native-reanimated/blob/master/Common/cpp/SharedItems/ShareableValue.cpp#L86-L91
  6. Call it with or without this:
    https://github.com/software-mansion/react-native-reanimated/blob/master/Common/cpp/SharedItems/MutableValue.cpp#L43-L46

Perhaps, you won't be able to call that function using JNI directly in Java/Kotlin, but I guess there is already some wrapper for this, take a look at fbjni and recent sources of React Native.

Update:
In this example, the module spec is used for TurboModules to work. The Codegen project will allow us to skip that part since all that code will be generated from the definition of the module written in Flow or TypeScript.

If you don't plan to use TurboModules right now, there is a way where you can do everything by yourself.
Here is an example of Reanimated 1 API "bridge" written in C++ with JSI, calling Java code using JNI I wrote almost a year ago when I had almost no knowledge and experience in C++:
https://github.com/terrysahaidak/reanimated-jsi/blob/master/jsi/android/ReanimatedJSI.cpp

Take a look at this get method. It's basically the same thing Codegen generates us for TurboModules:
https://github.com/terrysahaidak/reanimated-jsi/blob/master/jsi/android/ReanimatedJSI.cpp#L136

Also, lots of the code I wrote here is automated by Codegen and TurboModules such as resolving and storing references to Java classes

Here is an example of such generated file:
https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec-generated.cpp

As well as the generated specs for Java classes, for example:
https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeAccessibilityInfoSpec.java

@terrysahaidak With much help from Reanimated 2, I now have a (nearly) working JSI implementation of my SQLite wrapper for RN. One last puzzle remains, and I'm hoping you'll be able to provide some insight.

When I resolve a Promise that I have created on the native side and returned to JS, my app does not respond until I interact with it. It is as if the JS thread, having just resolved the promise, fails to continue at the awaiting location. If I physically interact with the App's UI, JS springs to life and continues with the resolved value.

Is there something I need to do, in addition to resolving the promise, the get JS to continue?

I guess @chrfalch can help here, he has a lot of experience with JSI. But I saw a mention of this problem already. Also, perhaps @shergin or @Kudo can help here.

Could you provide some code snippets to describe what you are doing? Like some isolated code from both the js/native sides?

I think it would be useful to many people if someone who knows their stuff could do a short article with code snippets showcasing JSI and TurboModules stuff, mainly because even the current official Native Modules docs aren't very detailed either.

@mrousavy I've already written up a few articles about the topic:

What I believe should be clear is that JSI is not some magic sauce that will change how we write our React Native apps. JSI is an interface (based on the interface implemented by the JSC engine) that has been added to React Native so that the bridge can be removed by letting native code and JS interface directly. Our apps will eventually run quicker and more reliable with the new UI Architecture called Fabric that uses JSI for everything UI related.

The interface lets us call methods on the native side directly from JavaScript - but there is a lot of details and hidden stuff going on in how it is implemented in React Native (just look at how TurboModules are implemented, and how JS engines like JSC and Hermes are deployed and initialized).

To be able to provide greate developer experience, we are still somewhat limited to the async / await pattern when calling into native code from JS - this has to do with the fact that when debugging we need to run a remote JavaScript engine (Chrome) and interface using messages that are dispatched between our device / simulator and our development tools. (It is not possible to debug your code when enabling TurboModules AFAIK).

From looking at the source code in React Native there are still a lot of native modules that will continue to be in use when running in debug mode, and they all have corresponding TurboModules that are used in a production environment to speed things up at runtime, meaning that we'll still have to live with getting async callbacks from native when calling functions - the big difference will be that the bridge (and it's serialize / deserialize form of communication) can be dropped alltogether - giving us a lot of performance benefits in our release builds.

JSI has a public, stable and well-known API, but the documentation is lacking - leading to giving the React Native core developers a lot more flexibility in how they implement and use it in React Native - there are a lot of changes going on to ensure backwards compatibility and avoid any regressions.

My understanding is that JSI, Fabric and TurboModules will be released and made available for all React Native developers, and then there will be a migration path later for native modules to be moved over and republished as TurboModules. We've also seen evidence that Hermes will be made available on iOS (Build Hermes for iOS), and this will in turn make it possible to write a new set of debuggers and tools that eventually might lead to a better experience writing code that interfaces between JS and native in a well defined way.

I'm also a strong believer in using C++ as the language for writing native logic - and TurboModules and JSI is basically a C++ feature - making it easier for us to create cross platform logic on the native side as well as on the JS side.

Disclaimer: If I'm wrong in any of my assumptions or explanations in this post, please provide feedback and I'll try to correct it - I think it is important that the community knows what these concepts and technologies are for, and what they're not for.

@bghgary and I just went through this for Babylon React Native with lots of help from @RSNara. Here are the things we learned that I think will be helpful to others:

  • When a promise is completed the promise "continuations" (functions passed to .then, or code after await) are "queued" via a call to setImmediate.
  • This immediates queue is flushed at different points in React Native execution, including pretty much any time there is activity across the classic bridge.
  • The immediates queue is also flushed by CallInvoker::invokeAsync after invoking the function you provide on the JS thread (see https://github.com/facebook/react-native/blob/ee3994530d4f76695d77c9a21a6b57429bac68aa/ReactCommon/cxxreact/Instance.cpp#L257-L296, and line 293 specifically).
  • When using JSI with React Native and doing async work on the C++ side that needs to later resolve a promise, you need to use CallInvoker::invokeAsync to properly get back on the JS thread and complete your promise so the above happens. If the immediates queue is not flushed, your pending promise continuations will not run until something else causes the immediates queue to be flushed (such as UI interactions, including tapping the screen).
  • With full TurboModule support, I think this will be trivial as it appears that TurboModules are passed the CallInvoker to their constructors.
  • Without full TurboModule support (the current state we are in with released version of React Native), it's a lot trickier, particularly on the android side. CallInvokerHolder is exposed on the Java side, and you can get the CallInvoker out of it, but:
    • CallInvokerHolder is built on top of fbjni (an Android C++/Java interop layer that simplifies JNI usage). libfbjni.so is included in React Native and available at runtime, but the headers that you need to program against it are not. They are in a Maven package that you can pull into your gradle project though.
    • The Java side of CallInvokerHolder expects to find a library named libturbomodulejsijni.so, which is not currently included in React Native (presumably in the future it will be with full TurboModule support). I believe Reanimated 2 builds the full TurboModule library and includes it in their package. For BabylonReactNative, we compile the absolute minimum (CallInvokerHolder.cpp) into a lib named libturbomodulejsijni.so and include it in our package. I assume if many packages use this workaround, they won't work together in one app since we'll end up with lots of libs named libturbomodulejsijni.so that will confict.
    • Once you have a libturbomodulejsijni.so (with at least CallInvokerHolder.cpp compiled into it) and fbjni, you can get the CallInvoker out of the CallInvokerHolder on the C++ side.

You can see the full set of changes we made to Babylon React Native to start using CallInvoker here: BabylonJS/BabylonReactNative#97

re-posting here a response on twitter from @fkgozali regarding TurboModules and Codegen in RN 0.64
https://twitter.com/fkgozali/status/1335004270991536129?s=21

As long as the modules are specific to your app, you can follow RNTester setup to enable it. All relevant code is already on master and will be included in 0.64. The setup is still rough and not very stable yet, but we’re planning to iterate further on this in early 2021.


edited by @kelset: to add text from tweet

What's the current optimal solution to install HostFunctions into the jsi::Runtime? I have been simply installing them from my native module, but that lead to race conditions/threading issues because that was being invoked on the Native Module Thread, when it really should only be called on the JS thread.

I have tried to play around with JSIModule, JSIModulePackage, etc only to find out that that's not yet actually usable without creating a whole TurboModule. Also I have tried to dispatch to the JS Thread, but that didn't work because the HostFunctions aren't immediately installed when JS starts to execute.

I've created an issue for this here: facebook/react-native#31197, would love to hear some suggestions.

/cc @chrfalch @ryantrem @terrysahaidak @RSNara

Can you tell me JSI's current progress, how long will it take to release?

I've been standing here for 3 years waiting for this new architecture, I love react native

e6a5 commented

@hcminhit Waiting is a sign of true love and patience :)

Can you tell me JSI's current progress, how long will it take to release?

Could you clarify what you need with JSI? Did you just mean getting access to the VM via JSI abstraction? If so, that's already possible today. In fact the JSIExecutor C++ impl today is already accessing the VM using JSI for a while now. All JSI code is already in github: https://github.com/facebook/react-native/blob/master/ReactCommon/jsi/jsi/jsi.h

If you're trying to access the VM to do something with it, you can also try accessing with a logic you can add to your app like in RNTester:

iOS:

Android:

  • It's less ideal right now, but you can access the runtime via JSIModulePackage.
  • See the samples from facebook/react-native#31197 for now.

Note that while this work, you'll be responsible to ensure the access to the VM is safe.


Another alternative is to just write your own subclass of TurboModule C++ class and treat it like a "NativeModule", just in pure C++ (no Java/ObjC). Example: https://github.com/facebook/react-native/blob/dc80b2dcb52fadec6a573a9dd1824393f8c29fdc/ReactCommon/react/nativemodule/samples/ReactCommon/NativeSampleTurboCxxModuleSpecJSI.h#L19, which you can provide in your app like in RNTester: https://github.com/facebook/react-native/blob/master/packages/rn-tester/RNTester/RNTesterTurboModuleProvider.mm#L25

Note: the latter requires TurboModule to be installed in your app, and we're working on creating a playbook for apps in OSS this year for clearer integration, as mentioned in #40 (comment).


For general updates regarding the new architecture, we're also preparing some writeup about it as mentioned in facebook/react-native#31469 (comment). Stay tuned.

I think the tl;dr of @fkgozali's comment is that JSI has already shipped and React Native releases have been using it for a while. For app authors there is no user facing change, it is an implementation detail of React Native. The TurboModule system (that is still in progress) will take advantage of it more. If you are a module author and want to do custom things with it directly, the links Kevin provided might help you out.

Does new all of the "new" architects ready? 2021 will be gone fast.

Another alternative is to just write your own subclass of TurboModule C++ class and treat it like a "NativeModule", just in pure C++ (no Java/ObjC). Example: https://github.com/facebook/react-native/blob/dc80b2dcb52fadec6a573a9dd1824393f8c29fdc/ReactCommon/react/nativemodule/samples/ReactCommon/NativeSampleTurboCxxModuleSpecJSI.h#L19, which you can provide in your app like in RNTester: https://github.com/facebook/react-native/blob/master/packages/rn-tester/RNTester/RNTesterTurboModuleProvider.mm#L25

Note: the latter requires TurboModule to be installed in your app, and we're working on creating a playbook for apps in OSS this year for clearer integration, as mentioned in #40 (comment).

https://github.com/facebook/react-native/blob/dc80b2dcb52fadec6a573a9dd1824393f8c29fdc/ReactCommon/react/nativemodule/samples/ReactCommon/SampleTurboCxxModule.cpp

Is it possible for the getValueWithCallback & getValueWithPromise to be genuinely asynchronous and if so could you give any pointers as regards how to achieve that?

(with the help of @mrousavy I've added a list of libs that use JSI to the top post, so that you can use them as references)

react-native-mmkv-storage also uses JSI to communicate with native side. To make secure storage work in the library I have also accessed Java classes via C++ JNI. It means that any current module that uses the old bridge can easily update their library to use JSI instead keeping the functionality same by accessing java objects from JNI. This also helps remove the need to async/await for getting small data.

Hey everyone, if you are looking to build a JSI module and don't understand much by looking at the code of libraries provided above, You can read my blog post where I have explained everything from scratch in detail.

JSC's C API (which React Native uses) does a lot of locking which makes any function calls into JSI from very large loops slow on iOS.

There's an open issue from 2019 about it here: https://bugs.webkit.org/show_bug.cgi?id=203463, but it seems inactive. Might be worth bumping it.

Hey everyone, is there any example of how to call JS function from c++ ? i found only examples with calling c++ from JS, thanks

@flexsurfer

If it's something global, such as a Promise, use this:

auto promiseCtor = runtime.global().getPropertyAsFunction(runtime, "Promise");
auto promise = promiseCtor.callAsConstructor(runtime, ....);

If it's an anonymous variable (const x = () => ...), then you have to pass it to the C++ func as an argument:

auto nativeFunc = jsi::Function::createFromHostFunction(runtime,
                                                         jsi::PropNameID::forAscii(runtime, "someFunc"),
                                                         1,  // a function
                                                         [](jsi::Runtime& runtime, const jsi::Value&, const jsi::Value* arguments, size_t count) -> jsi::Value {
    auto func = arguments[0].asObject().asFunction();
    return func.call(runtime, jsi::Value(42));
});

thanks a lot, it works
jsiRuntime.global().getPropertyAsFunction(jsiRuntime, "myFunction").call(jsiRuntime);

but if I'm trying to pass a parameter I have an error:

jsiRuntime.global().getPropertyAsFunction(jsiRuntime, "myFunction").call(jsiRuntime, value);

image

EDITED: seems like it's the case

// TODO(cjhopman): this seems unsafe unless we require that it is only called on the main js queue.
@property (nonatomic, readonly) void *runtime;

This is not the ideal place to discuss this. Post this on twitter and tag me (@mrousavy), but I'm guessing you called this code on some thread that's not the JS thread (which is exactly what your screenshot says)

Hey, not sure where/who I should post this but I'm going to post it here. Sorry for the spam.

What's the best approach to exposing a JSI Object that can act like an EventTarget in js, our use case is that want to create JsObjects in the JSI that have onevent listeners?

We've read as much of the code base as I think we can; we've seen that uimanger has constructs for eventemitters, but that codebase seems to imply that the requires the class be subclass of a view/node.

To be clear, we aren't looking to dispatch events, that can happen on the java side, but we would like to be able to expose a JSI Object that supports EventTarget functionality on the global namespace without having to wrap the 'native' object with a js object that interfaces with NativeEventEmitter.

We have also read the Re-animated code base that has AFIAK their own solution for a cpp driven event emitter, but we see that as a bit of overkill?

Basically it would be great to have a class we can implement that would enable the JSI object to marshal the event e.g MyJSObject -> EventTargert -> HostObject or the next best thing.

Btw this going for an OSS project if that counts for anything..

@jahead why do you absolutely need to use JSI for this now? It's definitely possible, but you need to create a JSI HostObject which is exposed to JS, that contains a list of listener functions and then calls them when you trigger it - but

  1. The setup for Android is really hard, you have to use JNI for C++ and if it's an instance you probably want to use fbjni - another thing to learn.
  2. The installation into the JS runtime is a bit tricky
  3. You have to add custom converters for any values (ObjC values -> JS values, Java values (C++ JNI bindings at least) -> JS values)

Why not just use a Native Bridge Module for this now? Once TurboModules land, your Native Bridge Module can easily be migrated to a TurboModule with just a few minor changes, and then it uses JSI. No C++ magic needed.

The module in question requires by spec to need a number of performant synchronous calls and we want to use the dtor for clean up (yes we are aware that it there's no guarantee around GC), we have used the jni for number of internal projects, and we have the initial plumbing for to use the build JSObjects. So 1, 2, 3 not a complete blocker for us, we have a reasonable/working solution - minus some head bumps :D.

What we lack in understanding is whats the appropriate way JSI/C++ to achieve something like an EventTarget class so our child classes can expose event listeners.

We have read the interactions with
https://github.com/facebook/react-native/blob/1465c8f3874cdee8c325ab4a4916fda0b3e43bdb/ReactCommon/react/renderer/core/EventTarget.h
and the associated classes to how react native core is doing something similar, but it all seams to be tied to the view?

basically is there a nice way to handle this currently or do we have standup our own EventTarget/Scheduler/Dispatcher/Emitter pipeline?
as I currently see it we have two options

  1. wrap the jsi object with a js object that bundles in the NativeEmitter
  2. build our own events infrastructure

but we want to make sure the react native team/community doesn't see a different way we should be doing this.
turbo modules isn't really going to solve our issue here, we are trying to expose a Native backed JSObject, we would still have the events problem. just less bolierplate.

if there is any information regarding the subject please let me know.

If I'm understanding your scenario correctly, then implementing events as a callback list that you invoke from native code seems like a pretty reasonable solution (we do this a lot in Babylon Native). As @mrousavy said though, this gets complicated (especially on Android due to fbjni) because you need to get back on the js thread. I posted some details about this earlier in the thread: #91 (comment). It's slightly easier on RN 0.64 plus because it generates librurbomodulejsijni.so for you, but still complicated. As @mrousavy said though I'd expect this to be much easier with TurboModules because I believe they provide you with the js CallInvoker (to get on the js thread).

This seems to be a great time to ask: What's the future of native C++ Android libraries? Currently a lot of community libraries that contain native C/C++ code are being pre-built into a .aar format.
This has a lot of issues:

  1. Autolinking doesn't work, you have to create a dummy java file matching that path
  2. Release cycles get slower because everytime you want to release something you have to build one (or multiple) .aars
  3. If you have version specific code (RNVERSION > 64, RNVERSION <= 64, ..), you have to build one .aar per RN version, which is horrible. Reanimated currently has 4 .aars and counting for each major RN version (62, 63, 64, 65)
  4. If you have to use another native library (such as reactnativejni or fbjni), you have to link it at build time (which is when you build your aar for distribution, and not when the user builds the app!) - makes it hard to upgrade dependencies such as fbjni.
  5. If you use the runtimes (JSC, Hermes, V8) you have to create one .aar per runtime, that's what Reanimated and VisionCamera does - so Reanimated is now up to 8 aars (62 JSC, 62 Hermes, 63 JSC, 63 Hermes, ....)
  6. Users cannot quickly test local changes or patches - they have to try building it from source and that's really really difficult.
  7. Users cannot help debugging a aar library
  8. Package size increases per aar

I see the following solution:

  1. Distribute every library as source and require users to install NDK
  2. Enable prefab for all react native native libraries to make the installation of dependencies (fbjni, reactnativejni, jsi) in third party packages easier.

What do you guys think?

@mrousavy Looks like we need a RFC for this topic

As promised, I have written my next blog on JSI where I talk about converting Native Modules to JSI Modules on Android and iOS.

Read the full blog on the link below:

React Native JSI: Part 2 - Converting Native Modules to JSI Modules

Here is roadmap update from react native team

https://reactnative.dev/blog/2021/08/19/h2-2021

Looks like they are planning to release new architecture by end of this year.

Hi guys!

Is there any easy way to run callback, passed from JS asynchronously?

Right now because of non-thread safety nature of JSI runtime, it's really difficult to do something like this:

auto foo = Function::createFromHostFunction(jsiRuntime, PropNameID::forAscii(jsiRuntime, "foo"), 0,
    [](Runtime & runtime, const Value & thisValue, const Value * arguments, size_t count) -> Value {

        Function callback = arguments[0].getObject(runtime).getFunction(runtime);

        std:: function c_callback = [ & runtime, & callback](const char * message) {
            String string = String::createFromUtf8(runtime, message);

            callback.call(runtime, string);
        };

        asyncModuleRunningSomeCode -> run(data.c_str(), c_callback);

        return nullptr;
    });

jsiRuntime.global().setProperty(jsiRuntime, "foo", std::move(foo));

I've seen workaround made by RNMultithreading's creator here but it looks little bit too complicated πŸ€”

Actually we really need something like

facebook::jsi::runOnJSThread([](){
   foo();
});

@stephenkopylov Hey, RNMultithreading creator here, what you're looking for is a reference to the CallInvoker. That's a property on the RCTBridge (iOS) and CatalystInstance (Android), I think I use this in Multithreading and VisionCamera and JSI-Contacts.
Also make sure to move your callback JSI Value, capturing by reference is a bit dangerous since it might've already been GC'd

@mrousavy thank you for your answers

CallInvoker

Yeap! And right now we have to use some kind of native wrapper (just like here in Reanimated) to have an ability to call something on the same thread that our function was called from

I thing It wold be really useful if we could do it using only <jsi/jsi.h> header

The <jsi/jsi.h> headers are part of the JSI specification - maybe it would be better to put it in TurboModuleUtils which contains extensions like promises etc? That way we won't need to ask the hermes/jcs/v8 engine maintainers to extend their implementations.

Given that you have a reference to the JS runtime and to the callInvoker,
what you need is basically a wrapper that looks something like this:

void runOnJavascriptThread(const jsi::HostFunction& func) {
  callInvoker->invokeAsync([runtime, callinvoker, func = std::move(func)]() {
      func->call(runtime, nullptr, 0)
  });
}

(This was freestyled)

@chrfalch you're absolutely right about header placement

Already having the Runtime the one question lasts - how to get pointer to current CallInvoker instance inside the jsi module...

@chrfalch you're absolutely right about header placement

Already having the Runtime the one question lasts - how to get pointer to current CallInvoker instance inside the jsi module...

You need to find a way to pass it down - like when constructing your class or something?

I've added a small utility class called PlatformContext which contains these objects in my projects - and then I pass a pointer to that around to the objects that needs it.

Well I figured it out, thank you guys - everything works now πŸŽ‰

For all of people who will face the same situation with lack of official documentation I can definitely recommend you this repo - it perfectly describes all you need to know about JSI, turbo modules and calling JS function from another threads

Any document or video that I can learn how JSI works or something like that (I come from Viet Nam so maybe my English skill is not good)

Is there any integration between JSI and Native UI components yet? I'm trying to figure out how to pass a reference to a piece of native state (ex.: contained inside of a host object) as a prop to a native UI component, and so far the best idea I've come up with is passing a pointer around as a string, which is dodgy at best ...

It sounds like you are looking for the new Fabric UI architecture (JSI-based), which I believe is not usable yet: https://reactnative.dev/docs/fabric-renderer

I've noticed some issues with using promises together with async JSI functions. I've raised an issue facebook/react-native#33006

Has anyone here experienced something similar?