Skip to content

Commit 5bb9639

Browse files
Kamil Paradowskimeta-codesync[bot]
authored andcommitted
JArrayBuffer zero-copy class for Java TM (#57897)
Summary: Android TurboModules mapped a JS `ArrayBuffer` to `java.nio.ByteBuffer`, copying every argument into a direct buffer — and `ByteBuffer` carries no ownership contract, so there was no way to express aliased or borrowed bytes for synchronous in-place access. This adds `ArrayBuffer` (`packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ArrayBuffer.kt`) as the Java representation of an `ArrayBuffer`. It carries an `isOwningBytes` flag: an owning buffer can be stored and returned to JS, a non-owning one aliases bytes valid only for the synchronous call that produced it. Codegen now emits `ArrayBuffer` for `ArrayBufferTypeAnnotation` params (was `ByteBuffer`) and returns (was `ByteBuffer`). ## Changelog: [ANDROID] [ADDED] - Add `ArrayBuffer`, the Java representation of a JS `ArrayBuffer` for TurboModules, with an explicit byte-ownership contract Pull Request resolved: #57897 Test Plan: - Codegen Java spec and JNI C++ snapshot tests updated for `ArrayBuffer` param/return signatures. - `SampleTurboModule` doubles its sync argument in place and returns the same buffer, covering the zero-copy path end to end; `createNativeBuffer` allocates via `ArrayBuffer`. - C++ API snapshots regenerated. Reviewed By: javache Differential Revision: D115755247 Pulled By: christophpurrer fbshipit-source-id: de067789ad145b7202da721a02f838358c82f4d8
1 parent 4bf5575 commit 5bb9639

15 files changed

Lines changed: 409 additions & 48 deletions

File tree

packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,8 @@ function translateFunctionParamToJavaType(
282282
imports.add('com.facebook.react.bridge.Callback');
283283
return wrapOptional('Callback', isRequired);
284284
case 'ArrayBufferTypeAnnotation':
285-
imports.add('java.nio.ByteBuffer');
286-
return wrapOptional('ByteBuffer', isRequired);
285+
imports.add('com.facebook.react.bridge.ArrayBuffer');
286+
return wrapOptional('ArrayBuffer', isRequired);
287287
default:
288288
realTypeAnnotation.type as 'MixedTypeAnnotation';
289289
throw new Error(createErrorMessage(realTypeAnnotation.type));
@@ -379,8 +379,8 @@ function translateFunctionReturnTypeToJavaType(
379379
imports.add('com.facebook.react.bridge.WritableArray');
380380
return wrapOptional('WritableArray', isRequired);
381381
case 'ArrayBufferTypeAnnotation':
382-
imports.add('java.nio.ByteBuffer');
383-
return wrapOptional('ByteBuffer', isRequired);
382+
imports.add('com.facebook.react.bridge.ArrayBuffer');
383+
return wrapOptional('ArrayBuffer', isRequired);
384384
default:
385385
realTypeAnnotation.type as 'MixedTypeAnnotation';
386386
throw new Error(createErrorMessage(realTypeAnnotation.type));

packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ function translateParamTypeToJniType(
311311
case 'FunctionTypeAnnotation':
312312
return 'Lcom/facebook/react/bridge/Callback;';
313313
case 'ArrayBufferTypeAnnotation':
314-
return 'Ljava/nio/ByteBuffer;';
314+
return 'Lcom/facebook/react/bridge/ArrayBuffer;';
315315
default:
316316
realTypeAnnotation.type as 'MixedTypeAnnotation';
317317
throw new Error(
@@ -397,7 +397,7 @@ function translateReturnTypeToJniType(
397397
case 'ArrayTypeAnnotation':
398398
return 'Lcom/facebook/react/bridge/WritableArray;';
399399
case 'ArrayBufferTypeAnnotation':
400-
return 'Ljava/nio/ByteBuffer;';
400+
return 'Lcom/facebook/react/bridge/ArrayBuffer;';
401401
default:
402402
realTypeAnnotation.type as 'MixedTypeAnnotation';
403403
throw new Error(

packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJavaSpec-test.js.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,11 @@ Map {
5858
package com.facebook.fbreact.specs;
5959
6060
import com.facebook.proguard.annotations.DoNotStrip;
61+
import com.facebook.react.bridge.ArrayBuffer;
6162
import com.facebook.react.bridge.ReactApplicationContext;
6263
import com.facebook.react.bridge.ReactContextBaseJavaModule;
6364
import com.facebook.react.bridge.ReactMethod;
6465
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
65-
import java.nio.ByteBuffer;
6666
import javax.annotation.Nonnull;
6767
import javax.annotation.Nullable;
6868
@@ -80,15 +80,15 @@ public abstract class NativeSampleTurboModuleSpec extends ReactContextBaseJavaMo
8080
8181
@ReactMethod(isBlockingSynchronousMethod = true)
8282
@DoNotStrip
83-
public abstract ByteBuffer getArrayBuffer();
83+
public abstract ArrayBuffer getArrayBuffer();
8484
8585
@ReactMethod
8686
@DoNotStrip
87-
public abstract void voidArrayBuffer(ByteBuffer arg);
87+
public abstract void voidArrayBuffer(ArrayBuffer arg);
8888
8989
@ReactMethod
9090
@DoNotStrip
91-
public abstract void voidNullableArrayBuffer(@Nullable ByteBuffer arg);
91+
public abstract void voidNullableArrayBuffer(@Nullable ArrayBuffer arg);
9292
}
9393
",
9494
}

packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,17 @@ namespace facebook::react {
5353
5454
static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_getArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
5555
static jmethodID cachedMethodId = nullptr;
56-
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, ArrayBufferKind, \\"getArrayBuffer\\", \\"()Ljava/nio/ByteBuffer;\\", args, count, cachedMethodId);
56+
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, ArrayBufferKind, \\"getArrayBuffer\\", \\"()Lcom/facebook/react/bridge/ArrayBuffer;\\", args, count, cachedMethodId);
5757
}
5858
5959
static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_voidArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
6060
static jmethodID cachedMethodId = nullptr;
61-
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidArrayBuffer\\", \\"(Ljava/nio/ByteBuffer;)V\\", args, count, cachedMethodId);
61+
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidArrayBuffer\\", \\"(Lcom/facebook/react/bridge/ArrayBuffer;)V\\", args, count, cachedMethodId);
6262
}
6363
6464
static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_voidNullableArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
6565
static jmethodID cachedMethodId = nullptr;
66-
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidNullableArrayBuffer\\", \\"(Ljava/nio/ByteBuffer;)V\\", args, count, cachedMethodId);
66+
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidNullableArrayBuffer\\", \\"(Lcom/facebook/react/bridge/ArrayBuffer;)V\\", args, count, cachedMethodId);
6767
}
6868
6969
NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const JavaTurboModule::InitParams &params)

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,28 @@ public final class com/facebook/react/bridge/Arguments {
537537
public static final fun toList (Lcom/facebook/react/bridge/ReadableArray;)Ljava/util/ArrayList;
538538
}
539539

540+
public final class com/facebook/react/bridge/ArrayBuffer : com/facebook/jni/HybridClassBase {
541+
public static final field Companion Lcom/facebook/react/bridge/ArrayBuffer$Companion;
542+
public fun <init> (I)V
543+
public synthetic fun <init> (Ljava/nio/ByteBuffer;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V
544+
public static final fun arrayBufferWithCopiedBytes (Lcom/facebook/react/bridge/ArrayBuffer;)Lcom/facebook/react/bridge/ArrayBuffer;
545+
public static final fun arrayBufferWithCopiedBytes (Ljava/nio/ByteBuffer;)Lcom/facebook/react/bridge/ArrayBuffer;
546+
public static final fun arrayBufferWithCopiedBytes ([B)Lcom/facebook/react/bridge/ArrayBuffer;
547+
public static final fun arrayBufferWithLength (I)Lcom/facebook/react/bridge/ArrayBuffer;
548+
public static final fun arrayBufferWithOwnedBytes (Ljava/nio/ByteBuffer;)Lcom/facebook/react/bridge/ArrayBuffer;
549+
public final fun getBytes ()Ljava/nio/ByteBuffer;
550+
public final fun getSize ()I
551+
public final fun isOwningBytes ()Z
552+
}
553+
554+
public final class com/facebook/react/bridge/ArrayBuffer$Companion {
555+
public final fun arrayBufferWithCopiedBytes (Lcom/facebook/react/bridge/ArrayBuffer;)Lcom/facebook/react/bridge/ArrayBuffer;
556+
public final fun arrayBufferWithCopiedBytes (Ljava/nio/ByteBuffer;)Lcom/facebook/react/bridge/ArrayBuffer;
557+
public final fun arrayBufferWithCopiedBytes ([B)Lcom/facebook/react/bridge/ArrayBuffer;
558+
public final fun arrayBufferWithLength (I)Lcom/facebook/react/bridge/ArrayBuffer;
559+
public final fun arrayBufferWithOwnedBytes (Ljava/nio/ByteBuffer;)Lcom/facebook/react/bridge/ArrayBuffer;
560+
}
561+
540562
public final class com/facebook/react/bridge/AssertionException : java/lang/RuntimeException {
541563
public fun <init> (Ljava/lang/String;)V
542564
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.bridge
9+
10+
import com.facebook.jni.HybridClassBase
11+
import com.facebook.proguard.annotations.DoNotStrip
12+
import java.nio.ByteBuffer
13+
14+
/**
15+
* A fixed-length byte buffer for TurboModule `ArrayBuffer` arguments and return values.
16+
*
17+
* @property isOwningBytes:
18+
* - `true` — safe to retain and return to JS. Synchronize externally if JS may touch the same
19+
* memory concurrently.
20+
* - `false` — bytes are borrowed from a JS `ArrayBuffer` for the current synchronous call only.
21+
* Copy with [arrayBufferWithCopiedBytes] to keep them.
22+
*/
23+
@DoNotStrip
24+
public class ArrayBuffer : HybridClassBase {
25+
26+
private val buffer: ByteBuffer
27+
28+
/** Whether this buffer owns its bytes. See the class documentation. */
29+
public val isOwningBytes: Boolean
30+
31+
@DoNotStrip
32+
private constructor(buffer: ByteBuffer, isOwningBytes: Boolean) : super() {
33+
this.buffer = buffer
34+
this.isOwningBytes = isOwningBytes
35+
}
36+
37+
/** @param size number of zero-filled bytes to allocate */
38+
public constructor(size: Int) : this(allocateDirect(size), true) {
39+
initHybrid(buffer, isOwningBytes)
40+
}
41+
42+
public val bytes: ByteBuffer
43+
get() = buffer
44+
45+
public val size: Int
46+
get() = buffer.capacity()
47+
48+
private external fun initHybrid(buffer: ByteBuffer, isOwningBytes: Boolean)
49+
50+
public companion object {
51+
init {
52+
ReactNativeJniCommonSoLoader.staticInit()
53+
}
54+
55+
/** @param size number of zero-filled bytes to allocate. Same as `ArrayBuffer(size)`. */
56+
@JvmStatic
57+
@DoNotStrip
58+
public fun arrayBufferWithLength(size: Int): ArrayBuffer = ArrayBuffer(size)
59+
60+
/** @param bytes copied into a new owning buffer */
61+
@JvmStatic
62+
@DoNotStrip
63+
public fun arrayBufferWithCopiedBytes(bytes: ByteArray): ArrayBuffer {
64+
val buffer = ArrayBuffer(bytes.size)
65+
if (bytes.isNotEmpty()) {
66+
buffer.bytes.put(bytes)
67+
buffer.bytes.rewind()
68+
}
69+
return buffer
70+
}
71+
72+
/** @param source remaining bytes are copied into a new owning buffer */
73+
@JvmStatic
74+
@DoNotStrip
75+
public fun arrayBufferWithCopiedBytes(source: ByteBuffer): ArrayBuffer {
76+
val length = source.remaining()
77+
val buffer = ArrayBuffer(length)
78+
if (length > 0) {
79+
buffer.bytes.put(source.duplicate())
80+
buffer.bytes.rewind()
81+
}
82+
return buffer
83+
}
84+
85+
/**
86+
* @param source copied into a new owning buffer. Use to keep bytes from a non-owning argument
87+
* after the call returns.
88+
*/
89+
@JvmStatic
90+
@DoNotStrip
91+
public fun arrayBufferWithCopiedBytes(source: ArrayBuffer): ArrayBuffer {
92+
val length = source.size
93+
val buffer = ArrayBuffer(length)
94+
if (length > 0) {
95+
val src = source.bytes.duplicate()
96+
src.position(0)
97+
src.limit(length)
98+
buffer.bytes.put(src)
99+
buffer.bytes.rewind()
100+
}
101+
return buffer
102+
}
103+
104+
/**
105+
* @param buffer direct [ByteBuffer] to alias without copying. The caller must keep it valid for
106+
* as long as this [ArrayBuffer] lives.
107+
*/
108+
@JvmStatic
109+
@DoNotStrip
110+
public fun arrayBufferWithOwnedBytes(buffer: ByteBuffer): ArrayBuffer {
111+
require(buffer.isDirect) { "arrayBufferWithOwnedBytes requires a direct ByteBuffer" }
112+
return ArrayBuffer(buffer, true).apply { initHybrid(buffer, isOwningBytes) }
113+
}
114+
115+
private fun allocateDirect(size: Int): ByteBuffer {
116+
require(size >= 0) { "ArrayBuffer size must not be negative, got $size" }
117+
return ByteBuffer.allocateDirect(size)
118+
}
119+
}
120+
}

packages/react-native/ReactAndroid/src/main/jni/react/jni/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ include(${REACT_ANDROID_DIR}/src/main/jni/first-party/jni-lib-merge/SoMerging-ut
2323
add_library(
2424
reactnativejni_common
2525
OBJECT
26+
JArrayBuffer.cpp
2627
JDynamicNative.cpp
2728
JReactMarker.cpp
2829
NativeArray.cpp
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include "JArrayBuffer.h"
9+
10+
#include <cstring>
11+
#include <span>
12+
#include <utility>
13+
#include <vector>
14+
15+
#include "JByteBufferMutableBuffer.h"
16+
17+
namespace facebook::react {
18+
19+
namespace {
20+
21+
// Holds a copy of bytes borrowed from a JS ArrayBuffer.
22+
class OwnedBytesBuffer final : public jsi::MutableBuffer {
23+
public:
24+
explicit OwnedBytesBuffer(std::vector<uint8_t> bytes) noexcept
25+
: bytes_(std::move(bytes)) {}
26+
27+
size_t size() const override {
28+
return bytes_.size();
29+
}
30+
31+
uint8_t* data() override {
32+
return bytes_.data();
33+
}
34+
35+
private:
36+
std::vector<uint8_t> bytes_;
37+
};
38+
39+
} // namespace
40+
41+
void JArrayBuffer::registerNatives() {
42+
registerHybrid({
43+
makeNativeMethod("initHybrid", JArrayBuffer::initHybrid),
44+
});
45+
}
46+
47+
void JArrayBuffer::initHybrid(
48+
jni::alias_ref<jhybridobject> jobj,
49+
jni::alias_ref<jni::JByteBuffer> buffer,
50+
jboolean owningBytes) {
51+
setCxxInstance(
52+
jobj,
53+
std::make_shared<JByteBufferMutableBuffer>(buffer),
54+
owningBytes != JNI_FALSE);
55+
}
56+
57+
jni::local_ref<JArrayBuffer::javaobject> JArrayBuffer::create(
58+
jni::local_ref<jni::JByteBuffer> byteBuffer,
59+
std::shared_ptr<jsi::MutableBuffer> buffer,
60+
bool owningBytes) {
61+
auto cxxPart = std::make_unique<JArrayBuffer>(std::move(buffer), owningBytes);
62+
auto javaPart = newObjectJavaArgs(byteBuffer, owningBytes);
63+
setNativePointer(javaPart, std::move(cxxPart));
64+
return javaPart;
65+
}
66+
67+
jni::local_ref<JArrayBuffer::javaobject> JArrayBuffer::createOwning(
68+
std::shared_ptr<jsi::MutableBuffer> buffer) {
69+
// NewDirectByteBuffer rejects a null address, which is what an empty
70+
// jsi::ArrayBuffer reports, so empty buffers get an allocation of their own.
71+
if (buffer->size() == 0) {
72+
return create(jni::JByteBuffer::allocateDirect(0), std::move(buffer), true);
73+
}
74+
75+
auto byteBuffer = jni::JByteBuffer::wrapBytes(buffer->data(), buffer->size());
76+
return create(std::move(byteBuffer), std::move(buffer), true);
77+
}
78+
79+
jni::local_ref<JArrayBuffer::javaobject> JArrayBuffer::createUnowned(
80+
void* bytes,
81+
size_t size) {
82+
// NewDirectByteBuffer rejects a null address, which is what an empty
83+
// jsi::ArrayBuffer reports, so empty buffers get an allocation of their own.
84+
auto byteBuffer = size == 0
85+
? jni::JByteBuffer::allocateDirect(0)
86+
: jni::JByteBuffer::wrapBytes(static_cast<uint8_t*>(bytes), size);
87+
auto buffer = std::make_shared<JByteBufferMutableBuffer>(byteBuffer);
88+
return create(std::move(byteBuffer), std::move(buffer), false);
89+
}
90+
91+
jni::local_ref<JArrayBuffer::javaobject> JArrayBuffer::createOwned(
92+
const void* bytes,
93+
size_t size) {
94+
auto byteBuffer = jni::JByteBuffer::allocateDirect(static_cast<jint>(size));
95+
if (size > 0 && bytes != nullptr) {
96+
// @lint-ignore CLANGSECURITY facebook-security-vulnerable-memcpy
97+
std::memcpy(byteBuffer->getDirectBytes(), bytes, size);
98+
}
99+
100+
auto buffer = std::make_shared<JByteBufferMutableBuffer>(byteBuffer);
101+
return create(std::move(byteBuffer), std::move(buffer), true);
102+
}
103+
104+
std::shared_ptr<jsi::MutableBuffer> JArrayBuffer::toJSBuffer(
105+
jni::alias_ref<javaobject> arrayBuffer) {
106+
auto* self = arrayBuffer->cthis();
107+
if (self->owningBytes_) {
108+
return self->buffer_;
109+
}
110+
111+
// Borrowed bytes still belong to the inbound JS ArrayBuffer; copy them before
112+
// handing a new buffer back to JS.
113+
auto bytes = std::span<uint8_t>(self->buffer_->data(), self->buffer_->size());
114+
return std::make_shared<OwnedBytesBuffer>(
115+
std::vector<uint8_t>(bytes.begin(), bytes.end()));
116+
}
117+
118+
} // namespace facebook::react

0 commit comments

Comments
 (0)