Skip to content

Conversation

paradowstack
Copy link

Proposal: Adding first-class ArrayBuffer support to Codegen and TurboModules to enable zero-copy binary data exchange between JavaScript and Native modules.

View the rendered RFC

@kraenhansen
Copy link

kraenhansen commented Oct 12, 2025

Firstly - congrats on a very thorough and well written RFC!

do we need to implement any additional synchronization mechanism to make this change thread-safe?

It's my understanding that the JS engine and JSI itself isn't thread-safe either.
In which case, I'd consider the limitation of thread-safely for ArrayBuffer an extension of that.

Should we introduce two kinds of buffers, mutable and read-only

I think that could be a nice addition indeed. I'd actually imagine a read-only variant would be used in most cases 🤔

Should this RFC focus on introducing only the basic and most valuable synchronous support for ArrayBuffer

Personally - I'd apply the 80-20 rule here and go for the least amount of work bringing most of the value and stick with the basic sync support.


Did you consider "views"? (DataView and typed arrays) and how those would interact with this feature? Could these be passed between JS and Native, if not - what's the failure case like? And should typed arrays be supported in codegen?

@gmemmy
Copy link

gmemmy commented Oct 13, 2025

i really like this. for real-time media or GPU pipelines, say camera ---> ML ---> WebRTC, efficient ArrayBuffer bridging would make a world of difference. it'd enable moving small binary payloads(LUTs, masks, uniform buffers) without having to serialize or clone data just to cross the bridge.

a few things worth clarifying though:

  • thread safety: how do concurrent module calls handle shared buffers? media pipelines often run multiple workers in parallel, so locking semantics matter.

  • read-only vs writable: some data(e.g frame masks or GPU uploads) should likely be immutable once passed to JS; being explicit here could save a lot of edge-case debugging imo.

@paradowstack
Copy link
Author

Firstly - congrats on a very thorough and well written RFC!

Thanks ❤️

do we need to implement any additional synchronization mechanism to make this change thread-safe?

It's my understanding that the JS engine and JSI itself isn't thread-safe either. In which case, I'd consider the limitation of thread-safely for ArrayBuffer an extension of that.

Agree with that.

Should we introduce two kinds of buffers, mutable and read-only

I think that could be a nice addition indeed. I'd actually imagine a read-only variant would be used in most cases 🤔

I agree with that, but after deeper investigation I couldn't find an easy and clean way to achieve that. One way would be to create a read-only view over the buffer when processing it, but that's on the developers. Also there is an active TC39 proposal for an Immutable ArrayBuffer which would provide a standardized, runtime-enforced way to prevent modifications to the buffer contents.

Should this RFC focus on introducing only the basic and most valuable synchronous support for ArrayBuffer

Personally - I'd apply the 80-20 rule here and go for the least amount of work bringing most of the value and stick with the basic sync support.

👍

Did you consider "views"? (DataView and typed arrays) and how those would interact with this feature? Could these be passed between JS and Native, if not - what's the failure case like? And should typed arrays be supported in codegen?

My idea is to have this solution type-agnostic as the underlying native classes, such as NSMutableData, java.nio.ByteBuffer and jsi::ArrayBuffer are an opaque containers for raw, uninterpreted bytes. If DataView or TypedArray is passed instead of ArrayBuffer, the developer should receive an appropriate warning.

@tom-sherman
Copy link

tom-sherman commented Oct 14, 2025

The semantics around memory ownership seem to deviate from the spec of ArrayBuffer in regards to transferring/detaching. I'm not sure of what the material consequences are, especially with existing code that handles ArrayBuffers, but it seems like this could break developer expectations in many ways.

I'd expect the buffer to be moved, not borrowed, when passing between JS and native (in both directions).

ArrayBuffer implementations are not thread-safe; if multiple threads simultaneously read from or write to an ArrayBuffer, race conditions can occur. To prevent this, developers must ensure that an ArrayBuffer is not accessed concurrently from different threads

This problem goes away if ownership is moved to the receiving thread. You shouldn't be able to even read an ArrayBuffer from multiple threads.

  1. Thread-safety of the ArrayBuffer - do we need to implement any additional synchronization mechanism to make this change thread-safe?

So in summary and to answer this unresolved question, I would say absolutely yes. And to do so by using moves and not borrows.

@paradowstack
Copy link
Author

Thanks @tom-sherman for your input!

Regarding this:

ArrayBuffer implementations are not thread-safe; if multiple threads simultaneously read from or write to an ArrayBuffer, race conditions can occur. To prevent this, developers must ensure that an ArrayBuffer is not accessed concurrently from different threads

