Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal about 0002-Turbo-Modules.md #11

Merged
merged 4 commits into from
Oct 11, 2018
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions proposals/0002-Turbo-Modules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# RFC0002: Turbo Modules ™
_Its blazing fast_

## Summary

React Native uses [Native Modules](https://facebook.github.io/react-native/docs/native-modules-ios) as a way for JavaScript to invoke functions in Java/ObjC. Listed under the API section in the [React Native documentation](https://facebook.github.io/react-native/docs/getting-started), popular native modules include Geolocation, Device Information, Async Storage, etc.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would explicitly mention here that Native Modules does not directly tight with UI rendering pipeline and that currently-existing View Managers (which are also Native Modules) will go away soon.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shergin Is there more information on the coming removal of View Managers?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

view managers will be handled as part of fabric rollout: #4

exact details for migration path is TBD

This proposal discusses a new architecture to speed up the way Native Modules are initialized and invoked from JavaScript.

## Motivation

Here are some issues with the current state. Though Android is used as the example, the narrative applies to iOS also.

1. Native Modules are specified in [a package](https://github.com/facebook/react-native/blob/1e8f3b11027fe0a7514b4fc97d0798d3c64bc895/local-cli/link/__fixtures__/android/0.20/MainActivity.java#L37) and are eagerly initialized. The startup time of React Native increases with the number of Native Modules, even if some of those Native Modules are never used. Native Modules can be initialized lazily using [LazyReactPackage](https://github.com/facebook/react-native/blob/42146a7a4ad992a3597e07ead3aafdc36d58ac26/ReactAndroid/src/main/java/com/facebook/react/LazyReactPackage.java) but this does not work in the Open Source repo as the annotation processor [ReactModuleSpecProcessor](https://github.com/facebook/react-native/blob/42146a7a4ad992a3597e07ead3aafdc36d58ac26/ReactAndroid/src/main/java/com/facebook/react/module/processing/ReactModuleSpecProcessor.java) does not run with Gradle yet. Note that there was [a PR to solve this](https://github.com/facebook/react-native/pull/10084), but it was not merged.

2. There is no simple way to check if the Native Modules that JavaScript calls are actually included in the native app. With over the air updates, there is no easy way to check if a newer version of JavaScript calls the right method with the correct set of arguments in Native Module.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if a newer version of JavaScript

What do you think about wording this "if a newer version of the JavaScript bundle" so that folks don't confuse it with a newer JavaScript version like ES2030 or whatever 😄


3. Native Modules are always singleton and their lifecycle is typically tied to the lifetime of the bridge. This issue is compounded in brownfield apps where the React Native bridge may start and shut down multiple times.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/singleton/singletons


4. During the startup process, Native Modules are typically specified in [multiple packages](https://github.com/facebook/react-native/blob/b938cd524a20c239a5d67e4a1150cd19e00e45ba/ReactAndroid/src/main/java/com/facebook/react/CompositeReactPackage.java). We then [iterate](https://github.com/facebook/react-native/blob/407e033b34b6afa0ea96ed72f16cd164d572e911/ReactAndroid/src/main/java/com/facebook/react/ReactInstanceManager.java#L1132) over the list [multiple times](https://github.com/facebook/react-native/blob/617e25d9b5cb10cfc6842eca62ff22d39eefcf7b/ReactAndroid/src/main/java/com/facebook/react/bridge/NativeModuleRegistry.java#L46) before we finally give the bridge a [list of Native Modules](https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java#L124). This does not need to happen at runtime.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be specific to Android. Is it worth mentioning the process that React Native for iOS uses here?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah probably good to have section explaining iOS, though the end goal is to share the c++/jsi layer across platforms


5. The actual methods and constants of a Native Module are [computed during runtime](https://github.com/facebook/react-native/blob/master/ReactCommon/cxxreact/ModuleRegistry.cpp#L81) using [reflection](https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaModuleWrapper.java#L75). This can also be done using build time. The individual method calls themselves are sent over a message queue.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also mention here quickness/grayish-area of methods returning result synchronously, I feel we have to "legalize" them.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

existing NativeModules system already supports returning values synchronously - not a gray area

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fkgozali Is there an officially supported way to return values from Native Modules synchronously? Or is this just something that's present internally but not exposed?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The support has existed for a while, you may use it, but just note that Chrome debugger doesn't support such synchronous call, so it may make debugging JS harder for you. It's kinda why it's not fully documented. See #7 for some context on debugging.

iOS: Use RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD () instead of RCT_EXPORT_METHOD(): https://github.com/facebook/react-native/blob/ca898f4367083e0943603521a41c48dec403e6c9/React/Base/RCTBridgeModule.h#L175

Android: add (isBlockingSynchronousMethod = true) to your @ReactMethod annotation:
https://github.com/facebook/react-native/blob/c8e000b19a3dc47d58bc3ea1ede8211a370d59bb/ReactAndroid/src/test/java/com/facebook/react/bridge/BaseJavaModuleTest.java#L103

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to note: both companies I have worked at ended up using sync calls in a surgical way for notable user-visible perf improvements.

## Background

### JavaScript Interface
The communication between JavaScript and the VM is being standardized using a JavaScript Interface (__JSI__). [Fabric](https://github.com/react-native-community/discussions-and-proposals/issues/4) uses JSI to [expose methods](https://github.com/facebook/react-native/blob/5d9326be29be8688f2238c72c18a9037f983c77d/ReactAndroid/src/main/java/com/facebook/react/fabric/jsc/jni/FabricJSCBinding.cpp#L264) to JavaScript code. Using JSI, JavaScript can hold reference to C++ Host Objects and invoke methods on them.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/reference/references


### Native Modules
> [TODO] - A detailed section on how native modules work today.

## Detailed design

## Defining Native Modules
Internally, we use flow types to define the Native Modules and their method and signatures (also called "shape of Native Modules"). This is then used to generate Java and ObjC classes with method stubs with matching signatures, which can be extended for the actual implementation. We plan to now generate C++ classes with the same shape, that can then be used to call into the Java/ObjC implementations.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does "internally" here refer to Facebook or React Native in general?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Facebook


## Initial Processing
All initial processing will be removed as C++ now knows the shape of Native Modules. A proxy similar to the [NativeModuleProxy](https://github.com/facebook/react-native/blob/da7873563bff945086a70306cc25fa4c048bb84b/ReactCommon/cxxreact/JSCExecutor.cpp#L141) will be defined in C++ and [exposed in JavaScript](https://github.com/facebook/react-native/blob/a93e281428f47ba355ba79cda3e98ca7da677636/Libraries/BatchedBridge/NativeModules.js#L152). The initialization can typically happen when the RootView is initialized, but once we are independent of the bridge, this can also happen at other times.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will still require a compilation step, however. Is that correct?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, everything still needs to be compiled, but c++ doesn’t need to ask Java/ObjC about the “shape of the module” anymore, reducing cost of starting up the system

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that initialization of a native module should/can be tight with life-cycle of RootView; it can be before or after depending on particular module.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree - all I am saying is that it MAY happen at the rootView initialization for now, but this could also happen at other times - typically during a "bridge-prewarmup"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keeping a "warm" bridge around initialized with the common first slice of the RAM bundle was used in a few RN Windows apps depending on if people launched the full app, or the "mini" tray app. I suspect this optimization is more common than even I know.


## Invoking methods on Native Module
When the JavaScript code like `require('NativeModule').DeviceInfoModule` is called, the getter on the proxy is invoked. It would return a reference to object generated in C++ with the shape of the Native Module. Method calls on this reference would simply call over to Java or ObjC. The actual call may also need to have checks to ensure the type and count of arguments.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind removing "simply" here? While it may be simple for you, using this word here might make others feel stupid if they don't understand what's so simple about this step.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use "directly".

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about modules that are 100% implemented in C++? we have a few modules like this, as did my previous employer. my guess is that it would be an even more efficient communication: JS<->C++<->JS, instead of JS<->C++<->Java/ObjC<->C++<->JS, right?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to this end, I think a sample AsyncStorage module that uses C++ iostreams to demonstrate the happy and error paths would be useful. specifically, an interface that takes this kind of race/lifecycle issue into account: microsoft/react-native-windows#1888

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C++ only modules are definitely a good use case to cover. We’ll have some hooks after the objc/java hooks are finalized. We do have some C++ based modules internally, but all of them today need to be backed by objc/java, via CxxNativeModule (eg deferring to the os API to get storage path, etc).

For now, first step is to make TurboModule compatible with CxxNativeModule mechanism, then improve the binding to allow direct C++ impl without objc/java as the second step.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great! avoiding the cost of even "fast" marshaling across JNI/.NET is key for scraping back CPU cycles and memory usage, especially in case of AsyncStorage, WebSocket, etc where giant blobs are potentially being copied today.


> [TODO] - A work in process.

## Open Questions
1. How would the C++ object corresponding to the shape of a Native Module know the Java class (for example com.facebook.react.nativemodule.DeviceInfo) that it needs to call. Does this need to be a switch/case or a map in Java/ObjC that is constructed manually ? Would this be done using BUCK dependencies ?
2. Today, native modules usually take `ReactApplicationContext` as a parameter when they are constructed, but they can also be constructed in other ways like using a builder, or multiple parameters in the constructor. How would this be enforced ?
3. How would the C++ code gen happen in Open Source, where BUCK is not used ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should look into a "compiler" that is similar to the Relay compiler. That is, some tool that would do the code gen for us in OSS, that BUCK could then depend on.

In the OSS version, we could add this as a build script to projects outside of BUCK.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, we can treat this as a “script” that the build system calls/executes, e.g. buck calls this script for us internally at Facebook

4. Since the generated C++ host objects have the same lifetime of the JS scopes they are referred in, what is the API to tell Java/ObjC when these objects are deallocated ?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do they have to have same lifetime as JS scope especially if they are not singletons?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The C++ host objects have destructors (which will be called when the JS object gets GC'ed) which can tell platform specific classes to "clean up" or deallocate themselves.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I know, I know. My point is that JS scope usually can increment and decrement ref-counter, this does not always mean that the life-times are identical. I think we are on the same page, I would just clarify the spec.

5. ReactModule options like `OnBatchCompleteListener` and `hasEagerInitialization` will be removed. Are they needed anymore, if yes - whats the alternative ?
6. This method allows Native Modules to not be singletons. What would this new pattern look like ? Would this lead to memory leaks ?

## Backward Compatiability
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compatiability/Compatibility/s

1. It looks like existing native modules do not have to change since they are usually singletons.