-
Notifications
You must be signed in to change notification settings - Fork 141
RFC: ArrayBuffer support in TurboModules #947
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,381 @@ | ||||||
| --- | ||||||
| title: ArrayBuffer support in TurboModules | ||||||
| author: | ||||||
| - Kamil Paradowski <[email protected]> | ||||||
| date: 10.10.2025 | ||||||
| --- | ||||||
|
|
||||||
| # RFC0000: ArrayBuffer support in TurboModules | ||||||
|
|
||||||
| ## Summary | ||||||
|
|
||||||
| This RFC outlines adding first-class `ArrayBuffer` support to TurboModules to enable zero-copy binary data exchange between JavaScript and native modules. | ||||||
|
|
||||||
| Codegen and TurboModules currently lack efficient binary data handling, forcing developers to rely on workarounds, such as using the `CodegenTypes.UnsafeObject` type or inefficient conversion to `base64`, when working with binary-heavy use cases. This feature addresses performance bottlenecks in data-intensive applications. By providing first-class `ArrayBuffer` support, developers will be able to seamlessly pass large binary payloads between JavaScript and native code without the overhead of data serialization and copying. This enhancement will unlock new possibilities for creating responsive, high-performance React Native applications across various domains including media processing, real-time communication, and AI/ML applications that require efficient handling of tensors and model data. | ||||||
|
|
||||||
| ## Basic example | ||||||
|
|
||||||
| Developers should be able to define a TypeScript spec as follows: | ||||||
|
|
||||||
| ```ts | ||||||
| export interface Spec extends TurboModule { | ||||||
| getBuffer(): ArrayBuffer; | ||||||
| processBuffer(buffer: ArrayBuffer): void; | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ## Motivation | ||||||
|
|
||||||
| TurboModules currently lack a first-class way to represent `ArrayBuffer` end-to-end in Codegen, which forces developers to rely on copies, ad-hoc platform bridges, global helpers, or external libraries. This hurts performance for binary-heavy use cases such as media data or ML tensors, and it increases implementation complexity. The expected outcome is a cross-platform contract that lets JS and native pass binary data with minimal copying. Codegen should be able to generate working code for the `ArrayBuffer` type on every platform. | ||||||
|
||||||
|
|
||||||
| For example, several important use cases are currently difficult to implement efficiently while working with TurboModules: | ||||||
|
|
||||||
| - **Real-time media streaming**: A native video decoder could stream frames directly to a JavaScript-based player component. Without zero-copy `ArrayBuffer`s, each frame would need to be copied, leading to significant performance overhead and potential frame drops. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think great example here would be to use Blob Manager as an example (and likely first candidate to migrate over, once this lands). Broadly speaking working with binary data in general. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great catch, will add a Blob Manager example to the next version of the RFC. |
||||||
| - **Machine Learning**: On-device ML models often require passing large tensors between native inference engines and JS. Copying this data can be a major bottleneck, especially for real-time applications like video analysis. | ||||||
| - **High-performance networking**: Applications that handle large binary payloads over WebSockets or other protocols (e.g., financial data streams, real-time gaming) may be forced into inefficient data conversion, which adds CPU and memory pressure. | ||||||
|
|
||||||
| By providing a first-class `ArrayBuffer` type support to TurboModules, this RFC will unblock these and other performance-sensitive areas, making it possible to develop faster more efficient applications for React Native. | ||||||
|
||||||
|
|
||||||
| ## Detailed design | ||||||
|
|
||||||
| This section contains a description of the design and related topics. It's split by topics and affected packages/areas. In the expandable sections, extensive code snippets can be found. In the final implementation, each area should come with appropriate unit tests. | ||||||
|
|
||||||
| ### Memory ownership | ||||||
|
|
||||||
| When passing an `ArrayBuffer` to native code, it should always be treated as "borrowed" or "non-owning": JS owns the ArrayBuffer's memory and the JS GC is responsible for freeing it. Native code should access the passed memory only for the duration of the synchronous call. | ||||||
|
||||||
| When passing an `ArrayBuffer` to native code, it should always be treated as "borrowed" or "non-owning": JS owns the ArrayBuffer's memory and the JS GC is responsible for freeing it. Native code should access the passed memory only for the duration of the synchronous call. | |
| When passing an `ArrayBuffer` that was created in JS to native code, it should always be treated as "borrowed" or "non-owning": JS owns the ArrayBuffer's memory and the JS GC is responsible for freeing it. Native code should access the passed memory only for the duration of the synchronous call. |
Let's clarify this a bit here; only if it was created in JS, we don't have a std::shared_ptr<jsi::MutableBuffer>.
If it was created in native, we can technically unwrap the native buffer again and can assume ownership safely. But; see my other discussion for more info on this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for clarification!
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this so? createArrayBuffer uses a shared_ptr so it should be feasible to implemented shared ownership semantics here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed - ownership can transfer from native to JS here, that's perfectly fine. We use this in a lot of Nitro libraries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought that aligning the ownership strategy for both directions would make sense - but of course you are right, it would work fine for case of "native to JS" direction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would recommend we use the existing fbjni abstraction here: https://github.com/facebookincubator/fbjni/blob/main/cxx/fbjni/ByteBuffer.cpp#L84
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missed that one - thanks!
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| While in C++ and Objective-C data can be easily shared between JS and Native, Java stores the data as a `folly::dynamic` map on the Native side. The `folly` library has support for data buffers (class `IOBuf`). This means that JS buffers can be stored on the Native side, but its implementation will be more challenging. Moreover, classes responsible for storing variables, such as `NativeMap` or `NatviveArray`, have a rich inheritance tree and are widely used across the JNI files. Adding storage for buffers to them will require changes to a large number of `ReactAndroid` JNI and Java/Kotlin files. These changes are required to add support for e.g. Promises or Structs. | |
| While in C++ and Objective-C data can be easily shared between JS and Native, Java stores the data as a `folly::dynamic` map on the Native side. The `folly` library has support for data buffers (class `IOBuf`). This means that JS buffers can be stored on the Native side, but its implementation will be more challenging. Moreover, classes responsible for storing variables, such as `NativeMap` or `NativeArray`, have a rich inheritance tree and are widely used across the JNI files. Adding storage for buffers to them will require changes to a large number of `ReactAndroid` JNI and Java/Kotlin files. These changes are required to add support for e.g. Promises or Structs. |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While in C++ and Objective-C data can be easily shared between JS and Native, Java stores the data as a
folly::dynamicmap on the Native side.
Yea I found the same issue a while ago - folly is quite cool but wrapping everything in dynamic probably has to go at some point in the future.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my opinion, the RFC should aim to cover all use cases and describe the complete implementation plan. We can then approach it incrementally, breaking the work into smaller, manageable PRs. It might also make sense to update the Adoption Strategy section with a detailed roll-out plan that outlines the milestones we’ll follow once this RFC is approved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea - will do that!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we extend the RFC with some notes on asynchronous functions? I know there was discussion beforehand whether to implement asynchronous code in the first PR, however, I would treat that separate from the RFC itself, which could cover broader use case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I unnecessarily coupled this RFC with the first PR in my head. Asynchronous functions are mentioned a bit later, but they should be outlined here as well as this is the final API we would like to have.