This problem goes away if ownership is moved to the receiving thread. You shouldn't be able to even read an ArrayBuffer from multiple threads.

  1. Thread-safety of the ArrayBuffer - do we need to implement any additional synchronization mechanism to make this change thread-safe?

So in summary and to answer this unresolved question, I would say absolutely yes. And to do so by using moves and not borrows.

I agree that moving (transferring ownership) an ArrayBuffer coul be fundamentally safer and cleaner than borrowing it. However, the primary technical challenge remains: the current JSI and Hermes Runtime implementations do not expose a dedicated API for "detaching" an ArrayBuffer from the JavaScript side. Without true detachment, the only immediate way to transfer ownership is by "moving" the underlying buffer to the native thread and extending its lifetime accordingly. This addresses the memory management aspect but has a critical flaw:

  • JS Validity: The ArrayBuffer remains valid on the JS side. Its properties, such as byteLength, are not cleared.
  • Thread Safety Risk: The buffer can still be read or written to simultaneously from the JS thread while it is being used natively. This creates an easy opportunity for developers to violate thread-safety rules, even if documentation warns against post-transfer access.

I currently do not see a clean, safe path to fully implement buffer transfers that invalidate the JS reference. Achieving this requires dedicated changes to both the JSI specification and the underlying Hermes engine to introduce a proper detachment mechanism. Since I don't have a deep expertise in this topic, output from more experienced developers is really welcome and highly appreciated.

@gmaclennan
Copy link

I don't have any expertise as to how to solve the invalidation of JS references in Hermes and JSI, but I wanted to add another voice highlighting the importance of ownership transfer. As far as I am aware, JS does not have other instances where a developer needs to think about thread-safety - it is always thread safe by default. When working with threads (e.g. a Worker in node or a WebWorker in the browser), references are either copied or transferred (e.g. zero-copy, but the reference is removed in the source thread). A JS developer needing to be aware that they cannot modify an ArrayBuffer while it is being written in the native thread is a big ask, especially as we are likely talking about developers who are consuming native modules which use this code, and may not be familiar with thread-safety as a concept at all. This might be a really hard problem to solve though as @paradowstack says!

Copy link
Member

@grabbou grabbou left a comment

Choose a reason for hiding this comment

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

Thanks for this RFC. Excited to get this going. Leaving some comments for consideration when it comes to the RFC document itself.

```ts
export interface Spec extends TurboModule {
getBuffer(): ArrayBuffer;
processBuffer(buffer: ArrayBuffer): void;
Copy link
Member

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.


## 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.
Copy link
Member

Choose a reason for hiding this comment

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

I would say current workaround is typically working directly with JSI, which has its own advantages and disadvantages. Great example here is prior art by Marc, who did a lot of this manually before migrating over to Nitro Modules. I also think a lot of Software Mansion libraries go with C++ and work directly with JSI for that reason as well.


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.
Copy link
Member

Choose a reason for hiding this comment

The 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.

- **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.
Copy link
Member

Choose a reason for hiding this comment

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

Another important indication here is that first-class support for ArrayBuffers will enable support of web spec compatible primitives (such as Blob or File). It is also great way to enable interoperability across different module frameworks.

For example, Expo currently has its own Blob implementation (based on ArrayBuffers, which they support). If React Native has same level of support, libraries relying on each will be able to work interchangeably, which is great.

@grabbou
Copy link
Member

grabbou commented Oct 17, 2025

The semantics around memory ownership seem to deviate from the spec of ArrayBuffer in regards to transferring/detaching. I'm not sure of what the material consequences are, especially with existing code that handles ArrayBuffers, but it seems like this could break developer expectations in many ways.

That said, it's worth noting that (unless I'm mistaken) this is already how JSI, Expo Modules, and Nitro Modules handle ArrayBuffers today.

On one hand, aligning with existing community behavior might make sense for practical and compatibility reasons. On the other hand, once this behavior becomes part of the core, its reach and visibility will likely expand far beyond those ecosystems, making the current de facto behavior less relevant over time.

Leaving this as an open question and summoning a few folks from the community for feedback!

To conclude, supporting only simple function input/output parameters is straightforward, but extending that support to cover all Codegen functionalities across every platform is significantly more complex. However, integrating Promises and asynchronous operations may be crucial for supporting binary-heavy use cases. This leads to the question:

> [!IMPORTANT]
> Should this RFC focus on introducing only the basic and most valuable synchronous support for ArrayBuffer, or should it aim for full coverage of all possible Codegen use cases, including asynchronous operations, despite the higher complexity and the impact on more files (especially on Android)?
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants