react-native-community / discussions-and-proposals

Discussions and proposals related to the main React Native project
https://reactnative.dev
1.69k stars 127 forks source link

ABI safe JSI #202

Closed tudorms closed 3 years ago

tudorms commented 4 years ago

Introduction

JSI is a great abstraction layer, facilitating experimentation and dynamic designs across different platforms and JavaScript engines; with a growing emphasis on C++ as a solution for sharing code and functionality across the expanding array of React Native platforms and its availability as first-class with TurboModules, the ABI safety of the JSI interface needs to be addressed.

The Core of It

What should the consumption model of React Native be in the future? Specifically, do we want users to build React Native and all modules from source for their applications every time or “install” the framework and any required modules as npm-distributed binaries?

If the answer is binary (for ease of adoption, simplified distribution model, etc.) then we need to think about how those binaries are built. It is conceivable that a TurboModule developer rebuilds binaries every time a new version of ReactNative comes out but that is unlikely to happen. Most likely, we’ll need a stable interface between TurboModules and the React Native framework (namely, JSI) that is resilient to (non-breaking) changes in the interface and differences in toolsets (compilers, standard libraries, release flavors or build flags): an ABI (application binary interface).

Even abstracting away JSI and libraries it uses (like STL), C++ just by itself doesn’t have a universally adopted ABI (certain compilers / versions on certain platforms implement the Itanium C++ ABI), the only truly safe option is to use flat C exports and packed structs (PODs). Empirically we know in practice we can do a bit more (at least across newer versions of the most popular toolsets), such as v-tables (abstract classes / interfaces with all methods pure virtual with exported virtual destructor) used in IUnknown / COM-like APIs.

Options

Work with the existing interface
Switch to a flat C API

Exports will be plain C and interfaces will be wrapped in opaque “handles”; coding directly against such an API may become tedious (be overly verbose). We can also write C++ wrappers that live on either side of the ABI boundary to present a nice object-oriented interface for consumers (and framework developers).

Perhaps we can wrap the exports using N-API (from Node); this will have the additional benefit of paving the way for the existing ecosystem of Node native modules to be ported over for use in React Native (freely or cheaply).

Discussion points

Appendix

Examples of ABI issues:

Thank you for your time!

shergin commented 4 years ago

There are several components of ReactNative written in C++ changes in which can break ABI: Hermes, JSI, Fabric, TurboModules, new Venice interfaces.

We recently discussed this issue at the ReactNative core team and we agreed that even if we recognize the importance of ABI stability for some customers, it's just not feasible to commit to this feature as something fundamental, something that the ReactNative supports out-of-the-box. Here are the reasons:

What should we do?

My hope is that the stable binary interfaces can be built as a separate effort for some subset of APIs (like JSI). We probably need to use plain-C or some small subset of core C++ for that. Seems Microsoft has the right set of constraints and people to lead this effort and make it real.

shergin commented 4 years ago

Another approach to solve this problem would be to rethink which exact APIs should be ABI stable. E.g., we probably should not impose ABI-stability to JSI because it's a too low-level concept which is not supposed to be used by product engineers. Instead, we could probably use C++ TurboModule APIs for that. Now C++ TurboModules have JSI elements in their methods signatures but that's not how it should be, IMO. If we modify TM design (and TM codegen) to use STL containers, we can make modules API somewhat ABI-safe (at least as safe as STL is).

cc @rsnara

shergin commented 4 years ago

For anyone interested in the topic, here are resources I found interesting:

Louis Dionne: "The C++ ABI From the Ground Up"

How to Apple deal with ABI stability of libcpp. Even if the talk discusses a very practical set of approaches to get the stability, it can be perceived as "it's too much of a price of this feature". https://www.youtube.com/watch?v=DZ93lP1I7wU

Titus Winters: CppCast with Titus Winters focused on ABI issues

https://cppcast.com/titus-winters-abi/

Titus Winters: "C++ Past vs. Future"

https://www.youtube.com/watch?v=IY8tHh2LSX4&t=1730s

Titus Winters: ABI - Now or Never

The formal paper from Titus practically suggesting giving up the stability http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1863r1.pdf

John Lakos

In several talks where John discusses some aspects of modules and avoiding recompilation of them, he – I think – implies some ABI stability guarantees. https://www.youtube.com/watch?v=K_fTl_hIEGY


I haven't encountered materials that would explicitly advocate for preserving ABI stability but that would be just because nobody invests in advocating the status quo.

tudorms commented 4 years ago

Thank you for the additional resources @shergin , ABI is a gnarly subject as you pointed out.

In general terms, STL is not ABI safe; I know some toolsets go to great lengths to preserve the ABI stability of their own STL implementations across versions (gcc's libstdc++ even offers guarantees for it with certain exceptions), but if you want to be able to mix&match toolsets that breaks down.

The standard defines the API of STL, thus std::string will have the same API on every standard-compliant STL implementation but the actual memory layout of the std::string structure is different and non-portable across gcc's libstdc++ , llvm's libc++and MSVC's stl and sometimes even across versions or flavors (debug vs release), making it ABI-unsafe. Of course individual binaries can use STL internally as long as they statically link the stdlib or reference a known version of a shared stdlib, but the only way you can pass STL stuff across a binary boundary is if you know all other binaries were built with the same toolset and flags (which as I understand is guaranteed inside Facebook due to the mono-repo design).

I think -for the most part- the API of TurboModules is JSI itself (or at least, a large subset of it), which is actually a great abstraction layer; I'm not sure how the surface could be reduced, except perhaps by going backwards and relying on serializing everything to strings / folly:dynamic like the cxxmodules of old (which would be less than ideal).

Thank you!