mirror of
https://github.com/react/react-native.git
synced 2026-09-28 13:23:09 +08:00
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: https://github.com/react/react-native/pull/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
This commit is contained in:
committed by
meta-codesync[bot]
parent
4bf5575490
commit
5bb9639594
+4
-4
@@ -282,8 +282,8 @@ function translateFunctionParamToJavaType(
|
||||
imports.add('com.facebook.react.bridge.Callback');
|
||||
return wrapOptional('Callback', isRequired);
|
||||
case 'ArrayBufferTypeAnnotation':
|
||||
imports.add('java.nio.ByteBuffer');
|
||||
return wrapOptional('ByteBuffer', isRequired);
|
||||
imports.add('com.facebook.react.bridge.ArrayBuffer');
|
||||
return wrapOptional('ArrayBuffer', isRequired);
|
||||
default:
|
||||
realTypeAnnotation.type as 'MixedTypeAnnotation';
|
||||
throw new Error(createErrorMessage(realTypeAnnotation.type));
|
||||
@@ -379,8 +379,8 @@ function translateFunctionReturnTypeToJavaType(
|
||||
imports.add('com.facebook.react.bridge.WritableArray');
|
||||
return wrapOptional('WritableArray', isRequired);
|
||||
case 'ArrayBufferTypeAnnotation':
|
||||
imports.add('java.nio.ByteBuffer');
|
||||
return wrapOptional('ByteBuffer', isRequired);
|
||||
imports.add('com.facebook.react.bridge.ArrayBuffer');
|
||||
return wrapOptional('ArrayBuffer', isRequired);
|
||||
default:
|
||||
realTypeAnnotation.type as 'MixedTypeAnnotation';
|
||||
throw new Error(createErrorMessage(realTypeAnnotation.type));
|
||||
|
||||
+2
-2
@@ -311,7 +311,7 @@ function translateParamTypeToJniType(
|
||||
case 'FunctionTypeAnnotation':
|
||||
return 'Lcom/facebook/react/bridge/Callback;';
|
||||
case 'ArrayBufferTypeAnnotation':
|
||||
return 'Ljava/nio/ByteBuffer;';
|
||||
return 'Lcom/facebook/react/bridge/ArrayBuffer;';
|
||||
default:
|
||||
realTypeAnnotation.type as 'MixedTypeAnnotation';
|
||||
throw new Error(
|
||||
@@ -397,7 +397,7 @@ function translateReturnTypeToJniType(
|
||||
case 'ArrayTypeAnnotation':
|
||||
return 'Lcom/facebook/react/bridge/WritableArray;';
|
||||
case 'ArrayBufferTypeAnnotation':
|
||||
return 'Ljava/nio/ByteBuffer;';
|
||||
return 'Lcom/facebook/react/bridge/ArrayBuffer;';
|
||||
default:
|
||||
realTypeAnnotation.type as 'MixedTypeAnnotation';
|
||||
throw new Error(
|
||||
|
||||
+4
-4
@@ -58,11 +58,11 @@ Map {
|
||||
package com.facebook.fbreact.specs;
|
||||
|
||||
import com.facebook.proguard.annotations.DoNotStrip;
|
||||
import com.facebook.react.bridge.ArrayBuffer;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
|
||||
import java.nio.ByteBuffer;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@@ -80,15 +80,15 @@ public abstract class NativeSampleTurboModuleSpec extends ReactContextBaseJavaMo
|
||||
|
||||
@ReactMethod(isBlockingSynchronousMethod = true)
|
||||
@DoNotStrip
|
||||
public abstract ByteBuffer getArrayBuffer();
|
||||
public abstract ArrayBuffer getArrayBuffer();
|
||||
|
||||
@ReactMethod
|
||||
@DoNotStrip
|
||||
public abstract void voidArrayBuffer(ByteBuffer arg);
|
||||
public abstract void voidArrayBuffer(ArrayBuffer arg);
|
||||
|
||||
@ReactMethod
|
||||
@DoNotStrip
|
||||
public abstract void voidNullableArrayBuffer(@Nullable ByteBuffer arg);
|
||||
public abstract void voidNullableArrayBuffer(@Nullable ArrayBuffer arg);
|
||||
}
|
||||
",
|
||||
}
|
||||
|
||||
+3
-3
@@ -53,17 +53,17 @@ namespace facebook::react {
|
||||
|
||||
static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_getArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
|
||||
static jmethodID cachedMethodId = nullptr;
|
||||
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, ArrayBufferKind, \\"getArrayBuffer\\", \\"()Ljava/nio/ByteBuffer;\\", args, count, cachedMethodId);
|
||||
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, ArrayBufferKind, \\"getArrayBuffer\\", \\"()Lcom/facebook/react/bridge/ArrayBuffer;\\", args, count, cachedMethodId);
|
||||
}
|
||||
|
||||
static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_voidArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
|
||||
static jmethodID cachedMethodId = nullptr;
|
||||
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidArrayBuffer\\", \\"(Ljava/nio/ByteBuffer;)V\\", args, count, cachedMethodId);
|
||||
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidArrayBuffer\\", \\"(Lcom/facebook/react/bridge/ArrayBuffer;)V\\", args, count, cachedMethodId);
|
||||
}
|
||||
|
||||
static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_voidNullableArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
|
||||
static jmethodID cachedMethodId = nullptr;
|
||||
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidNullableArrayBuffer\\", \\"(Ljava/nio/ByteBuffer;)V\\", args, count, cachedMethodId);
|
||||
return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidNullableArrayBuffer\\", \\"(Lcom/facebook/react/bridge/ArrayBuffer;)V\\", args, count, cachedMethodId);
|
||||
}
|
||||
|
||||
NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms)
|
||||
|
||||
@@ -537,6 +537,28 @@ public final class com/facebook/react/bridge/Arguments {
|
||||
public static final fun toList (Lcom/facebook/react/bridge/ReadableArray;)Ljava/util/ArrayList;
|
||||
}
|
||||
|
||||
public final class com/facebook/react/bridge/ArrayBuffer : com/facebook/jni/HybridClassBase {
|
||||
public static final field Companion Lcom/facebook/react/bridge/ArrayBuffer$Companion;
|
||||
public fun <init> (I)V
|
||||
public synthetic fun <init> (Ljava/nio/ByteBuffer;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public static final fun arrayBufferWithCopiedBytes (Lcom/facebook/react/bridge/ArrayBuffer;)Lcom/facebook/react/bridge/ArrayBuffer;
|
||||
public static final fun arrayBufferWithCopiedBytes (Ljava/nio/ByteBuffer;)Lcom/facebook/react/bridge/ArrayBuffer;
|
||||
public static final fun arrayBufferWithCopiedBytes ([B)Lcom/facebook/react/bridge/ArrayBuffer;
|
||||
public static final fun arrayBufferWithLength (I)Lcom/facebook/react/bridge/ArrayBuffer;
|
||||
public static final fun arrayBufferWithOwnedBytes (Ljava/nio/ByteBuffer;)Lcom/facebook/react/bridge/ArrayBuffer;
|
||||
public final fun getBytes ()Ljava/nio/ByteBuffer;
|
||||
public final fun getSize ()I
|
||||
public final fun isOwningBytes ()Z
|
||||
}
|
||||
|
||||
public final class com/facebook/react/bridge/ArrayBuffer$Companion {
|
||||
public final fun arrayBufferWithCopiedBytes (Lcom/facebook/react/bridge/ArrayBuffer;)Lcom/facebook/react/bridge/ArrayBuffer;
|
||||
public final fun arrayBufferWithCopiedBytes (Ljava/nio/ByteBuffer;)Lcom/facebook/react/bridge/ArrayBuffer;
|
||||
public final fun arrayBufferWithCopiedBytes ([B)Lcom/facebook/react/bridge/ArrayBuffer;
|
||||
public final fun arrayBufferWithLength (I)Lcom/facebook/react/bridge/ArrayBuffer;
|
||||
public final fun arrayBufferWithOwnedBytes (Ljava/nio/ByteBuffer;)Lcom/facebook/react/bridge/ArrayBuffer;
|
||||
}
|
||||
|
||||
public final class com/facebook/react/bridge/AssertionException : java/lang/RuntimeException {
|
||||
public fun <init> (Ljava/lang/String;)V
|
||||
}
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.bridge
|
||||
|
||||
import com.facebook.jni.HybridClassBase
|
||||
import com.facebook.proguard.annotations.DoNotStrip
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* A fixed-length byte buffer for TurboModule `ArrayBuffer` arguments and return values.
|
||||
*
|
||||
* @property isOwningBytes:
|
||||
* - `true` — safe to retain and return to JS. Synchronize externally if JS may touch the same
|
||||
* memory concurrently.
|
||||
* - `false` — bytes are borrowed from a JS `ArrayBuffer` for the current synchronous call only.
|
||||
* Copy with [arrayBufferWithCopiedBytes] to keep them.
|
||||
*/
|
||||
@DoNotStrip
|
||||
public class ArrayBuffer : HybridClassBase {
|
||||
|
||||
private val buffer: ByteBuffer
|
||||
|
||||
/** Whether this buffer owns its bytes. See the class documentation. */
|
||||
public val isOwningBytes: Boolean
|
||||
|
||||
@DoNotStrip
|
||||
private constructor(buffer: ByteBuffer, isOwningBytes: Boolean) : super() {
|
||||
this.buffer = buffer
|
||||
this.isOwningBytes = isOwningBytes
|
||||
}
|
||||
|
||||
/** @param size number of zero-filled bytes to allocate */
|
||||
public constructor(size: Int) : this(allocateDirect(size), true) {
|
||||
initHybrid(buffer, isOwningBytes)
|
||||
}
|
||||
|
||||
public val bytes: ByteBuffer
|
||||
get() = buffer
|
||||
|
||||
public val size: Int
|
||||
get() = buffer.capacity()
|
||||
|
||||
private external fun initHybrid(buffer: ByteBuffer, isOwningBytes: Boolean)
|
||||
|
||||
public companion object {
|
||||
init {
|
||||
ReactNativeJniCommonSoLoader.staticInit()
|
||||
}
|
||||
|
||||
/** @param size number of zero-filled bytes to allocate. Same as `ArrayBuffer(size)`. */
|
||||
@JvmStatic
|
||||
@DoNotStrip
|
||||
public fun arrayBufferWithLength(size: Int): ArrayBuffer = ArrayBuffer(size)
|
||||
|
||||
/** @param bytes copied into a new owning buffer */
|
||||
@JvmStatic
|
||||
@DoNotStrip
|
||||
public fun arrayBufferWithCopiedBytes(bytes: ByteArray): ArrayBuffer {
|
||||
val buffer = ArrayBuffer(bytes.size)
|
||||
if (bytes.isNotEmpty()) {
|
||||
buffer.bytes.put(bytes)
|
||||
buffer.bytes.rewind()
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
/** @param source remaining bytes are copied into a new owning buffer */
|
||||
@JvmStatic
|
||||
@DoNotStrip
|
||||
public fun arrayBufferWithCopiedBytes(source: ByteBuffer): ArrayBuffer {
|
||||
val length = source.remaining()
|
||||
val buffer = ArrayBuffer(length)
|
||||
if (length > 0) {
|
||||
buffer.bytes.put(source.duplicate())
|
||||
buffer.bytes.rewind()
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source copied into a new owning buffer. Use to keep bytes from a non-owning argument
|
||||
* after the call returns.
|
||||
*/
|
||||
@JvmStatic
|
||||
@DoNotStrip
|
||||
public fun arrayBufferWithCopiedBytes(source: ArrayBuffer): ArrayBuffer {
|
||||
val length = source.size
|
||||
val buffer = ArrayBuffer(length)
|
||||
if (length > 0) {
|
||||
val src = source.bytes.duplicate()
|
||||
src.position(0)
|
||||
src.limit(length)
|
||||
buffer.bytes.put(src)
|
||||
buffer.bytes.rewind()
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
/**
|
||||
* @param buffer direct [ByteBuffer] to alias without copying. The caller must keep it valid for
|
||||
* as long as this [ArrayBuffer] lives.
|
||||
*/
|
||||
@JvmStatic
|
||||
@DoNotStrip
|
||||
public fun arrayBufferWithOwnedBytes(buffer: ByteBuffer): ArrayBuffer {
|
||||
require(buffer.isDirect) { "arrayBufferWithOwnedBytes requires a direct ByteBuffer" }
|
||||
return ArrayBuffer(buffer, true).apply { initHybrid(buffer, isOwningBytes) }
|
||||
}
|
||||
|
||||
private fun allocateDirect(size: Int): ByteBuffer {
|
||||
require(size >= 0) { "ArrayBuffer size must not be negative, got $size" }
|
||||
return ByteBuffer.allocateDirect(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ include(${REACT_ANDROID_DIR}/src/main/jni/first-party/jni-lib-merge/SoMerging-ut
|
||||
add_library(
|
||||
reactnativejni_common
|
||||
OBJECT
|
||||
JArrayBuffer.cpp
|
||||
JDynamicNative.cpp
|
||||
JReactMarker.cpp
|
||||
NativeArray.cpp
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#include "JArrayBuffer.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "JByteBufferMutableBuffer.h"
|
||||
|
||||
namespace facebook::react {
|
||||
|
||||
namespace {
|
||||
|
||||
// Holds a copy of bytes borrowed from a JS ArrayBuffer.
|
||||
class OwnedBytesBuffer final : public jsi::MutableBuffer {
|
||||
public:
|
||||
explicit OwnedBytesBuffer(std::vector<uint8_t> bytes) noexcept
|
||||
: bytes_(std::move(bytes)) {}
|
||||
|
||||
size_t size() const override {
|
||||
return bytes_.size();
|
||||
}
|
||||
|
||||
uint8_t* data() override {
|
||||
return bytes_.data();
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> bytes_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void JArrayBuffer::registerNatives() {
|
||||
registerHybrid({
|
||||
makeNativeMethod("initHybrid", JArrayBuffer::initHybrid),
|
||||
});
|
||||
}
|
||||
|
||||
void JArrayBuffer::initHybrid(
|
||||
jni::alias_ref<jhybridobject> jobj,
|
||||
jni::alias_ref<jni::JByteBuffer> buffer,
|
||||
jboolean owningBytes) {
|
||||
setCxxInstance(
|
||||
jobj,
|
||||
std::make_shared<JByteBufferMutableBuffer>(buffer),
|
||||
owningBytes != JNI_FALSE);
|
||||
}
|
||||
|
||||
jni::local_ref<JArrayBuffer::javaobject> JArrayBuffer::create(
|
||||
jni::local_ref<jni::JByteBuffer> byteBuffer,
|
||||
std::shared_ptr<jsi::MutableBuffer> buffer,
|
||||
bool owningBytes) {
|
||||
auto cxxPart = std::make_unique<JArrayBuffer>(std::move(buffer), owningBytes);
|
||||
auto javaPart = newObjectJavaArgs(byteBuffer, owningBytes);
|
||||
setNativePointer(javaPart, std::move(cxxPart));
|
||||
return javaPart;
|
||||
}
|
||||
|
||||
jni::local_ref<JArrayBuffer::javaobject> JArrayBuffer::createOwning(
|
||||
std::shared_ptr<jsi::MutableBuffer> buffer) {
|
||||
// NewDirectByteBuffer rejects a null address, which is what an empty
|
||||
// jsi::ArrayBuffer reports, so empty buffers get an allocation of their own.
|
||||
if (buffer->size() == 0) {
|
||||
return create(jni::JByteBuffer::allocateDirect(0), std::move(buffer), true);
|
||||
}
|
||||
|
||||
auto byteBuffer = jni::JByteBuffer::wrapBytes(buffer->data(), buffer->size());
|
||||
return create(std::move(byteBuffer), std::move(buffer), true);
|
||||
}
|
||||
|
||||
jni::local_ref<JArrayBuffer::javaobject> JArrayBuffer::createUnowned(
|
||||
void* bytes,
|
||||
size_t size) {
|
||||
// NewDirectByteBuffer rejects a null address, which is what an empty
|
||||
// jsi::ArrayBuffer reports, so empty buffers get an allocation of their own.
|
||||
auto byteBuffer = size == 0
|
||||
? jni::JByteBuffer::allocateDirect(0)
|
||||
: jni::JByteBuffer::wrapBytes(static_cast<uint8_t*>(bytes), size);
|
||||
auto buffer = std::make_shared<JByteBufferMutableBuffer>(byteBuffer);
|
||||
return create(std::move(byteBuffer), std::move(buffer), false);
|
||||
}
|
||||
|
||||
jni::local_ref<JArrayBuffer::javaobject> JArrayBuffer::createOwned(
|
||||
const void* bytes,
|
||||
size_t size) {
|
||||
auto byteBuffer = jni::JByteBuffer::allocateDirect(static_cast<jint>(size));
|
||||
if (size > 0 && bytes != nullptr) {
|
||||
// @lint-ignore CLANGSECURITY facebook-security-vulnerable-memcpy
|
||||
std::memcpy(byteBuffer->getDirectBytes(), bytes, size);
|
||||
}
|
||||
|
||||
auto buffer = std::make_shared<JByteBufferMutableBuffer>(byteBuffer);
|
||||
return create(std::move(byteBuffer), std::move(buffer), true);
|
||||
}
|
||||
|
||||
std::shared_ptr<jsi::MutableBuffer> JArrayBuffer::toJSBuffer(
|
||||
jni::alias_ref<javaobject> arrayBuffer) {
|
||||
auto* self = arrayBuffer->cthis();
|
||||
if (self->owningBytes_) {
|
||||
return self->buffer_;
|
||||
}
|
||||
|
||||
// Borrowed bytes still belong to the inbound JS ArrayBuffer; copy them before
|
||||
// handing a new buffer back to JS.
|
||||
auto bytes = std::span<uint8_t>(self->buffer_->data(), self->buffer_->size());
|
||||
return std::make_shared<OwnedBytesBuffer>(
|
||||
std::vector<uint8_t>(bytes.begin(), bytes.end()));
|
||||
}
|
||||
|
||||
} // namespace facebook::react
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <fbjni/ByteBuffer.h>
|
||||
#include <fbjni/fbjni.h>
|
||||
#include <jsi/jsi.h>
|
||||
|
||||
namespace facebook::react {
|
||||
|
||||
// JNI side of com.facebook.react.bridge.ArrayBuffer.
|
||||
//
|
||||
// When owningBytes_ is true, the module may retain the buffer and return it to
|
||||
// JS without copying. When false, the bytes were borrowed from a JS-heap
|
||||
// ArrayBuffer for a synchronous call only.
|
||||
class JArrayBuffer : public jni::HybridClass<JArrayBuffer> {
|
||||
public:
|
||||
static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/ArrayBuffer;";
|
||||
|
||||
static void registerNatives();
|
||||
|
||||
// JS ArrayBuffer with a native MutableBuffer (tryGetMutableBuffer). Retain
|
||||
// the owner so the bytes stay valid after the call.
|
||||
static jni::local_ref<javaobject> createOwning(std::shared_ptr<jsi::MutableBuffer> buffer);
|
||||
|
||||
// JS-heap bytes passed to a synchronous call. Zero-copy for the call only;
|
||||
// do not retain the result.
|
||||
static jni::local_ref<javaobject> createUnowned(void *bytes, size_t size);
|
||||
|
||||
// Copy JS-heap bytes into a new owned buffer. Used for async/promise calls
|
||||
// and anywhere the module needs its own copy of the data.
|
||||
static jni::local_ref<javaobject> createOwned(const void *bytes, size_t size);
|
||||
|
||||
// Convert a module return value for rt.createArrayBuffer. Owning buffers pass
|
||||
// through; borrowed ones are copied because createArrayBuffer needs its own
|
||||
// backing store.
|
||||
static std::shared_ptr<jsi::MutableBuffer> toJSBuffer(jni::alias_ref<javaobject> arrayBuffer);
|
||||
|
||||
JArrayBuffer(std::shared_ptr<jsi::MutableBuffer> buffer, bool owningBytes) noexcept
|
||||
: buffer_(std::move(buffer)), owningBytes_(owningBytes)
|
||||
{
|
||||
}
|
||||
|
||||
private:
|
||||
friend HybridBase;
|
||||
|
||||
static void
|
||||
initHybrid(jni::alias_ref<jhybridobject> jobj, jni::alias_ref<jni::JByteBuffer> buffer, jboolean owningBytes);
|
||||
|
||||
static jni::local_ref<javaobject>
|
||||
create(jni::local_ref<jni::JByteBuffer> byteBuffer, std::shared_ptr<jsi::MutableBuffer> buffer, bool owningBytes);
|
||||
|
||||
std::shared_ptr<jsi::MutableBuffer> buffer_;
|
||||
bool owningBytes_;
|
||||
};
|
||||
|
||||
} // namespace facebook::react
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
#include <fbjni/fbjni.h>
|
||||
#include "JArrayBuffer.h"
|
||||
#include "JCallback.h"
|
||||
#include "JDynamicNative.h"
|
||||
#include "JReactMarker.h"
|
||||
@@ -18,6 +19,7 @@ namespace facebook::react {
|
||||
|
||||
extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
return facebook::jni::initialize(vm, [] {
|
||||
JArrayBuffer::registerNatives();
|
||||
JCxxCallbackImpl::registerNatives();
|
||||
JDynamicNative::registerNatives();
|
||||
JReactMarker::registerNatives();
|
||||
|
||||
+26
-29
@@ -23,7 +23,7 @@
|
||||
#include <react/bridging/Bridging.h>
|
||||
#include <react/debug/react_native_assert.h>
|
||||
#include <react/featureflags/ReactNativeFeatureFlags.h>
|
||||
#include <react/jni/JByteBufferMutableBuffer.h>
|
||||
#include <react/jni/JArrayBuffer.h>
|
||||
#include <react/jni/JDynamicNative.h>
|
||||
#include <react/jni/NativeMap.h>
|
||||
#include <react/jni/ReadableNativeMap.h>
|
||||
@@ -315,6 +315,8 @@ JNIArgs convertJSIArgsToJNIArgs(
|
||||
auto& jargs = jniArgs.args;
|
||||
auto& globalRefs = jniArgs.globalRefs;
|
||||
|
||||
auto isSyncInvocation = valueKind != VoidKind && valueKind != PromiseKind;
|
||||
|
||||
auto makeGlobalIfNecessary = [&](jobject obj) {
|
||||
if (valueKind == VoidKind || valueKind == PromiseKind) {
|
||||
jobject globalObj = env->NewGlobalRef(obj);
|
||||
@@ -440,7 +442,7 @@ JNIArgs convertJSIArgsToJNIArgs(
|
||||
auto dynamicFromValue = jsi::dynamicFromValue(rt, *arg);
|
||||
auto jParams = JDynamicNative::newObjectCxxArgs(dynamicFromValue);
|
||||
jarg->l = makeGlobalIfNecessary(jParams.release());
|
||||
} else if (type == "Ljava/nio/ByteBuffer;") {
|
||||
} else if (type == "Lcom/facebook/react/bridge/ArrayBuffer;") {
|
||||
if (!(arg->isObject() && arg->getObject(rt).isArrayBuffer(rt))) {
|
||||
throw JavaTurboModuleArgumentConversionException(
|
||||
"ArrayBuffer", argIndex, methodName, arg, &rt);
|
||||
@@ -459,18 +461,24 @@ JNIArgs convertJSIArgsToJNIArgs(
|
||||
rt,
|
||||
"JavaTurboModule::convertJSIArgsToJNIArgs: ArrayBuffer exceeds maximum size.");
|
||||
}
|
||||
auto data = arrayBuffer.data(rt);
|
||||
// ArrayBuffer arguments are always copied into a Java-owned direct
|
||||
// ByteBuffer, so Java fully owns the bytes. Borrowing the JS bytes is
|
||||
// never safe — even on a synchronous call the module may retain the
|
||||
// buffer or hand it to an async method, and JS may garbage-collect the
|
||||
// source ArrayBuffer, leaving Java with a dangling view.
|
||||
auto buffer = jni::JByteBuffer::allocateDirect(static_cast<jint>(size));
|
||||
if (size > 0) {
|
||||
// @lint-ignore CLANGSECURITY facebook-security-vulnerable-memcpy
|
||||
std::memcpy(buffer->getDirectBytes(), data, size);
|
||||
}
|
||||
jarg->l = makeGlobalIfNecessary(buffer.release());
|
||||
|
||||
auto jArrayBuffer = [&]() {
|
||||
// Backed by a native buffer: alias it and retain its owner, so the
|
||||
// bytes stay valid for as long as the module holds the ArrayBuffer.
|
||||
if (auto mutableBuffer = arrayBuffer.tryGetMutableBuffer(rt)) {
|
||||
return JArrayBuffer::createOwning(std::move(mutableBuffer));
|
||||
}
|
||||
|
||||
// JS heap bytes on a synchronous call: lend them for the duration of
|
||||
// the call.
|
||||
if (isSyncInvocation) {
|
||||
return JArrayBuffer::createUnowned(arrayBuffer.data(rt), size);
|
||||
}
|
||||
|
||||
// JS heap bytes that outlive the call: copy.
|
||||
return JArrayBuffer::createOwned(arrayBuffer.data(rt), size);
|
||||
}();
|
||||
jarg->l = makeGlobalIfNecessary(jArrayBuffer.release());
|
||||
} else {
|
||||
throw JavaTurboModuleInvalidArgumentTypeException(
|
||||
type, argIndex, methodName);
|
||||
@@ -1013,22 +1021,11 @@ jsi::Value JavaTurboModule::invokeJavaMethod(
|
||||
|
||||
jsi::Value returnValue = jsi::Value::null();
|
||||
if (returnObject != nullptr) {
|
||||
auto jByteBuffer = jni::adopt_local(
|
||||
static_cast<jni::JByteBuffer::javaobject>(returnObject));
|
||||
|
||||
if (!jByteBuffer->isDirect()) {
|
||||
throw jsi::JSError(
|
||||
runtime,
|
||||
"Only direct ByteBuffers (ByteBuffer.allocateDirect) can be returned from a TurboModule.");
|
||||
}
|
||||
// Zero-copy: JByteBufferMutableBuffer takes a global reference that
|
||||
// pins the ByteBuffer's memory for the lifetime of the JS ArrayBuffer,
|
||||
// and its destructor attaches the current thread before releasing that
|
||||
// ref, so JS GC finalization on any thread is safe.
|
||||
auto nativeBuffer =
|
||||
std::make_shared<JByteBufferMutableBuffer>(jByteBuffer);
|
||||
auto jArrayBuffer = jni::adopt_local(
|
||||
static_cast<JArrayBuffer::javaobject>(returnObject));
|
||||
returnValue = {
|
||||
runtime, jsi::ArrayBuffer{runtime, std::move(nativeBuffer)}};
|
||||
runtime,
|
||||
jsi::ArrayBuffer{runtime, JArrayBuffer::toJSBuffer(jArrayBuffer)}};
|
||||
}
|
||||
|
||||
TMPL::syncMethodCallReturnConversionEnd(moduleName, methodName);
|
||||
|
||||
+13
-6
@@ -15,6 +15,7 @@ import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.facebook.proguard.annotations.DoNotStrip
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ArrayBuffer
|
||||
import com.facebook.react.bridge.Callback
|
||||
import com.facebook.react.bridge.Promise
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
@@ -27,7 +28,6 @@ import com.facebook.react.bridge.WritableNativeMap
|
||||
import com.facebook.react.module.annotations.ReactModule
|
||||
import com.facebook.react.turbomodule.core.interfaces.BindingsInstallerHolder
|
||||
import com.facebook.react.turbomodule.core.interfaces.TurboModuleWithJSIBindings
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.UUID
|
||||
|
||||
@DoNotStrip
|
||||
@@ -155,28 +155,35 @@ public class SampleTurboModule(private val context: ReactApplicationContext) :
|
||||
return map
|
||||
}
|
||||
|
||||
// Mutating the argument updates the JS ArrayBuffer in place.
|
||||
@DoNotStrip
|
||||
@Suppress("unused")
|
||||
override fun getArrayBuffer(buffer: ByteBuffer?): ByteBuffer? {
|
||||
override fun getArrayBuffer(buffer: ArrayBuffer?): ArrayBuffer? {
|
||||
if (buffer != null) {
|
||||
val bytes = buffer.bytes
|
||||
for (i in 0 until bytes.capacity()) {
|
||||
bytes.put(i, (bytes.get(i) * 2).toByte())
|
||||
}
|
||||
}
|
||||
log("getArrayBuffer", buffer, buffer)
|
||||
return buffer
|
||||
}
|
||||
|
||||
@DoNotStrip
|
||||
@Suppress("unused")
|
||||
override fun createNativeBuffer(size: Double): ByteBuffer {
|
||||
override fun createNativeBuffer(size: Double): ArrayBuffer {
|
||||
require(size.isFinite() && size >= 0.0 && size <= Int.MAX_VALUE.toDouble()) {
|
||||
"createNativeBuffer: size must be a finite value in [0, ${Int.MAX_VALUE}], got $size"
|
||||
}
|
||||
val buffer = ByteBuffer.allocateDirect(size.toInt())
|
||||
val buffer = ArrayBuffer(size.toInt())
|
||||
log("createNativeBuffer", size, buffer)
|
||||
return buffer
|
||||
}
|
||||
|
||||
@DoNotStrip
|
||||
@Suppress("unused")
|
||||
override fun processAsyncBuffer(payload: ByteBuffer?, promise: Promise) {
|
||||
promise.resolve((payload?.capacity() ?: 0).toDouble())
|
||||
override fun processAsyncBuffer(payload: ArrayBuffer?, promise: Promise) {
|
||||
promise.resolve((payload?.size ?: 0).toDouble())
|
||||
}
|
||||
|
||||
@DoNotStrip
|
||||
|
||||
@@ -2792,6 +2792,16 @@ class facebook::react::IntersectionObserverState {
|
||||
public static facebook::react::IntersectionObserverState NotIntersecting();
|
||||
}
|
||||
|
||||
class facebook::react::JArrayBuffer : public jni::HybridClass<facebook::react::JArrayBuffer> {
|
||||
public JArrayBuffer(std::shared_ptr<facebook::jsi::MutableBuffer> buffer, bool owningBytes) noexcept;
|
||||
public static constexpr auto kJavaDescriptor;
|
||||
public static jni::local_ref<javaobject> createOwned(const void* bytes, size_t size);
|
||||
public static jni::local_ref<javaobject> createOwning(std::shared_ptr<facebook::jsi::MutableBuffer> buffer);
|
||||
public static jni::local_ref<javaobject> createUnowned(void* bytes, size_t size);
|
||||
public static std::shared_ptr<facebook::jsi::MutableBuffer> toJSBuffer(jni::alias_ref<javaobject> arrayBuffer);
|
||||
public static void registerNatives();
|
||||
}
|
||||
|
||||
class facebook::react::JBindingsInstaller : public jni::HybridClass<facebook::react::JBindingsInstaller>, public facebook::react::BindingsInstaller {
|
||||
public static constexpr auto kJavaDescriptor;
|
||||
public ~JBindingsInstaller();
|
||||
|
||||
@@ -2750,6 +2750,16 @@ class facebook::react::IntersectionObserverState {
|
||||
public static facebook::react::IntersectionObserverState NotIntersecting();
|
||||
}
|
||||
|
||||
class facebook::react::JArrayBuffer : public jni::HybridClass<facebook::react::JArrayBuffer> {
|
||||
public JArrayBuffer(std::shared_ptr<facebook::jsi::MutableBuffer> buffer, bool owningBytes) noexcept;
|
||||
public static constexpr auto kJavaDescriptor;
|
||||
public static jni::local_ref<javaobject> createOwned(const void* bytes, size_t size);
|
||||
public static jni::local_ref<javaobject> createOwning(std::shared_ptr<facebook::jsi::MutableBuffer> buffer);
|
||||
public static jni::local_ref<javaobject> createUnowned(void* bytes, size_t size);
|
||||
public static std::shared_ptr<facebook::jsi::MutableBuffer> toJSBuffer(jni::alias_ref<javaobject> arrayBuffer);
|
||||
public static void registerNatives();
|
||||
}
|
||||
|
||||
class facebook::react::JBindingsInstaller : public jni::HybridClass<facebook::react::JBindingsInstaller>, public facebook::react::BindingsInstaller {
|
||||
public static constexpr auto kJavaDescriptor;
|
||||
public ~JBindingsInstaller();
|
||||
|
||||
@@ -2789,6 +2789,16 @@ class facebook::react::IntersectionObserverState {
|
||||
public static facebook::react::IntersectionObserverState NotIntersecting();
|
||||
}
|
||||
|
||||
class facebook::react::JArrayBuffer : public jni::HybridClass<facebook::react::JArrayBuffer> {
|
||||
public JArrayBuffer(std::shared_ptr<facebook::jsi::MutableBuffer> buffer, bool owningBytes) noexcept;
|
||||
public static constexpr auto kJavaDescriptor;
|
||||
public static jni::local_ref<javaobject> createOwned(const void* bytes, size_t size);
|
||||
public static jni::local_ref<javaobject> createOwning(std::shared_ptr<facebook::jsi::MutableBuffer> buffer);
|
||||
public static jni::local_ref<javaobject> createUnowned(void* bytes, size_t size);
|
||||
public static std::shared_ptr<facebook::jsi::MutableBuffer> toJSBuffer(jni::alias_ref<javaobject> arrayBuffer);
|
||||
public static void registerNatives();
|
||||
}
|
||||
|
||||
class facebook::react::JBindingsInstaller : public jni::HybridClass<facebook::react::JBindingsInstaller>, public facebook::react::BindingsInstaller {
|
||||
public static constexpr auto kJavaDescriptor;
|
||||
public ~JBindingsInstaller();
|
||||
|
||||
Reference in New Issue
Block a user