From 64ee7d931eaa3ed97acf0b28125d9287a0bf0a8c Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Thu, 20 Aug 2026 13:32:01 +0200 Subject: [PATCH 1/3] feat: support Promise in Java and ObjC TurboModules --- .../modules/GenerateModuleJavaSpec.js | 11 +- .../modules/GenerateModuleJniCpp.js | 34 ++- .../GenerateModuleObjCpp/serializeMethod.js | 6 - .../src/generators/modules/Utils.js | 42 ---- .../modules/__test_fixtures__/fixtures.js | 38 ++-- .../__tests__/GenerateModuleHObjCpp-test.js | 40 ---- .../__tests__/GenerateModuleJavaSpec-test.js | 35 ---- .../__tests__/GenerateModuleJniCpp-test.js | 39 ---- .../GenerateModuleH-test.js.snap | 45 +--- .../GenerateModuleHObjCpp-test.js.snap | 46 +---- .../GenerateModuleJavaSpec-test.js.snap | 11 +- .../GenerateModuleJniCpp-test.js.snap | 40 ++-- .../GenerateModuleJniH-test.js.snap | 59 ------ .../GenerateModuleMm-test.js.snap | 36 ++-- .../bridge/CxxArrayBufferCallbackImpl.kt | 57 +++++ .../main/jni/react/jni/JArrayBufferCallback.h | 59 ++++++ .../src/main/jni/react/jni/OnLoad-common.cpp | 2 + .../android/ReactCommon/JavaTurboModule.cpp | 195 +++++++++++++++--- .../android/ReactCommon/JavaTurboModule.h | 3 +- .../platform/android/SampleTurboModule.kt | 21 ++ .../ios/ReactCommon/RCTSampleTurboModule.mm | 19 ++ .../modules/NativeSampleTurboModule.js | 1 + .../TurboModule/SampleTurboModuleExample.js | 5 + .../api-snapshots/ReactAndroidDebugCxx.api | 7 +- .../api-snapshots/ReactAndroidNewarchCxx.api | 7 +- .../api-snapshots/ReactAndroidReleaseCxx.api | 7 +- .../api-snapshots/ReactAppleDebugCxx.api | 1 + .../api-snapshots/ReactAppleNewarchCxx.api | 1 + .../api-snapshots/ReactAppleReleaseCxx.api | 1 + 29 files changed, 443 insertions(+), 425 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CxxArrayBufferCallbackImpl.kt create mode 100644 packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBufferCallback.h diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js index 9dc0b42ce8f6..c038f166e319 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js @@ -25,11 +25,7 @@ import type {AliasResolver} from './Utils'; const {unwrapNullable} = require('../../parsers/parsers-commons'); const {wrapOptional} = require('../TypeUtils/Java'); const {parseValidUnionType, toPascalCase} = require('../Utils'); -const { - createAliasResolver, - getModules, - throwIfUnsupportedPromiseArrayBuffer, -} = require('./Utils'); +const {createAliasResolver, getModules} = require('./Utils'); type FilesOutput = Map; @@ -599,11 +595,6 @@ module.exports = { method.typeAnnotation, ); - throwIfUnsupportedPromiseArrayBuffer( - method.name, - methodTypeAnnotation.returnTypeAnnotation, - ); - // Handle return type const translatedReturnType = translateFunctionReturnTypeToJavaType( methodTypeAnnotation.returnTypeAnnotation, diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js index 7c088461107c..da890c6cf98f 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js @@ -24,11 +24,7 @@ import type {AliasResolver} from './Utils'; const {unwrapNullable} = require('../../parsers/parsers-commons'); const {parseValidUnionType} = require('../Utils'); -const { - createAliasResolver, - getModules, - throwIfUnsupportedPromiseArrayBuffer, -} = require('./Utils'); +const {createAliasResolver, getModules} = require('./Utils'); type FilesOutput = Map; @@ -47,15 +43,20 @@ const HostFunctionTemplate = ({ propertyName, jniSignature, jsReturnType, + promiseResolveSupportsArrayBuffer, }: Readonly<{ hasteModuleName: string, propertyName: string, jniSignature: string, jsReturnType: JSReturnType, + promiseResolveSupportsArrayBuffer: boolean, }>) => { + const promiseResolveSupportsArrayBufferArg = promiseResolveSupportsArrayBuffer + ? ', true' + : ''; return `static facebook::jsi::Value __hostFunction_${hasteModuleName}SpecJSI_${propertyName}(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { static jmethodID cachedMethodId = nullptr; - return static_cast(turboModule).invokeJavaMethod(rt, ${jsReturnType}, "${propertyName}", "${jniSignature}", args, count, cachedMethodId); + return static_cast(turboModule).invokeJavaMethod(rt, ${jsReturnType}, "${propertyName}", "${jniSignature}", args, count, cachedMethodId${promiseResolveSupportsArrayBufferArg}); }`; }; @@ -406,6 +407,23 @@ function translateReturnTypeToJniType( } } +function doesPromiseResolveSupportArrayBuffer( + nullableReturnTypeAnnotation: Nullable, +): boolean { + const [returnTypeAnnotation] = + unwrapNullable( + nullableReturnTypeAnnotation, + ); + if (returnTypeAnnotation.type !== 'PromiseTypeAnnotation') { + return false; + } + + let elementType = returnTypeAnnotation.elementType; + [elementType] = unwrapNullable(elementType); + + return elementType.type === 'ArrayBufferTypeAnnotation'; +} + function translateMethodTypeToJniSignature( property: NativeModulePropertyShape, resolveAlias: AliasResolver, @@ -453,8 +471,6 @@ function translateMethodForImplementation( unwrapNullable(property.typeAnnotation); const {returnTypeAnnotation} = propertyTypeAnnotation; - throwIfUnsupportedPromiseArrayBuffer(property.name, returnTypeAnnotation); - if ( property.name === 'getConstants' && returnTypeAnnotation.type === 'ObjectTypeAnnotation' && @@ -468,6 +484,8 @@ function translateMethodForImplementation( propertyName: property.name, jniSignature: translateMethodTypeToJniSignature(property, resolveAlias), jsReturnType: translateReturnTypeToKind(returnTypeAnnotation, resolveAlias), + promiseResolveSupportsArrayBuffer: + doesPromiseResolveSupportArrayBuffer(returnTypeAnnotation), }); } diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js index 397516dc5c22..c22c45b9f4cc 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js @@ -26,7 +26,6 @@ const { } = require('../../../parsers/parsers-commons'); const {wrapOptional} = require('../../TypeUtils/Objective-C'); const {capitalize, parseValidUnionType} = require('../../Utils'); -const {throwIfUnsupportedPromiseArrayBuffer} = require('../Utils'); const {getNamespacedStructName} = require('./Utils'); const invariant = require('invariant'); @@ -104,11 +103,6 @@ function serializeMethod( } }); - throwIfUnsupportedPromiseArrayBuffer( - methodName, - propertyTypeAnnotation.returnTypeAnnotation, - ); - // Unwrap returnTypeAnnotation, so we check if the return type is Promise // TODO(T76719514): Disallow nullable PromiseTypeAnnotations const [returnTypeAnnotation] = unwrapNullable( diff --git a/packages/react-native-codegen/src/generators/modules/Utils.js b/packages/react-native-codegen/src/generators/modules/Utils.js index 8cd8d37ff096..ce6b63398417 100644 --- a/packages/react-native-codegen/src/generators/modules/Utils.js +++ b/packages/react-native-codegen/src/generators/modules/Utils.js @@ -13,7 +13,6 @@ import type { NativeModuleAliasMap, NativeModuleObjectTypeAnnotation, - NativeModuleReturnTypeAnnotation, NativeModuleSchema, NativeModuleTypeAnnotation, Nullable, @@ -78,50 +77,9 @@ function isArrayRecursiveMember( ); } -// Platform-native (Java/Kotlin and ObjC) TurboModules copy ArrayBuffer -// arguments and return ArrayBuffers zero-copy from synchronous methods, but -// `Promise` is not part of their contract. -// -// On Android it cannot work: the resolve path serializes through -// folly::dynamic, which cannot carry raw bytes. On iOS the resolve path is a -// direct ObjC->jsi conversion that would in fact produce an ArrayBuffer for an -// NSMutableData, so the limitation there is not technical — the guard is -// applied to ObjC as well to keep one cross-platform contract, so a spec that -// compiles for iOS cannot fail to build for Android. -// -// Reject `Promise` at codegen time for both native platforms so -// the unsupported case surfaces as a build error rather than a runtime failure -// or a silent iOS/Android divergence. -function throwIfUnsupportedPromiseArrayBuffer( - methodName: string, - nullableReturnTypeAnnotation: Nullable, -): void { - const [returnTypeAnnotation] = - unwrapNullable( - nullableReturnTypeAnnotation, - ); - if (returnTypeAnnotation.type !== 'PromiseTypeAnnotation') { - return; - } - let elementType = returnTypeAnnotation.elementType; - if (elementType.type === 'NullableTypeAnnotation') { - elementType = elementType.typeAnnotation; - } - if (elementType.type === 'ArrayBufferTypeAnnotation') { - throw new Error( - `Unsupported return type for method "${methodName}": Promise is not ` + - 'supported for Android (Java/Kotlin) or iOS (ObjC) TurboModules. Use a C++ ' + - '(Cxx) TurboModule, return the ArrayBuffer from a synchronous method, or resolve ' + - 'the Promise with a different type. ArrayBuffer is still supported as a method ' + - 'argument and as a synchronous return value on all platforms.', - ); - } -} - module.exports = { createAliasResolver, getModules, isDirectRecursiveMember, isArrayRecursiveMember, - throwIfUnsupportedPromiseArrayBuffer, }; diff --git a/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js index 83cc98bef054..9ad6453a0d69 100644 --- a/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js @@ -2661,25 +2661,6 @@ const ARRAY_BUFFER_NATIVE_MODULE: SchemaType = { ], }, }, - ], - }, - moduleName: 'SampleTurboModule', - }, - }, -}; - -// Promise is only supported by C++ (Cxx) TurboModules (see -// throwIfUnsupportedPromiseArrayBuffer), so this fixture is excluded on both -// Android and iOS. It keeps C++ codegen coverage for the async-return case. -const ARRAY_BUFFER_PROMISE_NATIVE_MODULE: SchemaType = { - modules: { - NativeSampleTurboModule: { - type: 'NativeModule', - aliasMap: {}, - enumMap: {}, - spec: { - eventEmitters: [], - methods: [ { name: 'promiseArrayBuffer', optional: false, @@ -2694,10 +2675,26 @@ const ARRAY_BUFFER_PROMISE_NATIVE_MODULE: SchemaType = { params: [], }, }, + { + name: 'promiseNullableArrayBuffer', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + elementType: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'ArrayBufferTypeAnnotation', + }, + }, + }, + params: [], + }, + }, ], }, moduleName: 'SampleTurboModule', - excludedPlatforms: ['android', 'iOS'], }, }, }; @@ -2896,7 +2893,6 @@ const STRING_LITERALS: SchemaType = { module.exports = { array_buffer_native_module: ARRAY_BUFFER_NATIVE_MODULE, - array_buffer_promise_native_module: ARRAY_BUFFER_PROMISE_NATIVE_MODULE, complex_objects: COMPLEX_OBJECTS, two_modules_different_files: TWO_MODULES_DIFFERENT_FILES, empty_native_modules: EMPTY_NATIVE_MODULES, diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleHObjCpp-test.js b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleHObjCpp-test.js index c151a4aa1cc1..bb50ae55a1ea 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleHObjCpp-test.js +++ b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleHObjCpp-test.js @@ -10,8 +10,6 @@ 'use strict'; -import type {SchemaType} from '../../../CodegenSchema'; - const fixtures = require('../__test_fixtures__/fixtures.js'); const generator = require('../GenerateModuleObjCpp'); @@ -33,42 +31,4 @@ describe('GenerateModuleHObjCpp', () => { ).toMatchSnapshot(); }); }); - - it('throws for a method returning Promise (unsupported on iOS)', () => { - const schema: SchemaType = { - modules: { - NativeSampleTurboModule: { - type: 'NativeModule', - aliasMap: {}, - enumMap: {}, - spec: { - eventEmitters: [], - methods: [ - { - name: 'getAsyncBuffer', - optional: false, - typeAnnotation: { - type: 'FunctionTypeAnnotation', - returnTypeAnnotation: { - type: 'PromiseTypeAnnotation', - elementType: {type: 'ArrayBufferTypeAnnotation'}, - }, - params: [], - }, - }, - ], - }, - moduleName: 'SampleTurboModule', - }, - }, - }; - expect(() => - generator.generate( - 'array_buffer_promise_throws', - schema, - 'com.facebook.fbreact.specs', - false, - ), - ).toThrow(/Promise is not supported/); - }); }); diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJavaSpec-test.js b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJavaSpec-test.js index 3cbcf9747179..45d7b0e58796 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJavaSpec-test.js +++ b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJavaSpec-test.js @@ -10,8 +10,6 @@ 'use strict'; -import type {SchemaType} from '../../../CodegenSchema'; - const fixtures = require('../__test_fixtures__/fixtures.js'); const generator = require('../GenerateModuleJavaSpec.js'); @@ -31,37 +29,4 @@ describe('GenerateModuleJavaSpec', () => { ).toMatchSnapshot(); }); }); - - it('throws for a method returning Promise (unsupported on Android)', () => { - const schema: SchemaType = { - modules: { - NativeSampleTurboModule: { - type: 'NativeModule', - aliasMap: {}, - enumMap: {}, - spec: { - eventEmitters: [], - methods: [ - { - name: 'getAsyncBuffer', - optional: false, - typeAnnotation: { - type: 'FunctionTypeAnnotation', - returnTypeAnnotation: { - type: 'PromiseTypeAnnotation', - elementType: {type: 'ArrayBufferTypeAnnotation'}, - }, - params: [], - }, - }, - ], - }, - moduleName: 'SampleTurboModule', - }, - }, - }; - expect(() => - generator.generate('array_buffer_promise_throws', schema), - ).toThrow(/Promise is not supported/); - }); }); diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniCpp-test.js b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniCpp-test.js index 72e173904c6a..0e1fae7402eb 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniCpp-test.js +++ b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniCpp-test.js @@ -10,8 +10,6 @@ 'use strict'; -import type {SchemaType} from '../../../CodegenSchema'; - const fixtures = require('../__test_fixtures__/fixtures.js'); const generator = require('../GenerateModuleJniCpp.js'); @@ -31,41 +29,4 @@ describe('GenerateModuleJniCpp', () => { ).toMatchSnapshot(); }); }); - - it('throws for a method returning Promise (unsupported on Android)', () => { - const schema: SchemaType = { - modules: { - NativeSampleTurboModule: { - type: 'NativeModule', - aliasMap: {}, - enumMap: {}, - spec: { - eventEmitters: [], - methods: [ - { - name: 'getAsyncBuffer', - optional: false, - typeAnnotation: { - type: 'FunctionTypeAnnotation', - returnTypeAnnotation: { - type: 'PromiseTypeAnnotation', - elementType: {type: 'ArrayBufferTypeAnnotation'}, - }, - params: [], - }, - }, - ], - }, - moduleName: 'SampleTurboModule', - }, - }, - }; - expect(() => - generator.generate( - 'array_buffer_promise_throws', - schema, - 'com.facebook.fbreact.specs', - ), - ).toThrow(/Promise is not supported/); - }); }); diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap index f2b1d6ad49ce..2e35e312030a 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap @@ -67,6 +67,8 @@ protected: methodMap_[\\"getArrayBuffer\\"] = MethodMetadata {.argCount = 0, .invoker = __getArrayBuffer}; methodMap_[\\"voidArrayBuffer\\"] = MethodMetadata {.argCount = 1, .invoker = __voidArrayBuffer}; methodMap_[\\"voidNullableArrayBuffer\\"] = MethodMetadata {.argCount = 1, .invoker = __voidNullableArrayBuffer}; + methodMap_[\\"promiseArrayBuffer\\"] = MethodMetadata {.argCount = 0, .invoker = __promiseArrayBuffer}; + methodMap_[\\"promiseNullableArrayBuffer\\"] = MethodMetadata {.argCount = 0, .invoker = __promiseNullableArrayBuffer}; } private: @@ -92,49 +94,20 @@ private: bridging::callFromJs(rt, &T::voidNullableArrayBuffer, static_cast(&turboModule)->jsInvoker_, static_cast(&turboModule), count <= 0 || args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt).getArrayBuffer(rt)));return jsi::Value::undefined(); } -}; - -} // namespace facebook::react -", -} -`; - -exports[`GenerateModuleH can generate fixture array_buffer_promise_native_module 1`] = ` -Map { - "array_buffer_promise_native_moduleJSI.h" => "/** - * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). - * - * Do not edit this file as changes may cause incorrect behavior and will be lost - * once the code is regenerated. - * - * @generated by codegen project: GenerateModuleH.js - */ - -#pragma once - -#include -#include - -namespace facebook::react { - - -template -class JSI_EXPORT NativeSampleTurboModuleCxxSpec : public TurboModule { -public: - static constexpr std::string_view kModuleName = \\"SampleTurboModule\\"; -protected: - NativeSampleTurboModuleCxxSpec(std::shared_ptr jsInvoker) : TurboModule(std::string{NativeSampleTurboModuleCxxSpec::kModuleName}, jsInvoker) { - methodMap_[\\"promiseArrayBuffer\\"] = MethodMetadata {.argCount = 0, .invoker = __promiseArrayBuffer}; - } - -private: static jsi::Value __promiseArrayBuffer(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* /*args*/, size_t /*count*/) { static_assert( bridging::getParameterCount(&T::promiseArrayBuffer) == 1, \\"Expected promiseArrayBuffer(...) to have 1 parameters\\"); return bridging::callFromJs(rt, &T::promiseArrayBuffer, static_cast(&turboModule)->jsInvoker_, static_cast(&turboModule)); } + + static jsi::Value __promiseNullableArrayBuffer(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* /*args*/, size_t /*count*/) { + static_assert( + bridging::getParameterCount(&T::promiseNullableArrayBuffer) == 1, + \\"Expected promiseNullableArrayBuffer(...) to have 1 parameters\\"); + return bridging::callFromJs(rt, &T::promiseNullableArrayBuffer, static_cast(&turboModule)->jsInvoker_, static_cast(&turboModule)); + } }; } // namespace facebook::react diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap index 4726cdcd0d96..327bf53723f1 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap @@ -107,6 +107,10 @@ Map { - (RCTArrayBuffer *)getArrayBuffer; - (void)voidArrayBuffer:(RCTArrayBuffer *)arg; - (void)voidNullableArrayBuffer:(RCTArrayBuffer * _Nullable)arg; +- (void)promiseArrayBuffer:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject; +- (void)promiseNullableArrayBuffer:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject; @end @@ -134,48 +138,6 @@ namespace facebook::react { } `; -exports[`GenerateModuleHObjCpp can generate fixture array_buffer_promise_native_module 1`] = ` -Map { - "array_buffer_promise_native_module.h" => "/** - * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). - * - * Do not edit this file as changes may cause incorrect behavior and will be lost - * once the code is regenerated. - * - * @generated by codegen project: GenerateModuleObjCpp - * - * We create an umbrella header (and corresponding implementation) here since - * Cxx compilation in BUCK has a limitation: source-code producing genrule()s - * must have a single output. More files => more genrule()s => slower builds. - */ - -#ifndef __cplusplus -#error This file must be compiled as Obj-C++. If you are importing it, you must change your file extension to .mm. -#endif - -// Avoid multiple includes of array_buffer_promise_native_module symbols -#ifndef array_buffer_promise_native_module_H -#define array_buffer_promise_native_module_H - -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import - - - -#endif // array_buffer_promise_native_module_H -", -} -`; - exports[`GenerateModuleHObjCpp can generate fixture complex_objects 1`] = ` Map { "complex_objects.h" => "/** diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJavaSpec-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJavaSpec-test.js.snap index 14722e2179f5..0c74d5657710 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJavaSpec-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJavaSpec-test.js.snap @@ -59,6 +59,7 @@ package com.facebook.fbreact.specs; import com.facebook.proguard.annotations.DoNotStrip; import com.facebook.react.bridge.ArrayBuffer; +import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; @@ -89,13 +90,19 @@ public abstract class NativeSampleTurboModuleSpec extends ReactContextBaseJavaMo @ReactMethod @DoNotStrip public abstract void voidNullableArrayBuffer(@Nullable ArrayBuffer arg); + + @ReactMethod + @DoNotStrip + public abstract void promiseArrayBuffer(Promise promise); + + @ReactMethod + @DoNotStrip + public abstract void promiseNullableArrayBuffer(Promise promise); } ", } `; -exports[`GenerateModuleJavaSpec can generate fixture array_buffer_promise_native_module 1`] = `Map {}`; - exports[`GenerateModuleJavaSpec can generate fixture complex_objects 1`] = ` Map { "java/com/facebook/fbreact/specs/NativeSampleTurboModuleSpec.java" => " diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap index 547344fda403..ca8e30f8fbf8 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap @@ -66,11 +66,23 @@ static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_voidNu return static_cast(turboModule).invokeJavaMethod(rt, VoidKind, \\"voidNullableArrayBuffer\\", \\"(Lcom/facebook/react/bridge/ArrayBuffer;)V\\", args, count, cachedMethodId); } +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_promiseArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + static jmethodID cachedMethodId = nullptr; + return static_cast(turboModule).invokeJavaMethod(rt, PromiseKind, \\"promiseArrayBuffer\\", \\"(Lcom/facebook/react/bridge/Promise;)V\\", args, count, cachedMethodId, true); +} + +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_promiseNullableArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + static jmethodID cachedMethodId = nullptr; + return static_cast(turboModule).invokeJavaMethod(rt, PromiseKind, \\"promiseNullableArrayBuffer\\", \\"(Lcom/facebook/react/bridge/Promise;)V\\", args, count, cachedMethodId, true); +} + NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms) : JavaTurboModule(params) { methodMap_[\\"getArrayBuffer\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_getArrayBuffer}; methodMap_[\\"voidArrayBuffer\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_voidArrayBuffer}; methodMap_[\\"voidNullableArrayBuffer\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_voidNullableArrayBuffer}; + methodMap_[\\"promiseArrayBuffer\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_promiseArrayBuffer}; + methodMap_[\\"promiseNullableArrayBuffer\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_promiseNullableArrayBuffer}; } std::shared_ptr array_buffer_native_module_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams ¶ms) { @@ -85,34 +97,6 @@ std::shared_ptr array_buffer_native_module_ModuleProvider(const std } `; -exports[`GenerateModuleJniCpp can generate fixture array_buffer_promise_native_module 1`] = ` -Map { - "jni/array_buffer_promise_native_module-generated.cpp" => " -/** - * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). - * - * Do not edit this file as changes may cause incorrect behavior and will be lost - * once the code is regenerated. - * - * @generated by codegen project: GenerateModuleJniCpp.js - */ - -#include \\"array_buffer_promise_native_module.h\\" - -namespace facebook::react { - - - -std::shared_ptr array_buffer_promise_native_module_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams ¶ms) { - - return nullptr; -} - -} // namespace facebook::react -", -} -`; - exports[`GenerateModuleJniCpp can generate fixture complex_objects 1`] = ` Map { "jni/complex_objects-generated.cpp" => " diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniH-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniH-test.js.snap index fed0ac2033fd..daa41686b1c4 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniH-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniH-test.js.snap @@ -132,65 +132,6 @@ target_compile_reactnative_options(react_codegen_array_buffer_native_module PRIV } `; -exports[`GenerateModuleJniH can generate fixture array_buffer_promise_native_module 1`] = ` -Map { - "jni/array_buffer_promise_native_module.h" => " -/** - * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). - * - * Do not edit this file as changes may cause incorrect behavior and will be lost - * once the code is regenerated. - * - * @generated by codegen project: GenerateModuleJniH.js - */ - -#pragma once - -#include -#include -#include - -namespace facebook::react { - - - -JSI_EXPORT -std::shared_ptr array_buffer_promise_native_module_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams ¶ms); - -} // namespace facebook::react -", - "jni/CMakeLists.txt" => "# 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. - -cmake_minimum_required(VERSION 3.13) -set(CMAKE_VERBOSE_MAKEFILE on) - -file(GLOB react_codegen_SRCS CONFIGURE_DEPENDS *.cpp react/renderer/components/array_buffer_promise_native_module/*.cpp) - -add_library( - react_codegen_array_buffer_promise_native_module - OBJECT - \${react_codegen_SRCS} -) - -target_include_directories(react_codegen_array_buffer_promise_native_module PUBLIC . react/renderer/components/array_buffer_promise_native_module) - -target_link_libraries( - react_codegen_array_buffer_promise_native_module - fbjni - jsi - # We need to link different libraries based on whether we are building rncore or not, that's necessary - # because we want to break a circular dependency between react_codegen_rncore and reactnative - reactnative -) - -target_compile_reactnative_options(react_codegen_array_buffer_promise_native_module PRIVATE) -", -} -`; - exports[`GenerateModuleJniH can generate fixture complex_objects 1`] = ` Map { "jni/complex_objects.h" => " diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleMm-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleMm-test.js.snap index f70403ac89b6..096243c34bcd 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleMm-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleMm-test.js.snap @@ -82,6 +82,14 @@ namespace facebook::react { return static_cast(turboModule).invokeObjCMethod(rt, VoidKind, \\"voidNullableArrayBuffer\\", @selector(voidNullableArrayBuffer:), args, count); } + static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_promiseArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, PromiseKind, \\"promiseArrayBuffer\\", @selector(promiseArrayBuffer:reject:), args, count); + } + + static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_promiseNullableArrayBuffer(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, PromiseKind, \\"promiseNullableArrayBuffer\\", @selector(promiseNullableArrayBuffer:reject:), args, count); + } + NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const ObjCTurboModule::InitParams ¶ms) : ObjCTurboModule(params) { @@ -93,34 +101,18 @@ namespace facebook::react { methodMap_[\\"voidNullableArrayBuffer\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_voidNullableArrayBuffer}; + + methodMap_[\\"promiseArrayBuffer\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_promiseArrayBuffer}; + + + methodMap_[\\"promiseNullableArrayBuffer\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_promiseNullableArrayBuffer}; + } } // namespace facebook::react ", } `; -exports[`GenerateModuleMm can generate fixture array_buffer_promise_native_module 1`] = ` -Map { - "array_buffer_promise_native_module-generated.mm" => "/** - * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). - * - * Do not edit this file as changes may cause incorrect behavior and will be lost - * once the code is regenerated. - * - * @generated by codegen project: GenerateModuleObjCpp - * - * We create an umbrella header (and corresponding implementation) here since - * Cxx compilation in BUCK has a limitation: source-code producing genrule()s - * must have a single output. More files => more genrule()s => slower builds. - */ - -#import \\"array_buffer_promise_native_module.h\\" - - -", -} -`; - exports[`GenerateModuleMm can generate fixture complex_objects 1`] = ` Map { "complex_objects-generated.mm" => "/** diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CxxArrayBufferCallbackImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CxxArrayBufferCallbackImpl.kt new file mode 100644 index 000000000000..cbfe73143c1c --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/CxxArrayBufferCallbackImpl.kt @@ -0,0 +1,57 @@ +/* + * 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 + +/** + * Resolve callback for a Promise that may be fulfilled with an [ArrayBuffer] or null. Created from + * C++ only where the JavaScript spec permits `Promise` or `Promise`. + * + * Unlike [CxxCallbackImpl], this does not serialize through folly::dynamic: an owning [ArrayBuffer] + * reaches JavaScript aliasing the same bytes, and null is forwarded explicitly. + * + * The buffer must own its bytes. A non-owning one borrows from the JS `ArrayBuffer` passed to some + * earlier synchronous call, and that borrow is revoked once the call returns - long before a + * Promise resolved here reaches JavaScript. + * + * A module that resolves with anything else is misusing its spec. Rather than throwing on whichever + * thread called `Promise.resolve`, the problem is described to C++, which rejects the Promise with + * it. + */ +@DoNotStrip +internal class CxxArrayBufferCallbackImpl @DoNotStrip private constructor() : + HybridClassBase(), Callback { + + override fun invoke(vararg args: Any?) { + if (args.size > 1) { + nativeInvoke(null, "expected at most one argument, got ${args.size}") + return + } + when (val arg = args.firstOrNull()) { + null -> nativeInvoke(null, null) + is ArrayBuffer -> + if (arg.isOwningBytes) { + nativeInvoke(arg, null) + } else { + nativeInvoke( + null, + "expected an ArrayBuffer that owns its bytes; the bytes of a non-owning one are " + + "no longer valid by the time the Promise resolves. Copy them with " + + "ArrayBuffer.arrayBufferWithCopiedBytes().") + } + else -> nativeInvoke(null, "expected an ArrayBuffer or null, got ${arg.javaClass.name}") + } + } + + /** + * At most one of [arrayBuffer] and [error] is non-null. Both null resolves with JavaScript null. + */ + private external fun nativeInvoke(arrayBuffer: ArrayBuffer?, error: String?) +} diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBufferCallback.h b/packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBufferCallback.h new file mode 100644 index 000000000000..f1edaf4b464a --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/JArrayBufferCallback.h @@ -0,0 +1,59 @@ +/* + * 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 + +#include + +#include "JArrayBuffer.h" +#include "JCallback.h" + +namespace facebook::react { + +// Resolve callback for a Promise that may be fulfilled with an ArrayBuffer or +// null. +// +// Created only where the JavaScript spec permits Promise or +// Promise. Does not use folly::dynamic; the bytes of an owning +// com.facebook.react.bridge.ArrayBuffer reach JavaScript without a copy, and +// null is forwarded explicitly. +// +// The Java side validates what the module resolved with and reports a +// description of the problem through `error` instead of throwing, so that +// misuse rejects the Promise rather than escaping on the resolving thread. +class JCxxArrayBufferCallbackImpl : public jni::HybridClass { + public: + constexpr static auto kJavaDescriptor = "Lcom/facebook/react/bridge/CxxArrayBufferCallbackImpl;"; + + static void registerNatives() + { + registerHybrid({ + makeNativeMethod("nativeInvoke", JCxxArrayBufferCallbackImpl::invoke), + }); + } + + private: + friend HybridBase; + + // At most one of `arrayBuffer` and `error` is non-null. Both null resolves + // the Promise with JavaScript null. + using Callback = std::function< + void(jni::alias_ref arrayBuffer, jni::alias_ref error)>; + + explicit JCxxArrayBufferCallbackImpl(Callback callback) : callback_(std::move(callback)) {} + + void invoke(jni::alias_ref arrayBuffer, jni::alias_ref error) + { + callback_(arrayBuffer, error); + } + + Callback callback_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad-common.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad-common.cpp index 8c6787bdde65..cb8729cd9d11 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad-common.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad-common.cpp @@ -7,6 +7,7 @@ #include #include "JArrayBuffer.h" +#include "JArrayBufferCallback.h" #include "JCallback.h" #include "JDynamicNative.h" #include "JReactMarker.h" @@ -20,6 +21,7 @@ namespace facebook::react { extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { return facebook::jni::initialize(vm, [] { JArrayBuffer::registerNatives(); + JCxxArrayBufferCallbackImpl::registerNatives(); JCxxCallbackImpl::registerNatives(); JDynamicNative::registerNatives(); JReactMarker::registerNatives(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp b/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp index 0db4927a6338..a8d8e8a44c06 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -24,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -134,20 +136,37 @@ jsi::Value createRejectionError(jsi::Runtime& rt, const folly::dynamic& args) { return jsError; } -auto createJavaCallback( - jsi::Runtime& rt, - jsi::Function&& function, - std::shared_ptr jsInvoker) { - std::optional> callback( - {rt, std::move(function), std::move(jsInvoker)}); - return JCxxCallbackImpl::newObjectCxxArgs( - [callback = std::move(callback)](folly::dynamic args) mutable { - if (!callback) { - LOG(FATAL) << "Callback arg cannot be called more than once"; - return; - } - callback->call([args = std::move(args)]( - jsi::Runtime& rt, jsi::Function& jsFunction) { +class OnceCallback { + std::optional> callback_; + + public: + OnceCallback( + jsi::Runtime& rt, + jsi::Function function, + std::shared_ptr jsInvoker) + : callback_( + AsyncCallback<>(rt, std::move(function), std::move(jsInvoker))) {} + + OnceCallback(const OnceCallback&) = delete; + OnceCallback& operator=(const OnceCallback&) = delete; + OnceCallback(OnceCallback&&) = default; + OnceCallback& operator=(OnceCallback&&) = default; + + template + void call(const char* what, F&& invoke) { + if (!callback_) { + LOG(FATAL) << what << " cannot be called more than once"; + return; + } + callback_->call(std::forward(invoke)); + callback_ = std::nullopt; + } + + void callWithArgs(const char* what, folly::dynamic&& args) noexcept { + call( + what, + [args = std::move(args)]( + jsi::Runtime& rt, jsi::Function& jsFunction) mutable { std::vector jsArgs; jsArgs.reserve(args.size()); for (const auto& val : args) { @@ -155,27 +174,135 @@ auto createJavaCallback( } jsFunction.call(rt, (const jsi::Value*)jsArgs.data(), jsArgs.size()); }); - callback = std::nullopt; - }); + } +}; + +template +jni::local_ref makeJavaOnceCallback( + jsi::Runtime& rt, + jsi::Function function, + std::shared_ptr jsInvoker, + Handler handler) { + auto once = std::make_shared( + rt, std::move(function), std::move(jsInvoker)); + return jni::static_ref_cast( + JavaCallbackImpl::newObjectCxxArgs( + [once = std::move(once), + handler = std::move(handler)](auto&&... args) mutable { + handler(*once, std::forward(args)...); + })); } -auto createJavaRejectCallback( +jni::local_ref createJavaCallback( jsi::Runtime& rt, jsi::Function&& function, std::shared_ptr jsInvoker) { - std::optional> callback( - {rt, std::move(function), std::move(jsInvoker)}); - return JCxxCallbackImpl::newObjectCxxArgs( - [callback = std::move(callback)](folly::dynamic args) mutable { - if (!callback) { - LOG(FATAL) << "Callback arg cannot be called more than once"; + return makeJavaOnceCallback( + rt, + std::move(function), + std::move(jsInvoker), + [](OnceCallback& once, folly::dynamic args) { + once.callWithArgs("Callback arg", std::move(args)); + }); +} + +jni::local_ref createJavaArrayBufferCallback( + jsi::Runtime& rt, + jsi::Function&& resolveFunction, + jsi::Function&& rejectFunction, + std::shared_ptr jsInvoker) { + auto rejectMisuse = + std::make_shared(rt, std::move(rejectFunction), jsInvoker); + return makeJavaOnceCallback( + rt, + std::move(resolveFunction), + std::move(jsInvoker), + [rejectMisuse = std::move(rejectMisuse)]( + OnceCallback& once, + jni::alias_ref arrayBuffer, + jni::alias_ref error) { + auto reject = [&rejectMisuse](std::string message) { + rejectMisuse->call( + "Promise reject", + [message = "Invalid Promise resolution: " + + std::move(message)]( + jsi::Runtime& rt, jsi::Function& jsFunction) { + jsFunction.call(rt, createJSRuntimeError(rt, message)); + }); + }; + + if (error) { + reject(error->toStdString()); return; } - callback->call([args = std::move(args)]( - jsi::Runtime& rt, jsi::Function& jsFunction) { - jsFunction.call(rt, createRejectionError(rt, args)); - }); - callback = std::nullopt; + + // Kotlin has already rejected anything but null or an owning + // ArrayBuffer, and invalidate() never revokes an owning buffer, so the + // peer's bytes are still there and can go to JS unchanged. hasBytes() + // is re-checked anyway: mutableBuffer() throws without it, and letting + // a std::runtime_error escape a JNI frame is a poor way to find out + // that the two sides ever disagreed about ownership. + std::shared_ptr buffer; + if (arrayBuffer) { + auto* peer = arrayBuffer->cthis(); + if (peer == nullptr) { + reject("ArrayBuffer has no native peer."); + return; + } + if (!peer->hasBytes()) { + reject( + "the bytes of this ArrayBuffer are no longer valid. Copy them " + "with ArrayBuffer.arrayBufferWithCopiedBytes() to resolve with " + "them later."); + return; + } + buffer = peer->mutableBuffer(); + } + + once.call( + "Promise resolve", + [buffer = std::move(buffer)]( + jsi::Runtime& rt, jsi::Function& jsFunction) { + if (!buffer) { + jsFunction.call(rt, jsi::Value::null()); + return; + } + jsFunction.call(rt, jsi::Value(jsi::ArrayBuffer(rt, buffer))); + }); + }); +} + +jni::local_ref createJavaResolveCallback( + jsi::Runtime& rt, + jsi::Function&& resolveFunction, + jsi::Function&& rejectFunction, + std::shared_ptr jsInvoker, + bool promiseResolveSupportsArrayBuffer) { + return promiseResolveSupportsArrayBuffer + ? createJavaArrayBufferCallback( + rt, + std::move(resolveFunction), + std::move(rejectFunction), + std::move(jsInvoker)) + : createJavaCallback( + rt, std::move(resolveFunction), std::move(jsInvoker)); +} + +jni::local_ref createJavaRejectCallback( + jsi::Runtime& rt, + jsi::Function&& function, + std::shared_ptr jsInvoker) { + return makeJavaOnceCallback( + rt, + std::move(function), + std::move(jsInvoker), + [](OnceCallback& once, folly::dynamic args) { + once.call( + "Promise reject", + [args = std::move(args)]( + jsi::Runtime& rt, jsi::Function& jsFunction) { + jsFunction.call(rt, createRejectionError(rt, args)); + }); }); } @@ -595,7 +722,11 @@ jsi::Value JavaTurboModule::invokeJavaMethod( const std::string& methodSignature, const jsi::Value* args, size_t argCount, - jmethodID& methodID) { + jmethodID& methodID, + bool promiseResolveSupportsArrayBuffer) { + react_native_assert( + !promiseResolveSupportsArrayBuffer || valueKind == PromiseKind); + const char* methodName = methodNameStr.c_str(); const char* moduleName = name_.c_str(); @@ -959,10 +1090,12 @@ jsi::Value JavaTurboModule::invokeJavaMethod( args[1].getObject(runtime).getFunction(runtime), jsInvoker_); - auto resolve = createJavaCallback( + auto resolve = createJavaResolveCallback( runtime, args[0].getObject(runtime).getFunction(runtime), - jsInvoker_); + args[1].getObject(runtime).getFunction(runtime), + jsInvoker_, + promiseResolveSupportsArrayBuffer); auto reject = createJavaRejectCallback( runtime, args[1].getObject(runtime).getFunction(runtime), diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.h b/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.h index c42f1c7dbac0..e2c8dd777917 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.h +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.h @@ -45,7 +45,8 @@ class JSI_EXPORT JavaTurboModule : public TurboModule { const std::string &methodSignature, const jsi::Value *args, size_t argCount, - jmethodID &cachedMethodID); + jmethodID &cachedMethodID, + bool promiseResolveSupportsArrayBuffer = false); protected: void configureEventEmitterCallback(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.kt b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.kt index 4c97e1140156..1f35cc70b760 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.kt +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.kt @@ -186,6 +186,27 @@ public class SampleTurboModule(private val context: ReactApplicationContext) : promise.resolve((payload?.size ?: 0).toDouble()) } + // Resolving with an owning ArrayBuffer hands JS the same bytes without + // copying them; the ArrayBuffer keeps them alive for as long as JS can reach + // them. + @DoNotStrip + @Suppress("unused") + override fun getAsyncBuffer(size: Double, promise: Promise) { + if (!size.isFinite() || size < 0.0 || size > Int.MAX_VALUE.toDouble()) { + promise.reject( + "invalid_size", + "getAsyncBuffer: size must be a finite value in [0, ${Int.MAX_VALUE}], got $size") + return + } + val buffer = ArrayBuffer(size.toInt()) + val bytes = buffer.bytes + for (i in 0 until bytes.capacity()) { + bytes.put(i, (i + 1).toByte()) + } + log("getAsyncBuffer", size, buffer) + promise.resolve(buffer) + } + @DoNotStrip @Suppress("unused") override fun getValueWithCallback(callback: Callback?) { diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm index 6ec73228f76e..a167efca0d77 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm @@ -16,6 +16,7 @@ #import #import +#include #include using namespace facebook::react; @@ -176,6 +177,24 @@ - (void)processAsyncBuffer:(RCTArrayBuffer *)payload resolve(@(payload.length)); } +// Resolving a Promise with an owning RCTArrayBuffer hands JS the same bytes +// without copying them; the buffer keeps them alive for as long as JS holds the +// ArrayBuffer. +- (void)getAsyncBuffer:(double)size resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject +{ + if (!std::isfinite(size) || size < 0 || size > (double)NSUIntegerMax) { + reject(@"invalid_size", [NSString stringWithFormat:@"getAsyncBuffer: invalid size %g", size], nil); + return; + } + + RCTArrayBuffer *buffer = [RCTArrayBuffer arrayBufferWithLength:(NSUInteger)size]; + std::span byteSpan(static_cast(buffer.mutableBytes), static_cast(buffer.length)); + for (size_t i = 0; i < byteSpan.size(); i++) { + byteSpan[i] = static_cast(i + 1); + } + resolve(buffer); +} + - (void)getValueWithCallback:(RCTResponseSenderBlock)callback { if (callback == nullptr) { diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeSampleTurboModule.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeSampleTurboModule.js index c458c91a2204..3f4b77c4801c 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeSampleTurboModule.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeSampleTurboModule.js @@ -54,6 +54,7 @@ export interface Spec extends TurboModule { readonly getArrayBuffer: (buffer: ArrayBuffer) => ArrayBuffer; readonly createNativeBuffer: (size: number) => ArrayBuffer; readonly processAsyncBuffer: (payload: ArrayBuffer) => Promise; + readonly getAsyncBuffer: (size: number) => Promise; readonly getValueWithCallback: (callback: (value: string) => void) => void; readonly getValueWithPromise: (error: boolean) => Promise; readonly voidFuncThrows?: () => void; diff --git a/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js b/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js index ddfcc31fb04f..f11aa856dc3b 100644 --- a/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js +++ b/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js @@ -53,6 +53,7 @@ type Examples = | 'getArrayBuffer' | 'createNativeBuffer' | 'processAsyncBuffer' + | 'getAsyncBuffer' | 'promise' | 'rejectPromise' | 'voidFunc' @@ -109,6 +110,10 @@ class SampleTurboModuleExample extends React.Component<{}, State> { NativeSampleTurboModule.processAsyncBuffer( new Uint8Array([1, 2, 3]).buffer, ).then(length => this._setResult('processAsyncBuffer', length)), + getAsyncBuffer: () => + NativeSampleTurboModule.getAsyncBuffer(4).then(buffer => + this._setResult('getAsyncBuffer', Array.from(new Uint8Array(buffer))), + ), getBool: () => NativeSampleTurboModule.getBool(true), getConstants: () => NativeSampleTurboModule.getConstants(), getEnum: () => diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 2e6dc7e0b4e2..8aa400d9f494 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -2828,6 +2828,11 @@ class facebook::react::JByteBufferMutableBuffer : public facebook::jsi::MutableB public ~JByteBufferMutableBuffer() override; } +class facebook::react::JCxxArrayBufferCallbackImpl : public jni::HybridClass { + public static constexpr auto kJavaDescriptor; + public static void registerNatives(); +} + class facebook::react::JCxxCallbackImpl : public jni::HybridClass { public static constexpr auto kJavaDescriptor; public static void registerNatives(); @@ -3103,7 +3108,7 @@ class facebook::react::JavaTurboModule : public facebook::react::TurboModule { protected void configureEventEmitterCallback(); protected void setEventEmitterCallback(jni::alias_ref); public JavaTurboModule(const facebook::react::JavaTurboModule::InitParams& params); - public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const std::string& methodName, const std::string& methodSignature, const facebook::jsi::Value* args, size_t argCount, jmethodID& cachedMethodID); + public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const std::string& methodName, const std::string& methodSignature, const facebook::jsi::Value* args, size_t argCount, jmethodID& cachedMethodID, bool promiseResolveSupportsArrayBuffer = false); public virtual ~JavaTurboModule(); } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 7ec351405ee3..f6c80cc91a20 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -2786,6 +2786,11 @@ class facebook::react::JByteBufferMutableBuffer : public facebook::jsi::MutableB public ~JByteBufferMutableBuffer() override; } +class facebook::react::JCxxArrayBufferCallbackImpl : public jni::HybridClass { + public static constexpr auto kJavaDescriptor; + public static void registerNatives(); +} + class facebook::react::JCxxCallbackImpl : public jni::HybridClass { public static constexpr auto kJavaDescriptor; public static void registerNatives(); @@ -3022,7 +3027,7 @@ class facebook::react::JavaTurboModule : public facebook::react::TurboModule { protected void configureEventEmitterCallback(); protected void setEventEmitterCallback(jni::alias_ref); public JavaTurboModule(const facebook::react::JavaTurboModule::InitParams& params); - public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const std::string& methodName, const std::string& methodSignature, const facebook::jsi::Value* args, size_t argCount, jmethodID& cachedMethodID); + public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const std::string& methodName, const std::string& methodSignature, const facebook::jsi::Value* args, size_t argCount, jmethodID& cachedMethodID, bool promiseResolveSupportsArrayBuffer = false); public virtual ~JavaTurboModule(); } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 6843410835c3..61b12b3fe21f 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -2825,6 +2825,11 @@ class facebook::react::JByteBufferMutableBuffer : public facebook::jsi::MutableB public ~JByteBufferMutableBuffer() override; } +class facebook::react::JCxxArrayBufferCallbackImpl : public jni::HybridClass { + public static constexpr auto kJavaDescriptor; + public static void registerNatives(); +} + class facebook::react::JCxxCallbackImpl : public jni::HybridClass { public static constexpr auto kJavaDescriptor; public static void registerNatives(); @@ -3100,7 +3105,7 @@ class facebook::react::JavaTurboModule : public facebook::react::TurboModule { protected void configureEventEmitterCallback(); protected void setEventEmitterCallback(jni::alias_ref); public JavaTurboModule(const facebook::react::JavaTurboModule::InitParams& params); - public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const std::string& methodName, const std::string& methodSignature, const facebook::jsi::Value* args, size_t argCount, jmethodID& cachedMethodID); + public facebook::jsi::Value invokeJavaMethod(facebook::jsi::Runtime& runtime, facebook::react::TurboModuleMethodValueKind valueKind, const std::string& methodName, const std::string& methodSignature, const facebook::jsi::Value* args, size_t argCount, jmethodID& cachedMethodID, bool promiseResolveSupportsArrayBuffer = false); public virtual ~JavaTurboModule(); } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index d6dc3f80a6ad..35a791e71a7a 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -2521,6 +2521,7 @@ protocol NativeSampleTurboModuleSpec : public NSObjectRCTBridgeModule, public RC public virtual RCTArrayBuffer* getArrayBuffer:(RCTArrayBuffer* buffer); public virtual facebook::react::ModuleConstants constantsToExport(); public virtual facebook::react::ModuleConstants getConstants(); + public virtual void getAsyncBuffer:resolve:reject:(double size, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void getImageUrl:reject:(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void getValueWithCallback:(RCTResponseSenderBlock callback); public virtual void getValueWithPromise:resolve:reject:(BOOL error, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index da7a542fd69f..27b7de33cafb 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -2514,6 +2514,7 @@ protocol NativeSampleTurboModuleSpec : public NSObjectRCTBridgeModule, public RC public virtual RCTArrayBuffer* getArrayBuffer:(RCTArrayBuffer* buffer); public virtual facebook::react::ModuleConstants constantsToExport(); public virtual facebook::react::ModuleConstants getConstants(); + public virtual void getAsyncBuffer:resolve:reject:(double size, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void getImageUrl:reject:(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void getValueWithCallback:(RCTResponseSenderBlock callback); public virtual void getValueWithPromise:resolve:reject:(BOOL error, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index a92742d8c14e..c6ad51282fe2 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -2521,6 +2521,7 @@ protocol NativeSampleTurboModuleSpec : public NSObjectRCTBridgeModule, public RC public virtual RCTArrayBuffer* getArrayBuffer:(RCTArrayBuffer* buffer); public virtual facebook::react::ModuleConstants constantsToExport(); public virtual facebook::react::ModuleConstants getConstants(); + public virtual void getAsyncBuffer:resolve:reject:(double size, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void getImageUrl:reject:(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); public virtual void getValueWithCallback:(RCTResponseSenderBlock callback); public virtual void getValueWithPromise:resolve:reject:(BOOL error, RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject); From 127c367ddcbc517e549612fc2bf743acd08e1ff1 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Thu, 20 Aug 2026 15:20:22 +0200 Subject: [PATCH 2/3] feat: support Array as a TurboModule method parameter --- .../modules/GenerateModuleJavaSpec.js | 10 +- .../modules/GenerateModuleJniCpp.js | 10 +- .../GenerateModuleObjCpp/serializeMethod.js | 6 + .../src/generators/modules/Utils.js | 22 ++ .../modules/__test_fixtures__/fixtures.js | 22 ++ .../__tests__/GenerateModuleHObjCpp-test.js | 12 + .../__tests__/GenerateModuleJavaSpec-test.js | 56 +++ .../__tests__/GenerateModuleJniCpp-test.js | 11 + .../GenerateModuleH-test.js.snap | 9 + .../GenerateModuleHObjCpp-test.js.snap | 1 + .../GenerateModuleJavaSpec-test.js.snap | 4 + .../GenerateModuleJniCpp-test.js.snap | 6 + .../GenerateModuleMm-test.js.snap | 7 + .../src/parsers/__tests__/error-utils-test.js | 348 ++++++++++++++++++ .../src/parsers/error-utils.js | 186 +++++++++- .../src/parsers/errors.js | 27 ++ .../modules/__test_fixtures__/failures.js | 301 +++++++++++++++ .../modules/__test_fixtures__/fixtures.js | 1 + .../module-parser-snapshot-test.js.snap | 46 +++ .../src/parsers/parsers-commons.js | 49 ++- .../modules/__test_fixtures__/failures.js | 267 ++++++++++++++ .../modules/__test_fixtures__/fixtures.js | 1 + ...script-module-parser-snapshot-test.js.snap | 46 +++ .../android/ReactCommon/JavaTurboModule.cpp | 143 ++++--- .../ios/ReactCommon/RCTTurboModule.mm | 15 +- .../platform/android/SampleTurboModule.kt | 17 + .../ios/ReactCommon/RCTSampleTurboModule.mm | 17 + .../modules/NativeSampleTurboModule.js | 1 + .../RCTTurboModuleArrayBufferTests.mm | 251 +++++++++++++ .../TurboModule/SampleTurboModuleExample.js | 9 + .../api-snapshots/ReactAppleDebugCxx.api | 1 + .../api-snapshots/ReactAppleNewarchCxx.api | 1 + .../api-snapshots/ReactAppleReleaseCxx.api | 1 + 33 files changed, 1835 insertions(+), 69 deletions(-) diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js index c038f166e319..a025086811eb 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js @@ -25,7 +25,11 @@ import type {AliasResolver} from './Utils'; const {unwrapNullable} = require('../../parsers/parsers-commons'); const {wrapOptional} = require('../TypeUtils/Java'); const {parseValidUnionType, toPascalCase} = require('../Utils'); -const {createAliasResolver, getModules} = require('./Utils'); +const { + createAliasResolver, + getModules, + isArrayBufferElementType, +} = require('./Utils'); type FilesOutput = Map; @@ -272,6 +276,10 @@ function translateFunctionParamToJavaType( imports.add('com.facebook.react.bridge.ReadableMap'); return wrapOptional('ReadableMap', isRequired); case 'ArrayTypeAnnotation': + if (isArrayBufferElementType(realTypeAnnotation.elementType)) { + imports.add('com.facebook.react.bridge.ArrayBuffer'); + return wrapOptional('ArrayBuffer[]', isRequired); + } imports.add('com.facebook.react.bridge.ReadableArray'); return wrapOptional('ReadableArray', isRequired); case 'FunctionTypeAnnotation': diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js index da890c6cf98f..f6eb7a12bf2d 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js @@ -24,7 +24,11 @@ import type {AliasResolver} from './Utils'; const {unwrapNullable} = require('../../parsers/parsers-commons'); const {parseValidUnionType} = require('../Utils'); -const {createAliasResolver, getModules} = require('./Utils'); +const { + createAliasResolver, + getModules, + isArrayBufferElementType, +} = require('./Utils'); type FilesOutput = Map; @@ -308,7 +312,9 @@ function translateParamTypeToJniType( case 'ObjectTypeAnnotation': return 'Lcom/facebook/react/bridge/ReadableMap;'; case 'ArrayTypeAnnotation': - return 'Lcom/facebook/react/bridge/ReadableArray;'; + return isArrayBufferElementType(realTypeAnnotation.elementType) + ? '[Lcom/facebook/react/bridge/ArrayBuffer;' + : 'Lcom/facebook/react/bridge/ReadableArray;'; case 'FunctionTypeAnnotation': return 'Lcom/facebook/react/bridge/Callback;'; case 'ArrayBufferTypeAnnotation': diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js index c22c45b9f4cc..635965ffc215 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js @@ -26,6 +26,7 @@ const { } = require('../../../parsers/parsers-commons'); const {wrapOptional} = require('../../TypeUtils/Objective-C'); const {capitalize, parseValidUnionType} = require('../../Utils'); +const {isArrayBufferElementType} = require('../Utils'); const {getNamespacedStructName} = require('./Utils'); const invariant = require('invariant'); @@ -218,6 +219,11 @@ function getParamObjCType( * type Animal = {}; * Array => NSArray, etc. */ + if (isArrayBufferElementType(typeAnnotation.elementType)) { + return notStruct( + wrapOptional('NSArray *', !nullable), + ); + } return notStruct(wrapOptional('NSArray *', !nullable)); } case 'ArrayBufferTypeAnnotation': { diff --git a/packages/react-native-codegen/src/generators/modules/Utils.js b/packages/react-native-codegen/src/generators/modules/Utils.js index ce6b63398417..21aca9c6fd95 100644 --- a/packages/react-native-codegen/src/generators/modules/Utils.js +++ b/packages/react-native-codegen/src/generators/modules/Utils.js @@ -12,11 +12,13 @@ import type { NativeModuleAliasMap, + NativeModuleBaseTypeAnnotation, NativeModuleObjectTypeAnnotation, NativeModuleSchema, NativeModuleTypeAnnotation, Nullable, SchemaType, + UnsafeAnyTypeAnnotation, } from '../../CodegenSchema'; const {unwrapNullable} = require('../../parsers/parsers-commons'); @@ -77,9 +79,29 @@ function isArrayRecursiveMember( ); } +/** + * Whether an array's element type is `ArrayBuffer`. Rejects nullable elements + * (`Array`) so generators fall back to an untyped array. Handles + * the `AnyTypeAnnotation` that `emitArrayType` substitutes when the element + * type failed to parse. + */ +function isArrayBufferElementType( + elementType: + Nullable | UnsafeAnyTypeAnnotation, +): boolean { + if (elementType == null || elementType.type === 'AnyTypeAnnotation') { + return false; + } + if (elementType.type === 'NullableTypeAnnotation') { + return false; + } + return elementType.type === 'ArrayBufferTypeAnnotation'; +} + module.exports = { createAliasResolver, getModules, isDirectRecursiveMember, isArrayRecursiveMember, + isArrayBufferElementType, }; diff --git a/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js index 9ad6453a0d69..9809f8d98110 100644 --- a/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js @@ -2692,6 +2692,28 @@ const ARRAY_BUFFER_NATIVE_MODULE: SchemaType = { params: [], }, }, + { + name: 'arrayBufferArray', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'NumberTypeAnnotation', + }, + params: [ + { + name: 'values', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ArrayBufferTypeAnnotation', + }, + }, + }, + ], + }, + }, ], }, moduleName: 'SampleTurboModule', diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleHObjCpp-test.js b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleHObjCpp-test.js index bb50ae55a1ea..f295419b1a85 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleHObjCpp-test.js +++ b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleHObjCpp-test.js @@ -31,4 +31,16 @@ describe('GenerateModuleHObjCpp', () => { ).toMatchSnapshot(); }); }); + + it('generates NSArray for a top-level Array parameter', () => { + const output = generator.generate( + 'array_buffer_native_module', + fixtures.array_buffer_native_module, + 'com.facebook.fbreact.specs', + false, + ); + expect([...output.values()].join('\n')).toContain( + 'arrayBufferArray:(NSArray *)values', + ); + }); }); diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJavaSpec-test.js b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJavaSpec-test.js index 45d7b0e58796..79bc7ce298f6 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJavaSpec-test.js +++ b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJavaSpec-test.js @@ -29,4 +29,60 @@ describe('GenerateModuleJavaSpec', () => { ).toMatchSnapshot(); }); }); + + it('generates ArrayBuffer[] for a top-level Array parameter', () => { + const output = generator.generate( + 'array_buffer_native_module', + fixtures.array_buffer_native_module, + 'com.facebook.fbreact.specs', + ); + expect([...output.values()].join('\n')).toContain('ArrayBuffer[] values'); + }); + + it('does not generate ArrayBuffer[] when array element type is nullable', () => { + const schema: $FlowFixMe = { + modules: { + NativeSampleTurboModule: { + type: 'NativeModule', + aliasMap: {}, + enumMap: {}, + moduleName: 'SampleTurboModule', + spec: { + eventEmitters: [], + methods: [ + { + name: 'nullableElements', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: {type: 'NumberTypeAnnotation'}, + params: [ + { + name: 'values', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'NullableTypeAnnotation', + typeAnnotation: {type: 'ArrayBufferTypeAnnotation'}, + }, + }, + }, + ], + }, + }, + ], + }, + }, + }, + }; + const output = generator.generate( + 'nullable_array_buffer_elements', + schema, + 'com.facebook.fbreact.specs', + ); + const contents = [...output.values()].join('\n'); + expect(contents).toContain('nullableElements(ReadableArray values)'); + expect(contents).not.toMatch(/nullableElements\(ArrayBuffer\[\] values\)/); + }); }); diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniCpp-test.js b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniCpp-test.js index 0e1fae7402eb..bc6f3332723f 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniCpp-test.js +++ b/packages/react-native-codegen/src/generators/modules/__tests__/GenerateModuleJniCpp-test.js @@ -29,4 +29,15 @@ describe('GenerateModuleJniCpp', () => { ).toMatchSnapshot(); }); }); + + it('generates a JNI ArrayBuffer array signature for a top-level Array parameter', () => { + const output = generator.generate( + 'array_buffer_native_module', + fixtures.array_buffer_native_module, + 'com.facebook.fbreact.specs', + ); + expect([...output.values()].join('\n')).toContain( + '[Lcom/facebook/react/bridge/ArrayBuffer;', + ); + }); }); diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap index 2e35e312030a..02be8c7ce549 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap @@ -69,6 +69,7 @@ protected: methodMap_[\\"voidNullableArrayBuffer\\"] = MethodMetadata {.argCount = 1, .invoker = __voidNullableArrayBuffer}; methodMap_[\\"promiseArrayBuffer\\"] = MethodMetadata {.argCount = 0, .invoker = __promiseArrayBuffer}; methodMap_[\\"promiseNullableArrayBuffer\\"] = MethodMetadata {.argCount = 0, .invoker = __promiseNullableArrayBuffer}; + methodMap_[\\"arrayBufferArray\\"] = MethodMetadata {.argCount = 1, .invoker = __arrayBufferArray}; } private: @@ -108,6 +109,14 @@ private: \\"Expected promiseNullableArrayBuffer(...) to have 1 parameters\\"); return bridging::callFromJs(rt, &T::promiseNullableArrayBuffer, static_cast(&turboModule)->jsInvoker_, static_cast(&turboModule)); } + + static jsi::Value __arrayBufferArray(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + static_assert( + bridging::getParameterCount(&T::arrayBufferArray) == 2, + \\"Expected arrayBufferArray(...) to have 2 parameters\\"); + return bridging::callFromJs(rt, &T::arrayBufferArray, static_cast(&turboModule)->jsInvoker_, static_cast(&turboModule), + count <= 0 ? throw jsi::JSError(rt, \\"Expected argument in position 0 to be passed\\") : args[0].asObject(rt).asArray(rt)); + } }; } // namespace facebook::react diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap index 327bf53723f1..d20f9cdccd4a 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleHObjCpp-test.js.snap @@ -111,6 +111,7 @@ Map { reject:(RCTPromiseRejectBlock)reject; - (void)promiseNullableArrayBuffer:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; +- (NSNumber *)arrayBufferArray:(NSArray *)values; @end diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJavaSpec-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJavaSpec-test.js.snap index 0c74d5657710..e54ebf6d160b 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJavaSpec-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJavaSpec-test.js.snap @@ -98,6 +98,10 @@ public abstract class NativeSampleTurboModuleSpec extends ReactContextBaseJavaMo @ReactMethod @DoNotStrip public abstract void promiseNullableArrayBuffer(Promise promise); + + @ReactMethod(isBlockingSynchronousMethod = true) + @DoNotStrip + public abstract double arrayBufferArray(ArrayBuffer[] values); } ", } diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap index ca8e30f8fbf8..e4c5ba3cc0e1 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleJniCpp-test.js.snap @@ -76,6 +76,11 @@ static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_promis return static_cast(turboModule).invokeJavaMethod(rt, PromiseKind, \\"promiseNullableArrayBuffer\\", \\"(Lcom/facebook/react/bridge/Promise;)V\\", args, count, cachedMethodId, true); } +static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_arrayBufferArray(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + static jmethodID cachedMethodId = nullptr; + return static_cast(turboModule).invokeJavaMethod(rt, NumberKind, \\"arrayBufferArray\\", \\"([Lcom/facebook/react/bridge/ArrayBuffer;)D\\", args, count, cachedMethodId); +} + NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const JavaTurboModule::InitParams ¶ms) : JavaTurboModule(params) { methodMap_[\\"getArrayBuffer\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_getArrayBuffer}; @@ -83,6 +88,7 @@ NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const JavaTurboMo methodMap_[\\"voidNullableArrayBuffer\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_voidNullableArrayBuffer}; methodMap_[\\"promiseArrayBuffer\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_promiseArrayBuffer}; methodMap_[\\"promiseNullableArrayBuffer\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_promiseNullableArrayBuffer}; + methodMap_[\\"arrayBufferArray\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_arrayBufferArray}; } std::shared_ptr array_buffer_native_module_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams ¶ms) { diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleMm-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleMm-test.js.snap index 096243c34bcd..5fa12c8b5587 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleMm-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleMm-test.js.snap @@ -90,6 +90,10 @@ namespace facebook::react { return static_cast(turboModule).invokeObjCMethod(rt, PromiseKind, \\"promiseNullableArrayBuffer\\", @selector(promiseNullableArrayBuffer:reject:), args, count); } + static facebook::jsi::Value __hostFunction_NativeSampleTurboModuleSpecJSI_arrayBufferArray(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, NumberKind, \\"arrayBufferArray\\", @selector(arrayBufferArray:), args, count); + } + NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI(const ObjCTurboModule::InitParams ¶ms) : ObjCTurboModule(params) { @@ -107,6 +111,9 @@ namespace facebook::react { methodMap_[\\"promiseNullableArrayBuffer\\"] = MethodMetadata {0, __hostFunction_NativeSampleTurboModuleSpecJSI_promiseNullableArrayBuffer}; + + methodMap_[\\"arrayBufferArray\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_arrayBufferArray}; + } } // namespace facebook::react ", diff --git a/packages/react-native-codegen/src/parsers/__tests__/error-utils-test.js b/packages/react-native-codegen/src/parsers/__tests__/error-utils-test.js index 6e893f6e4197..a9e48ddc87bc 100644 --- a/packages/react-native-codegen/src/parsers/__tests__/error-utils-test.js +++ b/packages/react-native-codegen/src/parsers/__tests__/error-utils-test.js @@ -30,6 +30,7 @@ const { throwIfPartialNotAnnotatingTypeParameter, throwIfPartialWithMoreParameter, throwIfTypeAliasIsNotInterface, + throwIfUnsupportedArrayBufferArrayUsage, throwIfUnsupportedFunctionParamTypeAnnotationParserError, throwIfUnsupportedFunctionReturnTypeAnnotationParserError, throwIfUntypedModule, @@ -823,6 +824,353 @@ describe('throwIfArrayElementTypeAnnotationIsUnsupported', () => { ); }).not.toThrow(UnsupportedArrayElementTypeAnnotationParserError); }); + + it('does not throw the error if the type is ArrayBufferTypeAnnotation', () => { + expect(() => { + throwIfArrayElementTypeAnnotationIsUnsupported( + moduleName, + undefined, + 'Array', + 'ArrayBufferTypeAnnotation', + ); + }).not.toThrow(UnsupportedArrayElementTypeAnnotationParserError); + }); +}); + +describe('throwIfUnsupportedArrayBufferArrayUsage', () => { + const arrayBufferArray = { + type: 'ArrayTypeAnnotation', + elementType: {type: 'ArrayBufferTypeAnnotation'}, + }; + const objectWithArrayBufferArray = { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'buffers', + optional: false, + typeAnnotation: arrayBufferArray, + }, + ], + }; + + const moduleName = 'moduleName'; + + function check( + methodName: string, + functionTypeAnnotation: $FlowFixMe, + aliasMap: $FlowFixMe = {}, + ): void { + throwIfUnsupportedArrayBufferArrayUsage( + moduleName, + null, + methodName, + functionTypeAnnotation, + aliasMap, + ); + } + + it('does not throw for a top-level Array parameter', () => { + expect(() => { + check('arrayBufferArray', { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: {type: 'VoidTypeAnnotation'}, + params: [ + { + name: 'values', + optional: false, + typeAnnotation: arrayBufferArray, + }, + ], + }); + }).not.toThrow(); + }); + + it('throws for a nested Array> parameter', () => { + expect(() => { + check('nested', { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: {type: 'VoidTypeAnnotation'}, + params: [ + { + name: 'v', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: arrayBufferArray, + }, + }, + ], + }); + }).toThrow( + "Module moduleName: 'Array' is only supported as a top-level parameter, but 'nested' uses it in a parameter type.", + ); + }); + + it('throws for a parameter object with an Array property', () => { + expect(() => { + check('objArg', { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: {type: 'VoidTypeAnnotation'}, + params: [ + { + name: 'v', + optional: false, + typeAnnotation: objectWithArrayBufferArray, + }, + ], + }); + }).toThrow( + "Module moduleName: 'Array' is only supported as a top-level parameter, but 'objArg' uses it in a parameter type.", + ); + }); + + it('throws for a method returning Array', () => { + expect(() => { + check('getBuffers', { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: arrayBufferArray, + params: [], + }); + }).toThrow( + "Module moduleName: 'Array' is only supported as a top-level parameter, but 'getBuffers' uses it in a return type.", + ); + }); + + it('throws for a method returning Array>', () => { + expect(() => { + check('retNested', { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: arrayBufferArray, + }, + params: [], + }); + }).toThrow( + "Module moduleName: 'Array' is only supported as a top-level parameter, but 'retNested' uses it in a return type.", + ); + }); + + it('throws for a method returning an object with an Array property', () => { + expect(() => { + check('retObj', { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: objectWithArrayBufferArray, + params: [], + }); + }).toThrow( + "Module moduleName: 'Array' is only supported as a top-level parameter, but 'retObj' uses it in a return type.", + ); + }); + + it('throws for a method returning Promise>', () => { + expect(() => { + check('getAsyncBuffers', { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + elementType: arrayBufferArray, + }, + params: [], + }); + }).toThrow( + "Module moduleName: 'Array' is only supported as a top-level parameter, but 'getAsyncBuffers' uses it in a resolution type.", + ); + }); + + it('throws for a method returning Promise>', () => { + expect(() => { + check('retPromiseObj', { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + elementType: objectWithArrayBufferArray, + }, + params: [], + }); + }).toThrow( + "Module moduleName: 'Array' is only supported as a top-level parameter, but 'retPromiseObj' uses it in a resolution type.", + ); + }); + + it('resolves aliases when looking for Array', () => { + expect(() => { + check( + 'aliasedArg', + { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: {type: 'VoidTypeAnnotation'}, + params: [ + { + name: 'v', + optional: false, + typeAnnotation: { + type: 'TypeAliasTypeAnnotation', + name: 'Payload', + }, + }, + ], + }, + {Payload: objectWithArrayBufferArray}, + ); + }).toThrow( + "Module moduleName: 'Array' is only supported as a top-level parameter, but 'aliasedArg' uses it in a parameter type.", + ); + }); + + it('does not infinitely recurse on a self-referential alias', () => { + const selfReferential = { + type: 'ObjectTypeAnnotation', + properties: [ + { + name: 'next', + optional: true, + typeAnnotation: {type: 'TypeAliasTypeAnnotation', name: 'Node'}, + }, + ], + }; + expect(() => { + check( + 'recursive', + { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: {type: 'VoidTypeAnnotation'}, + params: [ + { + name: 'v', + optional: false, + typeAnnotation: {type: 'TypeAliasTypeAnnotation', name: 'Node'}, + }, + ], + }, + {Node: selfReferential}, + ); + }).not.toThrow(); + }); + + it('throws for Array parameter elements', () => { + expect(() => { + check('nullableElements', { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: {type: 'VoidTypeAnnotation'}, + params: [ + { + name: 'values', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'NullableTypeAnnotation', + typeAnnotation: {type: 'ArrayBufferTypeAnnotation'}, + }, + }, + }, + ], + }); + }).toThrow( + "Module moduleName: 'Array' does not support nullable elements. Change 'nullableElements' to use 'Array' instead of 'Array'.", + ); + }); + + it('does not throw for ?Array parameter', () => { + expect(() => { + check('nullableArray', { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: {type: 'VoidTypeAnnotation'}, + params: [ + { + name: 'values', + optional: false, + typeAnnotation: { + type: 'NullableTypeAnnotation', + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: {type: 'ArrayBufferTypeAnnotation'}, + }, + }, + }, + ], + }); + }).not.toThrow(); + }); + + it('throws for Array in a callback parameter type', () => { + expect(() => { + check('withCallback', { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: {type: 'VoidTypeAnnotation'}, + params: [ + { + name: 'cb', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: {type: 'VoidTypeAnnotation'}, + params: [ + { + name: 'bufs', + optional: false, + typeAnnotation: arrayBufferArray, + }, + ], + }, + }, + ], + }); + }).toThrow( + "Module moduleName: 'Array' is only supported as a top-level parameter, but 'withCallback' uses it in a callback parameter type.", + ); + }); + + it('throws for Array in a callback return type', () => { + expect(() => { + check('withCallbackReturn', { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: {type: 'VoidTypeAnnotation'}, + params: [ + { + name: 'cb', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: arrayBufferArray, + params: [], + }, + }, + ], + }); + }).toThrow( + "Module moduleName: 'Array' is only supported as a top-level parameter, but 'withCallbackReturn' uses it in a callback return type.", + ); + }); + + it('throws for Array nested in a callback object parameter', () => { + expect(() => { + check('nestedCallback', { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: {type: 'VoidTypeAnnotation'}, + params: [ + { + name: 'cb', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: {type: 'VoidTypeAnnotation'}, + params: [ + { + name: 'x', + optional: false, + typeAnnotation: objectWithArrayBufferArray, + }, + ], + }, + }, + ], + }); + }).toThrow( + "Module moduleName: 'Array' is only supported as a top-level parameter, but 'nestedCallback' uses it in a callback parameter type.", + ); + }); }); describe('throwIfPartialNotAnnotatingTypeParameter', () => { diff --git a/packages/react-native-codegen/src/parsers/error-utils.js b/packages/react-native-codegen/src/parsers/error-utils.js index 348f688f9b7a..81b817229ccb 100644 --- a/packages/react-native-codegen/src/parsers/error-utils.js +++ b/packages/react-native-codegen/src/parsers/error-utils.js @@ -10,7 +10,11 @@ 'use strict'; -import type {NativeModuleTypeAnnotation} from '../CodegenSchema'; +import type { + NativeModuleAliasMap, + NativeModuleFunctionTypeAnnotation, + NativeModuleTypeAnnotation, +} from '../CodegenSchema'; import type {TypeDeclarationMap} from '../parsers/utils'; import type {ParserType} from './errors'; import type {Parser} from './parser'; @@ -24,12 +28,14 @@ const { ModuleInterfaceNotFoundParserError, MoreThanOneModuleInterfaceParserError, MoreThanOneModuleRegistryCallsParserError, + UnsupportedArrayBufferArrayUsageParserError, UnsupportedArrayElementTypeAnnotationParserError, UnsupportedFunctionParamTypeAnnotationParserError, UnsupportedFunctionReturnTypeAnnotationParserError, UnsupportedModuleEventEmitterPropertyParserError, UnsupportedModuleEventEmitterTypePropertyParserError, UnsupportedModulePropertyParserError, + UnsupportedNullableArrayBufferElementParserError, UnsupportedObjectPropertyValueTypeAnnotationParserError, UntypedModuleRegistryCallParserError, UnusedModuleInterfaceParserError, @@ -268,6 +274,181 @@ function throwIfUnsupportedFunctionParamTypeAnnotationParserError( ); } +/** + * `Array` is only implemented as a top-level method parameter. + * Reject it everywhere else - nested arrays, object properties, return types, + * Promise resolutions, callback parameters, and event payloads - so codegen + * cannot silently emit an untyped array. + * + * This validates the translated schema instead of adding `ArrayBuffer` to + * `UnsupportedArrayElementTypes`, because `translateArrayTypeAnnotation` + * swallows element-level errors and degrades to `Array`. A blocklist entry + * would therefore reject nothing, and quietly produce an untyped array instead. + * + * C++ TurboModules (`cxxOnly`) skip this guard: `Array` degrades + * to a plain `jsi::Array` there, which is intentional. + */ +function rejectUnsupportedArrayBufferArrayInTypeAnnotation( + hasteModuleName: string, + ast: $FlowFixMe, + name: string, + typeAnnotation: $FlowFixMe, + position: string, + aliasMap: {...NativeModuleAliasMap}, +): void { + const seenAliases: Set = new Set(); + + function unwrap(annotation: $FlowFixMe): $FlowFixMe { + return annotation != null && annotation.type === 'NullableTypeAnnotation' + ? unwrap(annotation.typeAnnotation) + : annotation; + } + + function isArrayBufferArray(annotation: $FlowFixMe): boolean { + if (annotation == null || annotation.type !== 'ArrayTypeAnnotation') { + return false; + } + const elementType = unwrap(annotation.elementType); + return ( + elementType != null && elementType.type === 'ArrayBufferTypeAnnotation' + ); + } + + function rejectNullableArrayBufferElement( + arrayTypeAnnotation: $FlowFixMe, + ): void { + const elementType = arrayTypeAnnotation.elementType; + if ( + elementType != null && + elementType.type === 'NullableTypeAnnotation' && + elementType.typeAnnotation != null && + elementType.typeAnnotation.type === 'ArrayBufferTypeAnnotation' + ) { + throw new UnsupportedNullableArrayBufferElementParserError( + hasteModuleName, + ast, + name, + ); + } + } + + function reject(annotation: $FlowFixMe, currentPosition: string): void { + const unwrapped = unwrap(annotation); + if (unwrapped == null) { + return; + } + + if (unwrapped.type === 'ArrayTypeAnnotation') { + rejectNullableArrayBufferElement(unwrapped); + } + + if (isArrayBufferArray(unwrapped)) { + throw new UnsupportedArrayBufferArrayUsageParserError( + hasteModuleName, + ast, + name, + currentPosition, + ); + } + + switch (unwrapped.type) { + case 'ArrayTypeAnnotation': + return reject(unwrapped.elementType, currentPosition); + case 'PromiseTypeAnnotation': + return reject(unwrapped.elementType, 'resolution type'); + case 'GenericObjectTypeAnnotation': + return reject(unwrapped.dictionaryValueType, currentPosition); + case 'ObjectTypeAnnotation': + for (const property of unwrapped.properties || []) { + reject(property.typeAnnotation, currentPosition); + } + return; + case 'UnionTypeAnnotation': + for (const member of unwrapped.types || []) { + reject(member, currentPosition); + } + return; + case 'TypeAliasTypeAnnotation': + if (seenAliases.has(unwrapped.name)) { + return; + } + seenAliases.add(unwrapped.name); + return reject(aliasMap[unwrapped.name], currentPosition); + case 'FunctionTypeAnnotation': + for (const param of unwrapped.params || []) { + reject(param.typeAnnotation, 'callback parameter type'); + } + return reject(unwrapped.returnTypeAnnotation, 'callback return type'); + } + } + + reject(typeAnnotation, position); +} + +function isNonNullableArrayBufferArray(annotation: $FlowFixMe): boolean { + return ( + annotation != null && + annotation.type === 'ArrayTypeAnnotation' && + annotation.elementType != null && + annotation.elementType.type === 'ArrayBufferTypeAnnotation' + ); +} + +function throwIfUnsupportedArrayBufferArrayUsage( + hasteModuleName: string, + methodAST: $FlowFixMe, + methodName: string, + functionTypeAnnotation: NativeModuleFunctionTypeAnnotation, + aliasMap: {...NativeModuleAliasMap}, +): void { + function unwrapParam(annotation: $FlowFixMe): $FlowFixMe { + return annotation != null && annotation.type === 'NullableTypeAnnotation' + ? unwrapParam(annotation.typeAnnotation) + : annotation; + } + + for (const param of functionTypeAnnotation.params) { + const unwrappedParam = unwrapParam(param.typeAnnotation); + if (isNonNullableArrayBufferArray(unwrappedParam)) { + continue; + } + rejectUnsupportedArrayBufferArrayInTypeAnnotation( + hasteModuleName, + methodAST, + methodName, + param.typeAnnotation, + 'parameter type', + aliasMap, + ); + } + + rejectUnsupportedArrayBufferArrayInTypeAnnotation( + hasteModuleName, + methodAST, + methodName, + functionTypeAnnotation.returnTypeAnnotation, + 'return type', + aliasMap, + ); +} + +function throwIfUnsupportedArrayBufferArrayUsageInEventEmitter( + hasteModuleName: string, + propertyAST: $FlowFixMe, + eventName: string, + eventTypeAnnotation: NativeModuleTypeAnnotation, + aliasMap: {...NativeModuleAliasMap}, +): void { + rejectUnsupportedArrayBufferArrayInTypeAnnotation( + hasteModuleName, + propertyAST, + eventName, + eventTypeAnnotation, + 'event payload type', + aliasMap, + ); +} + function throwIfArrayElementTypeAnnotationIsUnsupported( hasteModuleName: string, flowElementType: $FlowFixMe, @@ -278,7 +459,6 @@ function throwIfArrayElementTypeAnnotationIsUnsupported( FunctionTypeAnnotation: 'FunctionTypeAnnotation', VoidTypeAnnotation: 'void', PromiseTypeAnnotation: 'Promise', - ArrayBufferTypeAnnotation: 'ArrayBuffer', // TODO: Added as a work-around for now until TupleTypeAnnotation are fully supported in both flow and TS // Right now they are partially treated as UnionTypeAnnotation // UnionTypeAnnotation: 'UnionTypeAnnotation', @@ -422,6 +602,8 @@ module.exports = { throwIfMoreThanOneModuleInterfaceParserError, throwIfUnsupportedFunctionParamTypeAnnotationParserError, throwIfArrayElementTypeAnnotationIsUnsupported, + throwIfUnsupportedArrayBufferArrayUsage, + throwIfUnsupportedArrayBufferArrayUsageInEventEmitter, throwIfIncorrectModuleRegistryCallArgument, throwIfPartialNotAnnotatingTypeParameter, throwIfPartialWithMoreParameter, diff --git a/packages/react-native-codegen/src/parsers/errors.js b/packages/react-native-codegen/src/parsers/errors.js index 7e0b2277930f..8055bfb61f2a 100644 --- a/packages/react-native-codegen/src/parsers/errors.js +++ b/packages/react-native-codegen/src/parsers/errors.js @@ -213,6 +213,31 @@ class UnsupportedArrayElementTypeAnnotationParserError extends ParserError { } } +class UnsupportedArrayBufferArrayUsageParserError extends ParserError { + constructor( + nativeModuleName: string, + methodAST: $FlowFixMe, + methodName: string, + position: string, + ) { + super( + nativeModuleName, + methodAST, + `'Array' is only supported as a top-level parameter, but '${methodName}' uses it in a ${position}.`, + ); + } +} + +class UnsupportedNullableArrayBufferElementParserError extends ParserError { + constructor(nativeModuleName: string, ast: $FlowFixMe, contextName: string) { + super( + nativeModuleName, + ast, + `'Array' does not support nullable elements. Change '${contextName}' to use 'Array' instead of 'Array'.`, + ); + } +} + /** * Object parsing errors */ @@ -448,6 +473,8 @@ module.exports = { ModuleInterfaceNotFoundParserError, MoreThanOneModuleInterfaceParserError, UnnamedFunctionParamParserError, + UnsupportedArrayBufferArrayUsageParserError, + UnsupportedNullableArrayBufferElementParserError, UnsupportedArrayElementTypeAnnotationParserError, UnsupportedGenericParserError, UnsupportedTypeAnnotationParserError, diff --git a/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/failures.js b/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/failures.js index 515098be5462..8cdb74a263aa 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/failures.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/failures.js @@ -350,6 +350,295 @@ export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); `; +const NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_PARAM = ` +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +nested: (v: Array>) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_OBJECT_PARAM = ` +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +objArg: (v: {buffers: Array}) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_RETURN = ` +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getBuffers: () => Array; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_RETURN = ` +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +retNested: () => Array>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_OBJECT_RETURN = ` +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +retObj: () => {buffers: Array}; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_PROMISE_RETURN = ` +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +getAsyncBuffers: () => Promise>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_PROMISE_OBJECT = ` +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +retPromiseObj: () => Promise<{buffers: Array}>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_NULLABLE_ARRAY_BUFFER_ELEMENT_PARAM = ` +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +nullableElements: (values: Array) => number; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_CALLBACK_PARAM = ` +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +withCallback: (cb: (bufs: Array) => void) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_CALLBACK_RETURN = ` +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +withCallbackReturn: (cb: () => Array) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_ARRAY_IN_CALLBACK = ` +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +nestedCallback: (cb: (x: {buffers: Array}) => void) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_EVENT_EMITTER = ` +/** + * 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. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {TurboModule} from '../RCTExport'; +import type {EventEmitter} from '../CodegenTypes'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +onBuffers: EventEmitter>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + module.exports = { NATIVE_MODULES_WITH_READ_ONLY_OBJECT_NO_TYPE_FOR_CONTENT, NATIVE_MODULES_WITH_UNNAMED_PARAMS, @@ -364,4 +653,16 @@ module.exports = { NUMERIC_VALUES_ENUM_NATIVE_MODULE, MAP_WITH_EXTRA_KEYS_NATIVE_MODULE, NATIVE_MODULES_WITH_ARRAY_BUFFER_IN_OBJECT_PROPERTY, + NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_PARAM, + NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_OBJECT_PARAM, + NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_RETURN, + NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_RETURN, + NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_OBJECT_RETURN, + NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_PROMISE_RETURN, + NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_PROMISE_OBJECT, + NATIVE_MODULE_WITH_NULLABLE_ARRAY_BUFFER_ELEMENT_PARAM, + NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_CALLBACK_PARAM, + NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_CALLBACK_RETURN, + NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_ARRAY_IN_CALLBACK, + NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_EVENT_EMITTER, }; diff --git a/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js index 53605cbe10ea..a9d9d87efa50 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js @@ -1034,6 +1034,7 @@ export interface Spec extends TurboModule { +voidArrayBuffer: (arg: ArrayBuffer) => void; +voidNullableArrayBuffer: (arg: ?ArrayBuffer) => void; +promiseArrayBuffer: () => Promise; + +arrayBufferArray: (values: Array) => number; } export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); diff --git a/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap b/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap index 5309c7d0c616..fed61b166d08 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap +++ b/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap @@ -6,6 +6,30 @@ exports[`RN Codegen Flow Parser Fails with error message MAP_WITH_EXTRA_KEYS_NAT exports[`RN Codegen Flow Parser Fails with error message MIXED_VALUES_ENUM_NATIVE_MODULE 1`] = `"Module NativeSampleTurboModule: Failed parsing the enum SomeEnum in NativeSampleTurboModule with the error: Enums can not be mixed- they all must be either blank, number, or string values."`; +exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_CALLBACK_PARAM 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'withCallback' uses it in a callback parameter type."`; + +exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_CALLBACK_RETURN 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'withCallbackReturn' uses it in a callback return type."`; + +exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_EVENT_EMITTER 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'onBuffers' uses it in a event payload type."`; + +exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_OBJECT_PARAM 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'objArg' uses it in a parameter type."`; + +exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_OBJECT_RETURN 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'retObj' uses it in a return type."`; + +exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_PROMISE_OBJECT 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'retPromiseObj' uses it in a resolution type."`; + +exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_PROMISE_RETURN 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'getAsyncBuffers' uses it in a resolution type."`; + +exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_RETURN 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'getBuffers' uses it in a return type."`; + +exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_ARRAY_IN_CALLBACK 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'nestedCallback' uses it in a callback parameter type."`; + +exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_PARAM 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'nested' uses it in a parameter type."`; + +exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_RETURN 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'retNested' uses it in a return type."`; + +exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULE_WITH_NULLABLE_ARRAY_BUFFER_ELEMENT_PARAM 1`] = `"Module NativeSampleTurboModule: 'Array' does not support nullable elements. Change 'nullableElements' to use 'Array' instead of 'Array'."`; + exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULES_WITH_ARRAY_BUFFER_IN_OBJECT_PROPERTY 1`] = `"Module NativeSampleTurboModule: Object property '[object Object]' cannot have type 'ArrayBuffer'."`; exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT 1`] = `"Module NativeSampleTurboModule: Generic 'Array' must have type parameters."`; @@ -1232,6 +1256,28 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_ARRAY_BU }, 'params': [] } + }, + { + 'name': 'arrayBufferArray', + 'optional': false, + 'typeAnnotation': { + 'type': 'FunctionTypeAnnotation', + 'returnTypeAnnotation': { + 'type': 'NumberTypeAnnotation' + }, + 'params': [ + { + 'name': 'values', + 'optional': false, + 'typeAnnotation': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'ArrayBufferTypeAnnotation' + } + } + } + ] + } } ] }, diff --git a/packages/react-native-codegen/src/parsers/parsers-commons.js b/packages/react-native-codegen/src/parsers/parsers-commons.js index 330e52fe8c76..0b0bf046bde6 100644 --- a/packages/react-native-codegen/src/parsers/parsers-commons.js +++ b/packages/react-native-codegen/src/parsers/parsers-commons.js @@ -54,6 +54,8 @@ const { throwIfMoreThanOneModuleRegistryCalls, throwIfPropertyValueTypeIsUnsupported, throwIfTypeAliasIsNotInterface, + throwIfUnsupportedArrayBufferArrayUsage, + throwIfUnsupportedArrayBufferArrayUsageInEventEmitter, throwIfUnsupportedFunctionParamTypeAnnotationParserError, throwIfUnsupportedFunctionReturnTypeAnnotationParserError, throwIfUntypedModule, @@ -466,23 +468,32 @@ function buildPropertySchema( parser, ); + const functionTypeAnnotation = translateFunctionTypeAnnotation( + hasteModuleName, + value, + types, + aliasMap, + enumMap, + tryParse, + cxxOnly, + translateTypeAnnotation, + parser, + ); + + if (!cxxOnly) { + throwIfUnsupportedArrayBufferArrayUsage( + hasteModuleName, + property, + methodName, + functionTypeAnnotation, + aliasMap, + ); + } + return { name: methodName, optional: Boolean(property.optional), - typeAnnotation: wrapNullable( - nullable, - translateFunctionTypeAnnotation( - hasteModuleName, - value, - types, - aliasMap, - enumMap, - tryParse, - cxxOnly, - translateTypeAnnotation, - parser, - ), - ), + typeAnnotation: wrapNullable(nullable, functionTypeAnnotation), }; } @@ -551,6 +562,16 @@ function buildEventEmitterSchema( parser, ); + if (!cxxOnly) { + throwIfUnsupportedArrayBufferArrayUsageInEventEmitter( + hasteModuleName, + property, + eventemitterName, + eventTypeAnnotation, + aliasMap, + ); + } + return { name: eventemitterName, optional: Boolean(property.optional), diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/failures.js b/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/failures.js index a59c138df1d8..8984c1487b43 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/failures.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/failures.js @@ -286,6 +286,261 @@ export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); `; +const NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_PARAM = ` +/** + * 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. + * + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly nested: (v: Array>) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_OBJECT_PARAM = ` +/** + * 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. + * + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly objArg: (v: {buffers: Array}) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_RETURN = ` +/** + * 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. + * + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getBuffers: () => Array; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_RETURN = ` +/** + * 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. + * + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly retNested: () => Array>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_OBJECT_RETURN = ` +/** + * 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. + * + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly retObj: () => {buffers: Array}; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_PROMISE_RETURN = ` +/** + * 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. + * + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly getAsyncBuffers: () => Promise>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_PROMISE_OBJECT = ` +/** + * 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. + * + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly retPromiseObj: () => Promise<{buffers: Array}>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_NULLABLE_ARRAY_BUFFER_ELEMENT_PARAM = ` +/** + * 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. + * + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly nullableElements: (values: Array) => number; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_CALLBACK_PARAM = ` +/** + * 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. + * + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly withCallback: (cb: (bufs: Array) => void) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_CALLBACK_RETURN = ` +/** + * 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. + * + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly withCallbackReturn: (cb: () => Array) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_ARRAY_IN_CALLBACK = ` +/** + * 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. + * + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly nestedCallback: ( + cb: (x: {buffers: Array}) => void, + ) => void; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + +const NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_EVENT_EMITTER = ` +/** + * 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. + * + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import type {EventEmitter} from 'react-native/Libraries/Types/CodegenTypes'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + readonly onBuffers: EventEmitter>; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + module.exports = { NATIVE_MODULES_WITH_UNNAMED_PARAMS, NATIVE_MODULES_WITH_PROMISE_WITHOUT_TYPE, @@ -299,4 +554,16 @@ module.exports = { NUMERIC_VALUES_ENUM_NATIVE_MODULE, MAP_WITH_EXTRA_KEYS_NATIVE_MODULE, NATIVE_MODULES_WITH_ARRAY_BUFFER_IN_OBJECT_PROPERTY, + NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_PARAM, + NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_OBJECT_PARAM, + NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_RETURN, + NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_RETURN, + NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_OBJECT_RETURN, + NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_PROMISE_RETURN, + NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_PROMISE_OBJECT, + NATIVE_MODULE_WITH_NULLABLE_ARRAY_BUFFER_ELEMENT_PARAM, + NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_CALLBACK_PARAM, + NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_CALLBACK_RETURN, + NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_ARRAY_IN_CALLBACK, + NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_EVENT_EMITTER, }; diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/fixtures.js index c405627b93e4..c3b632acec43 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/fixtures.js @@ -1038,6 +1038,7 @@ export interface Spec extends TurboModule { readonly voidArrayBuffer: (arg: ArrayBuffer) => void; readonly voidNullableArrayBuffer: (arg: ArrayBuffer | null) => void; readonly promiseArrayBuffer: () => Promise; + readonly arrayBufferArray: (values: Array) => number; } export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap b/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap index 4b098b27af55..c6020f7f346b 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap +++ b/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap @@ -6,6 +6,30 @@ exports[`RN Codegen TypeScript Parser Fails with error message MAP_WITH_EXTRA_KE exports[`RN Codegen TypeScript Parser Fails with error message MIXED_VALUES_ENUM_NATIVE_MODULE 1`] = `"Module NativeSampleTurboModule: Failed parsing the enum SomeEnum in NativeSampleTurboModule with the error: Enum values can not be mixed. They all must be either blank, number, or string values."`; +exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_CALLBACK_PARAM 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'withCallback' uses it in a callback parameter type."`; + +exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_CALLBACK_RETURN 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'withCallbackReturn' uses it in a callback return type."`; + +exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_EVENT_EMITTER 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'onBuffers' uses it in a event payload type."`; + +exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_OBJECT_PARAM 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'objArg' uses it in a parameter type."`; + +exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_OBJECT_RETURN 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'retObj' uses it in a return type."`; + +exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_IN_PROMISE_OBJECT 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'retPromiseObj' uses it in a resolution type."`; + +exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_PROMISE_RETURN 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'getAsyncBuffers' uses it in a resolution type."`; + +exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULE_WITH_ARRAY_BUFFER_ARRAY_RETURN 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'getBuffers' uses it in a return type."`; + +exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_ARRAY_IN_CALLBACK 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'nestedCallback' uses it in a callback parameter type."`; + +exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_PARAM 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'nested' uses it in a parameter type."`; + +exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULE_WITH_NESTED_ARRAY_BUFFER_RETURN 1`] = `"Module NativeSampleTurboModule: 'Array' is only supported as a top-level parameter, but 'retNested' uses it in a return type."`; + +exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULE_WITH_NULLABLE_ARRAY_BUFFER_ELEMENT_PARAM 1`] = `"Module NativeSampleTurboModule: 'Array' does not support nullable elements. Change 'nullableElements' to use 'Array' instead of 'Array'."`; + exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULES_WITH_ARRAY_BUFFER_IN_OBJECT_PROPERTY 1`] = `"Module NativeSampleTurboModule: Object property '[object Object]' cannot have type 'ArrayBuffer'."`; exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULES_WITH_ARRAY_WITH_NO_TYPE_FOR_CONTENT 1`] = `"Module NativeSampleTurboModule: Generic 'Array' must have type parameters."`; @@ -1230,6 +1254,28 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_AR }, 'params': [] } + }, + { + 'name': 'arrayBufferArray', + 'optional': false, + 'typeAnnotation': { + 'type': 'FunctionTypeAnnotation', + 'returnTypeAnnotation': { + 'type': 'NumberTypeAnnotation' + }, + 'params': [ + { + 'name': 'values', + 'optional': false, + 'typeAnnotation': { + 'type': 'ArrayTypeAnnotation', + 'elementType': { + 'type': 'ArrayBufferTypeAnnotation' + } + } + } + ] + } } ] }, diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp b/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp index a8d8e8a44c06..65094986e026 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/android/ReactCommon/JavaTurboModule.cpp @@ -5,14 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -#include #include #include #include #include #include -#include #include #include #include @@ -431,6 +429,67 @@ int32_t getUniqueId() { return counter++; } +jni::local_ref convertJSIArrayBufferToJArrayBuffer( + jsi::Runtime& rt, + const jsi::Value* arg, + int argIndex, + const std::string& methodName, + bool isSyncInvocation, + std::vector>* borrowedBuffers) { + if (!(arg->isObject() && arg->getObject(rt).isArrayBuffer(rt))) { + throw JavaTurboModuleArgumentConversionException( + "ArrayBuffer", argIndex, methodName, arg, &rt); + } + + auto arrayBuffer = arg->getObject(rt).getArrayBuffer(rt); + AsyncArrayBuffer::throwIfDetached( + rt, arrayBuffer, "JavaTurboModule::convertJSIArgsToJNIArgs"); + + auto size = arrayBuffer.size(rt); + if (size > static_cast(std::numeric_limits::max())) { + throw jsi::JSError( + rt, + "JavaTurboModule::convertJSIArgsToJNIArgs: ArrayBuffer exceeds maximum size."); + } + + // Runtimes without a native buffer for this ArrayBuffer return nullptr, + // but the Static Hermes tracing runtime throws instead, and argument + // conversion runs outside any std::exception handler. Treat a failed + // probe as "no native buffer" so a traced session copies the bytes rather + // than aborting the process. + std::shared_ptr mutableBuffer; + try { + mutableBuffer = arrayBuffer.tryGetMutableBuffer(rt); + } catch (const std::exception&) { + mutableBuffer = nullptr; + } + + bool borrowsJSBytes = false; + 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 (mutableBuffer) { + return JArrayBuffer::createOwning(std::move(mutableBuffer)); + } + + // JS heap bytes on a synchronous call: lend them for the duration of + // the call. + if (isSyncInvocation) { + borrowsJSBytes = true; + return JArrayBuffer::createUnowned(arrayBuffer.data(rt), size); + } + + // JS heap bytes that outlive the call: copy. + return JArrayBuffer::createOwned(arrayBuffer.data(rt), size); + }(); + + if (borrowsJSBytes) { + borrowedBuffers->push_back(jni::make_local(jArrayBuffer)); + } + + return jArrayBuffer; +} + // fbjni already does this conversion, but since we are using plain JNI, this // needs to be done again // TODO (axe) Reuse existing implementation as needed - the exist in @@ -587,57 +646,53 @@ JNIArgs convertJSIArgsToJNIArgs( auto jParams = JDynamicNative::newObjectCxxArgs(dynamicFromValue); jarg->l = makeGlobalIfNecessary(jParams.release()); } else if (type == "Lcom/facebook/react/bridge/ArrayBuffer;") { - if (!(arg->isObject() && arg->getObject(rt).isArrayBuffer(rt))) { + auto jArrayBuffer = convertJSIArrayBufferToJArrayBuffer( + rt, + arg, + argIndex, + methodName, + isSyncInvocation, + &jniArgs.borrowedBuffers); + jarg->l = makeGlobalIfNecessary(jArrayBuffer.release()); + } else if (type == "[Lcom/facebook/react/bridge/ArrayBuffer;") { + if (!(arg->isObject() && arg->getObject(rt).isArray(rt))) { throw JavaTurboModuleArgumentConversionException( - "ArrayBuffer", argIndex, methodName, arg, &rt); + "Array", argIndex, methodName, arg, &rt); } - auto arrayBuffer = arg->getObject(rt).getArrayBuffer(rt); - AsyncArrayBuffer::throwIfDetached( - rt, arrayBuffer, "JavaTurboModule::convertJSIArgsToJNIArgs"); - - auto size = arrayBuffer.size(rt); - if (size > static_cast(std::numeric_limits::max())) { + auto jsArray = arg->getObject(rt).getArray(rt); + auto arraySize = jsArray.size(rt); + if (arraySize > static_cast(std::numeric_limits::max())) { throw jsi::JSError( rt, - "JavaTurboModule::convertJSIArgsToJNIArgs: ArrayBuffer exceeds maximum size."); + "JavaTurboModule::convertJSIArgsToJNIArgs: Array exceeds maximum size."); } - // Runtimes without a native buffer for this ArrayBuffer return nullptr, - // but the Static Hermes tracing runtime throws instead, and argument - // conversion runs outside any std::exception handler. Treat a failed - // probe as "no native buffer" so a traced session copies the bytes rather - // than aborting the process. - std::shared_ptr mutableBuffer; - try { - mutableBuffer = arrayBuffer.tryGetMutableBuffer(rt); - } catch (const std::exception&) { - mutableBuffer = nullptr; - } - - bool borrowsJSBytes = false; - 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 (mutableBuffer) { - return JArrayBuffer::createOwning(std::move(mutableBuffer)); - } - - // JS heap bytes on a synchronous call: lend them for the duration of - // the call. - if (isSyncInvocation) { - borrowsJSBytes = true; - return JArrayBuffer::createUnowned(arrayBuffer.data(rt), size); + auto result = + jni::JArrayClass::newArray(arraySize); + + for (size_t i = 0; i < arraySize; i++) { + auto element = jsArray.getValueAtIndex(rt, i); + try { + auto jArrayBuffer = convertJSIArrayBufferToJArrayBuffer( + rt, + &element, + argIndex, + methodName, + isSyncInvocation, + &jniArgs.borrowedBuffers); + result->setElement(i, jArrayBuffer.get()); + } catch (const JavaTurboModuleArgumentConversionException&) { + throw JavaTurboModuleArgumentConversionException( + "ArrayBuffer at index " + std::to_string(i), + argIndex, + methodName, + &element, + &rt); } - - // JS heap bytes that outlive the call: copy. - return JArrayBuffer::createOwned(arrayBuffer.data(rt), size); - }(); - - if (borrowsJSBytes) { - jniArgs.borrowedBuffers.push_back(jni::make_local(jArrayBuffer)); } - jarg->l = makeGlobalIfNecessary(jArrayBuffer.release()); + + jarg->l = makeGlobalIfNecessary(result.release()); } else { throw JavaTurboModuleInvalidArgumentTypeException( type, argIndex, methodName); diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm index 7270187b4142..4e6d6a70315a 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm @@ -161,13 +161,15 @@ size_t size() const override jsi::Runtime &runtime, const jsi::Array &value, const std::shared_ptr &jsInvoker, - BOOL useNSNull) + BOOL useNSNull, + BOOL mustCopyBytes) { size_t size = value.size(runtime); NSMutableArray *result = [NSMutableArray new]; for (size_t i = 0; i < size; i++) { // Insert kCFNull when it's `undefined` value to preserve the indices. - id convertedObject = convertJSIValueToObjCObject(runtime, value.getValueAtIndex(runtime, i), jsInvoker, useNSNull); + id convertedObject = + convertJSIValueToObjCObject(runtime, value.getValueAtIndex(runtime, i), jsInvoker, useNSNull, mustCopyBytes); [result addObject:(convertedObject != nullptr) ? convertedObject : (id)kCFNull]; } return result; @@ -177,7 +179,8 @@ size_t size() const override jsi::Runtime &runtime, const jsi::Object &value, const std::shared_ptr &jsInvoker, - BOOL useNSNull) + BOOL useNSNull, + BOOL mustCopyBytes) { jsi::Array propertyNames = value.getPropertyNames(runtime); size_t size = propertyNames.size(runtime); @@ -185,7 +188,7 @@ size_t size() const override for (size_t i = 0; i < size; i++) { jsi::String name = propertyNames.getValueAtIndex(runtime, i).getString(runtime); NSString *k = convertJSIStringToNSString(runtime, name); - id v = convertJSIValueToObjCObject(runtime, value.getProperty(runtime, name), jsInvoker, useNSNull); + id v = convertJSIValueToObjCObject(runtime, value.getProperty(runtime, name), jsInvoker, useNSNull, mustCopyBytes); if (v != nullptr) { result[k] = v; } @@ -264,7 +267,7 @@ id convertJSIValueToObjCObject( if (value.isObject()) { jsi::Object o = value.getObject(runtime); if (o.isArray(runtime)) { - return convertJSIArrayToNSArray(runtime, o.getArray(runtime), jsInvoker, useNSNull); + return convertJSIArrayToNSArray(runtime, o.getArray(runtime), jsInvoker, useNSNull, mustCopyBytes); } if (o.isFunction(runtime)) { return convertJSIFunctionToCallback(runtime, o.getFunction(runtime), jsInvoker); @@ -272,7 +275,7 @@ id convertJSIValueToObjCObject( if (o.isArrayBuffer(runtime)) { return convertJSIArrayBufferToRCTArrayBuffer(runtime, o.getArrayBuffer(runtime), mustCopyBytes); } - return convertJSIObjectToNSDictionary(runtime, o, jsInvoker, useNSNull); + return convertJSIObjectToNSDictionary(runtime, o, jsInvoker, useNSNull, mustCopyBytes); } throw jsi::JSError(runtime, "Unsupported jsi::Value kind"); diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.kt b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.kt index 1f35cc70b760..655395e8172b 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.kt +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.kt @@ -207,6 +207,23 @@ public class SampleTurboModule(private val context: ReactApplicationContext) : promise.resolve(buffer) } + // Every element borrows the bytes of the matching JS ArrayBuffer for the duration of this + // synchronous call, so they are read here and never retained. Summing every byte proves the + // whole array crossed the boundary, not just its shape. + @DoNotStrip + @Suppress("unused") + override fun arrayBufferArray(values: Array): Double { + var checksum = 0L + for (value in values) { + val bytes = value.bytes + for (i in 0 until bytes.capacity()) { + checksum += (bytes.get(i).toInt() and 0xFF).toLong() + } + } + log("arrayBufferArray", values.size, checksum) + return checksum.toDouble() + } + @DoNotStrip @Suppress("unused") override fun getValueWithCallback(callback: Callback?) { diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm index a167efca0d77..908180998000 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm @@ -195,6 +195,23 @@ - (void)getAsyncBuffer:(double)size resolve:(RCTPromiseResolveBlock)resolve reje resolve(buffer); } +- (NSNumber *)arrayBufferArray:(NSArray *)values +{ + uint64_t checksum = 0; + for (RCTArrayBuffer *value in values) { + const auto *bytes = static_cast(value.mutableBytes); + if (bytes == nullptr) { + continue; + } + + std::span byteSpan(bytes, static_cast(value.length)); + for (auto byte : byteSpan) { + checksum += byte; + } + } + return @(checksum); +} + - (void)getValueWithCallback:(RCTResponseSenderBlock)callback { if (callback == nullptr) { diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeSampleTurboModule.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeSampleTurboModule.js index 3f4b77c4801c..3f4973553f81 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeSampleTurboModule.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeSampleTurboModule.js @@ -55,6 +55,7 @@ export interface Spec extends TurboModule { readonly createNativeBuffer: (size: number) => ArrayBuffer; readonly processAsyncBuffer: (payload: ArrayBuffer) => Promise; readonly getAsyncBuffer: (size: number) => Promise; + readonly arrayBufferArray: (values: Array) => number; readonly getValueWithCallback: (callback: (value: string) => void) => void; readonly getValueWithPromise: (error: boolean) => Promise; readonly voidFuncThrows?: () => void; diff --git a/packages/rn-tester/RNTesterUnitTests/RCTTurboModuleArrayBufferTests.mm b/packages/rn-tester/RNTesterUnitTests/RCTTurboModuleArrayBufferTests.mm index 14f32b48e849..ea979cdb7c76 100644 --- a/packages/rn-tester/RNTesterUnitTests/RCTTurboModuleArrayBufferTests.mm +++ b/packages/rn-tester/RNTesterUnitTests/RCTTurboModuleArrayBufferTests.mm @@ -99,6 +99,7 @@ void flushQueue() @interface RCTTestArrayBufferTurboModule : NSObject @property (nonatomic, copy) NSData *lastReceivedPayload; +@property (nonatomic, copy) NSArray *storedArrayBufferArrayElements; @property (nonatomic, assign) BOOL sawAliasedBytes; @property (nonatomic, assign) BOOL sawUnownedBytes; @@ -158,6 +159,71 @@ - (void)testMethodWhichReturnsArrayBuffer:(double)size resolve(createIntegerSequenceBuffer(static_cast(size))); } +- (NSNumber *)testMethodWhichSumsArrayBufferArray:(NSArray *)buffers +{ + uint64_t checksum = 0; + for (id value in buffers) { + if (![value isKindOfClass:[RCTArrayBuffer class]]) { + continue; + } + + RCTArrayBuffer *buffer = (RCTArrayBuffer *)value; + const auto *bytes = static_cast(buffer.mutableBytes); + if (bytes == nullptr) { + continue; + } + + std::span byteSpan(bytes, static_cast(buffer.length)); + for (auto byte : byteSpan) { + checksum += byte; + } + } + return @(checksum); +} + +- (NSNumber *)testMethodWhichChecksArrayBufferArrayAliasing:(NSArray *)buffers +{ + self.sawAliasedBytes = NO; + self.sawUnownedBytes = NO; + if (buffers.count == 0) { + return @(YES); + } + + BOOL allAliased = YES; + BOOL allUnowned = YES; + for (id value in buffers) { + if (![value isKindOfClass:[RCTArrayBuffer class]]) { + continue; + } + + RCTArrayBuffer *buffer = (RCTArrayBuffer *)value; + auto *bytes = static_cast(buffer.mutableBytes); + if (bytes != nullptr && buffer.length > 0) { + bytes[0] = 77; + if (bytes[0] != 77) { + allAliased = NO; + } + } + if (buffer.isOwningBytes) { + allUnowned = NO; + } + } + self.sawAliasedBytes = allAliased; + self.sawUnownedBytes = allUnowned; + return @(YES); +} + +- (void)testMethodWhichStoresArrayBufferArrayElements:(NSArray *)buffers +{ + NSMutableArray *stored = [NSMutableArray array]; + for (id value in buffers) { + if ([value isKindOfClass:[RCTArrayBuffer class]]) { + [stored addObject:(RCTArrayBuffer *)value]; + } + } + self.storedArrayBufferArrayElements = stored; +} + @end @interface RCTTurboModuleArrayBufferTests : XCTestCase @@ -498,4 +564,189 @@ - (void)testPromiseResolvesArrayBuffer XCTAssertEqual(resolvedBytes[3], 3); } +- (void)testSyncArrayBufferArrayRoundTrip +{ + auto hermesRuntime = createHermesRuntime(); + facebook::jsi::Runtime *rt = hermesRuntime.get(); + auto *instance = [RCTTestArrayBufferTurboModule new]; + + ObjCTurboModule::InitParams params = { + .moduleName = "TestModule", + .instance = instance, + .jsInvoker = nullptr, + .nativeMethodCallInvoker = std::make_shared(), + .isSyncModule = false, + }; + ObjCTurboModule module(params); + + auto jsArray = rt->global() + .getPropertyAsFunction(*rt, "eval") + .call(*rt, "[new Uint8Array([1, 2]).buffer, new Uint8Array([3]).buffer]") + .asObject(*rt) + .getArray(*rt); + facebook::jsi::Value args[1] = {facebook::jsi::Value(*rt, jsArray)}; + + auto result = module.invokeObjCMethod( + *rt, + NumberKind, + "testMethodWhichSumsArrayBufferArray", + @selector(testMethodWhichSumsArrayBufferArray:), + args, + 1); + + XCTAssertTrue(result.isNumber()); + XCTAssertEqual(result.getNumber(), 7.0); +} + +- (void)testEmptyArrayBufferArrayRoundTrip +{ + auto hermesRuntime = createHermesRuntime(); + facebook::jsi::Runtime *rt = hermesRuntime.get(); + auto *instance = [RCTTestArrayBufferTurboModule new]; + + ObjCTurboModule::InitParams params = { + .moduleName = "TestModule", + .instance = instance, + .jsInvoker = nullptr, + .nativeMethodCallInvoker = std::make_shared(), + .isSyncModule = false, + }; + ObjCTurboModule module(params); + + auto jsArray = rt->global().getPropertyAsFunction(*rt, "eval").call(*rt, "[]").asObject(*rt).getArray(*rt); + facebook::jsi::Value args[1] = {facebook::jsi::Value(*rt, jsArray)}; + + auto result = module.invokeObjCMethod( + *rt, + NumberKind, + "testMethodWhichSumsArrayBufferArray", + @selector(testMethodWhichSumsArrayBufferArray:), + args, + 1); + + XCTAssertTrue(result.isNumber()); + XCTAssertEqual(result.getNumber(), 0.0); +} + +- (void)testNonArrayBufferElementsInArrayBufferArrayAreIgnored +{ + auto hermesRuntime = createHermesRuntime(); + facebook::jsi::Runtime *rt = hermesRuntime.get(); + auto *instance = [RCTTestArrayBufferTurboModule new]; + + ObjCTurboModule::InitParams params = { + .moduleName = "TestModule", + .instance = instance, + .jsInvoker = nullptr, + .nativeMethodCallInvoker = std::make_shared(), + .isSyncModule = false, + }; + ObjCTurboModule module(params); + + auto jsArray = + rt->global().getPropertyAsFunction(*rt, "eval").call(*rt, "[1, 2, 3]").asObject(*rt).getArray(*rt); + facebook::jsi::Value args[1] = {facebook::jsi::Value(*rt, jsArray)}; + + auto result = module.invokeObjCMethod( + *rt, + NumberKind, + "testMethodWhichSumsArrayBufferArray", + @selector(testMethodWhichSumsArrayBufferArray:), + args, + 1); + + XCTAssertTrue(result.isNumber()); + XCTAssertEqual(result.getNumber(), 0.0); +} + +// Every JS-heap element in a sync Array argument must alias its source buffer, not just +// the first one. +- (void)testJSBackedArrayBufferArrayElementsAreNotCopiedDuringTheCall +{ + auto hermesRuntime = createHermesRuntime(); + facebook::jsi::Runtime *rt = hermesRuntime.get(); + auto *instance = [RCTTestArrayBufferTurboModule new]; + + ObjCTurboModule::InitParams params = { + .moduleName = "TestModule", + .instance = instance, + .jsInvoker = nullptr, + .nativeMethodCallInvoker = std::make_shared(), + .isSyncModule = false, + }; + ObjCTurboModule module(params); + + auto firstSourceBuffer = rt->global() + .getPropertyAsFunction(*rt, "eval") + .call(*rt, "new Uint8Array([1, 2, 3]).buffer") + .asObject(*rt) + .getArrayBuffer(*rt); + auto secondSourceBuffer = rt->global() + .getPropertyAsFunction(*rt, "eval") + .call(*rt, "new Uint8Array([4, 5]).buffer") + .asObject(*rt) + .getArrayBuffer(*rt); + auto jsArray = facebook::jsi::Array(*rt, 2); + jsArray.setValueAtIndex(*rt, 0, facebook::jsi::Value(*rt, firstSourceBuffer)); + jsArray.setValueAtIndex(*rt, 1, facebook::jsi::Value(*rt, secondSourceBuffer)); + facebook::jsi::Value args[1] = {facebook::jsi::Value(*rt, jsArray)}; + + module.invokeObjCMethod( + *rt, + NumberKind, + "testMethodWhichChecksArrayBufferArrayAliasing", + @selector(testMethodWhichChecksArrayBufferArrayAliasing:), + args, + 1); + + XCTAssertTrue(instance.sawAliasedBytes, @"Every element must alias its JS ArrayBuffer's bytes"); + XCTAssertTrue(instance.sawUnownedBytes, @"JS-heap elements in a sync call must not own their bytes"); + XCTAssertEqual( + bytesFromArrayBuffer(*rt, firstSourceBuffer)[0], 77, @"The native write must land on the first JS ArrayBuffer"); + XCTAssertEqual( + bytesFromArrayBuffer(*rt, secondSourceBuffer)[0], + 77, + @"The native write must land on the second JS ArrayBuffer"); +} + +// RCTArrayBuffer has no invalidate(). Unlike Android, retaining an element past a sync call does not +// throw — the wrapper still exposes the same pointer. Document the gap here rather than asserting +// Android-style post-return revocation. +- (void)testStoredJSBackedArrayBufferArrayElementsRemainAccessibleAfterSyncCallReturns +{ + auto hermesRuntime = createHermesRuntime(); + facebook::jsi::Runtime *rt = hermesRuntime.get(); + auto *instance = [RCTTestArrayBufferTurboModule new]; + + ObjCTurboModule::InitParams params = { + .moduleName = "TestModule", + .instance = instance, + .jsInvoker = nullptr, + .nativeMethodCallInvoker = std::make_shared(), + .isSyncModule = false, + }; + ObjCTurboModule module(params); + + auto jsArray = rt->global() + .getPropertyAsFunction(*rt, "eval") + .call(*rt, "[new Uint8Array([1, 2]).buffer, new Uint8Array([3]).buffer]") + .asObject(*rt) + .getArray(*rt); + facebook::jsi::Value args[1] = {facebook::jsi::Value(*rt, jsArray)}; + + module.invokeObjCMethod( + *rt, + VoidKind, + "testMethodWhichStoresArrayBufferArrayElements", + @selector(testMethodWhichStoresArrayBufferArrayElements:), + args, + 1); + + XCTAssertEqual(instance.storedArrayBufferArrayElements.count, 2u); + for (RCTArrayBuffer *buffer in instance.storedArrayBufferArrayElements) { + XCTAssertFalse(buffer.isOwningBytes); + XCTAssertNotNil(buffer.mutableBytes); + } +} + @end diff --git a/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js b/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js index f11aa856dc3b..48a1da11f1f4 100644 --- a/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js +++ b/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js @@ -54,6 +54,7 @@ type Examples = | 'createNativeBuffer' | 'processAsyncBuffer' | 'getAsyncBuffer' + | 'arrayBufferArray' | 'promise' | 'rejectPromise' | 'voidFunc' @@ -114,6 +115,14 @@ class SampleTurboModuleExample extends React.Component<{}, State> { NativeSampleTurboModule.getAsyncBuffer(4).then(buffer => this._setResult('getAsyncBuffer', Array.from(new Uint8Array(buffer))), ), + arrayBufferArray: () => { + const checksum = NativeSampleTurboModule.arrayBufferArray([ + new Uint8Array([1, 2, 3]).buffer, + new Uint8Array([4, 5]).buffer, + new ArrayBuffer(0), + ]); + return {checksum, isExpected: checksum === 15}; + }, getBool: () => NativeSampleTurboModule.getBool(true), getConstants: () => NativeSampleTurboModule.getConstants(), getEnum: () => diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 35a791e71a7a..5ed3eaf0a1d2 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -2512,6 +2512,7 @@ protocol NativeSampleTurboModuleSpec : public NSObjectRCTBridgeModule, public RC public virtual NSDictionary* getObjectThrows:(NSDictionary* arg); public virtual NSDictionary* getUnsafeObject:(NSDictionary* arg); public virtual NSDictionary* getValue:y:z:(double x, NSString* y, NSDictionary* z); + public virtual NSNumber* arrayBufferArray:(NSArray* values); public virtual NSNumber* getBool:(BOOL arg); public virtual NSNumber* getEnum:(double arg); public virtual NSNumber* getNumber:(double arg); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 27b7de33cafb..a684210fc5d1 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -2505,6 +2505,7 @@ protocol NativeSampleTurboModuleSpec : public NSObjectRCTBridgeModule, public RC public virtual NSDictionary* getObjectThrows:(NSDictionary* arg); public virtual NSDictionary* getUnsafeObject:(NSDictionary* arg); public virtual NSDictionary* getValue:y:z:(double x, NSString* y, NSDictionary* z); + public virtual NSNumber* arrayBufferArray:(NSArray* values); public virtual NSNumber* getBool:(BOOL arg); public virtual NSNumber* getEnum:(double arg); public virtual NSNumber* getNumber:(double arg); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index c6ad51282fe2..aba10ceba893 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -2512,6 +2512,7 @@ protocol NativeSampleTurboModuleSpec : public NSObjectRCTBridgeModule, public RC public virtual NSDictionary* getObjectThrows:(NSDictionary* arg); public virtual NSDictionary* getUnsafeObject:(NSDictionary* arg); public virtual NSDictionary* getValue:y:z:(double x, NSString* y, NSDictionary* z); + public virtual NSNumber* arrayBufferArray:(NSArray* values); public virtual NSNumber* getBool:(BOOL arg); public virtual NSNumber* getEnum:(double arg); public virtual NSNumber* getNumber:(double arg); From 9f5366592768991974345c348c6ee777295b46d6 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Thu, 20 Aug 2026 16:37:49 +0200 Subject: [PATCH 3/3] feat: accept ArrayBuffer and ArrayBufferView parts in Blob and File --- packages/react-native/Libraries/Blob/Blob.js | 6 +- .../Libraries/Blob/BlobManager.js | 89 ++++++---- .../react-native/Libraries/Blob/BlobTypes.js | 10 +- packages/react-native/Libraries/Blob/File.js | 8 +- .../Libraries/Blob/RCTBlobManager.h | 5 +- .../Libraries/Blob/RCTBlobManager.mm | 61 ++++++- .../Libraries/Blob/React-RCTBlob.podspec | 1 + .../Libraries/Blob/__mocks__/BlobModule.js | 9 +- .../Libraries/Blob/__tests__/Blob-test.js | 85 ++++++++++ .../Blob/__tests__/BlobManager-test.js | 158 +++++++++++++++++- .../Libraries/Blob/__tests__/File-test.js | 6 + .../ReactAndroid/api/ReactAndroid.api | 2 +- .../facebook/react/modules/blob/BlobModule.kt | 56 ++++--- .../react/modules/blob/BlobModuleTest.kt | 153 ++++++++++++++++- packages/react-native/ReactNativeApi.d.ts | 7 +- .../react-native/__typetests__/globals.tsx | 3 + .../modules/NativeBlobModule.js | 33 +++- packages/react-native/src/types/globals.d.ts | 2 +- .../RNTesterUnitTests/RCTBlobManagerTests.m | 69 +++++++- .../api-snapshots/ReactAndroidDebugCxx.api | 72 ++++++++ .../api-snapshots/ReactAndroidNewarchCxx.api | 60 +++++++ .../api-snapshots/ReactAndroidReleaseCxx.api | 60 +++++++ .../api-snapshots/ReactAppleDebugCxx.api | 92 +++++++++- .../api-snapshots/ReactAppleNewarchCxx.api | 80 ++++++++- .../api-snapshots/ReactAppleReleaseCxx.api | 80 ++++++++- 25 files changed, 1108 insertions(+), 99 deletions(-) diff --git a/packages/react-native/Libraries/Blob/Blob.js b/packages/react-native/Libraries/Blob/Blob.js index 90a4e5800813..92c321267578 100644 --- a/packages/react-native/Libraries/Blob/Blob.js +++ b/packages/react-native/Libraries/Blob/Blob.js @@ -10,7 +10,7 @@ 'use strict'; -import type {BlobData, BlobOptions} from './BlobTypes'; +import type {BlobData, BlobOptions, BlobPart} from './BlobTypes'; /** * Opaque JS representation of some binary data in native. @@ -54,10 +54,10 @@ class Blob { /** * Constructor for JS consumers. - * Currently we only support creating Blobs from other Blobs. + * Accepts `Blob`, string, `ArrayBuffer`, and `ArrayBufferView` parts. * Reference: https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob */ - constructor(parts: Array = [], options?: BlobOptions) { + constructor(parts: Array = [], options?: BlobOptions) { const BlobManager = require('./BlobManager').default; this.data = BlobManager.createFromParts(parts, options).data; } diff --git a/packages/react-native/Libraries/Blob/BlobManager.js b/packages/react-native/Libraries/Blob/BlobManager.js index f6b2b7bec7a1..8d395e3e6caa 100644 --- a/packages/react-native/Libraries/Blob/BlobManager.js +++ b/packages/react-native/Libraries/Blob/BlobManager.js @@ -9,7 +9,8 @@ */ import typeof BlobT from './Blob'; -import type {BlobCollector, BlobData, BlobOptions} from './BlobTypes'; +import type {BlobCollector, BlobData, BlobOptions, BlobPart} from './BlobTypes'; +import type {BlobPart as NativeBlobPart} from './NativeBlobModule'; import NativeBlobModule from './NativeBlobModule'; import invariant from 'invariant'; @@ -59,44 +60,66 @@ class BlobManager { /** * Create blob from existing array of blobs. */ - static createFromParts( - parts: Array, - options?: BlobOptions, - ): Blob { + static createFromParts(parts: Array, options?: BlobOptions): Blob { invariant(NativeBlobModule, 'NativeBlobModule is available.'); - const blobId = uuidv4(); - const items = parts.map(part => { - if (part instanceof ArrayBuffer || ArrayBuffer.isView(part)) { - throw new Error( - "Creating blobs from 'ArrayBuffer' and 'ArrayBufferView' are not supported", - ); - } + const binaryParts: Array = []; + let size = 0; + + const nativeParts: Array = []; + + for (const part of parts) { if (part instanceof Blob) { - return { - data: part.data, - type: 'blob', - }; - } else { - return { - data: String(part), - type: 'string', - }; + size += part.size; + nativeParts.push({type: 'blob', data: part.data}); + continue; } - }); - const size = items.reduce((acc, curr) => { - if (curr.type === 'string') { - /* $FlowFixMe[incompatible-type] Natural Inference rollout. See - * https://fburl.com/workplace/6291gfvu */ - return acc + global.unescape(encodeURI(curr.data)).length; - } else { - /* $FlowFixMe[prop-missing] Natural Inference rollout. See - * https://fburl.com/workplace/6291gfvu */ - return acc + curr.data.size; + + if ( + typeof part !== 'string' && + (part instanceof ArrayBuffer || ArrayBuffer.isView(part)) + ) { + const byteSize = part.byteLength; + size += byteSize; + + // A detached or empty buffer contributes no bytes, and `slice` throws on + // a detached buffer — so there is nothing to send. + if (byteSize === 0) { + continue; + } + + const index = binaryParts.length; + // Forwarded without copying here. A JS-heap `ArrayBuffer` is copied + // during argument conversion, because `createFromParts` is asynchronous + // — see `convertJSIArrayBufferToJArrayBuffer` and + // `convertJSIArrayBufferToRCTArrayBuffer`. A native-backed one is + // aliased instead, per the TurboModule zero-copy contract, so it is + // snapshotted only once the call reaches the module thread. + // + // A whole buffer therefore goes as-is; only a partial view is sliced, + // because the wire format carries whole buffers. + let source: ArrayBuffer; + if (part instanceof ArrayBuffer) { + source = part; + } else { + const buffer = part.buffer; + const byteOffset = part.byteOffset; + source = + byteOffset === 0 && byteSize === buffer.byteLength + ? buffer + : buffer.slice(byteOffset, byteOffset + byteSize); + } + binaryParts.push(source); + nativeParts.push({type: 'binaryPart', data: index}); + continue; } - }, 0); - NativeBlobModule.createFromParts(items, blobId); + const text = String(part); + size += global.unescape(encodeURI(text)).length; + nativeParts.push({type: 'string', data: text}); + } + + NativeBlobModule.createFromParts(nativeParts, binaryParts, blobId); return BlobManager.createFromOptions({ blobId, diff --git a/packages/react-native/Libraries/Blob/BlobTypes.js b/packages/react-native/Libraries/Blob/BlobTypes.js index 2c3e00eb06e4..da661d0473a0 100644 --- a/packages/react-native/Libraries/Blob/BlobTypes.js +++ b/packages/react-native/Libraries/Blob/BlobTypes.js @@ -4,14 +4,22 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow strict + * @flow strict-local * @format */ 'use strict'; +import type Blob from './Blob'; + export opaque type BlobCollector = {...}; +/** + * A value accepted by the Blob and File constructors (W3C `BlobPart`). + * https://w3c.github.io/FileAPI/#typedefdef-blobpart + */ +export type BlobPart = Blob | string | ArrayBuffer | $ArrayBufferView; + export type BlobData = { blobId: string, offset: number, diff --git a/packages/react-native/Libraries/Blob/File.js b/packages/react-native/Libraries/Blob/File.js index 0783d1c503b6..060e9b377fab 100644 --- a/packages/react-native/Libraries/Blob/File.js +++ b/packages/react-native/Libraries/Blob/File.js @@ -10,7 +10,7 @@ 'use strict'; -import type {BlobOptions} from './BlobTypes'; +import type {BlobOptions, BlobPart} from './BlobTypes'; import Blob from './Blob'; @@ -23,11 +23,7 @@ class File extends Blob { /** * Constructor for JS consumers. */ - constructor( - parts: Array, - name: string, - options?: BlobOptions, - ) { + constructor(parts: Array, name: string, options?: BlobOptions) { invariant( parts != null && name != null, 'Failed to construct `File`: Must pass both `parts` and `name` arguments.', diff --git a/packages/react-native/Libraries/Blob/RCTBlobManager.h b/packages/react-native/Libraries/Blob/RCTBlobManager.h index 59659b6ac9ce..b31a5337c1db 100755 --- a/packages/react-native/Libraries/Blob/RCTBlobManager.h +++ b/packages/react-native/Libraries/Blob/RCTBlobManager.h @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +#import #import #import #import @@ -26,6 +27,8 @@ - (void)remove:(NSString *)blobId; -- (void)createFromParts:(NSArray *> *)parts withId:(NSString *)blobId; +- (void)createFromParts:(NSArray *> *)parts + binaryParts:(NSArray *)binaryParts + withId:(NSString *)blobId; @end diff --git a/packages/react-native/Libraries/Blob/RCTBlobManager.mm b/packages/react-native/Libraries/Blob/RCTBlobManager.mm index 8a4b6740ea15..cd1edcd4e4d1 100755 --- a/packages/react-native/Libraries/Blob/RCTBlobManager.mm +++ b/packages/react-native/Libraries/Blob/RCTBlobManager.mm @@ -178,26 +178,75 @@ - (void)removeWebSocketHandler:(double)socketID }); } -// @lint-ignore FBOBJCUNTYPEDCOLLECTION1 -- (void)sendOverSocket:(NSDictionary *)blob socketID:(double)socketID +- (void)sendOverSocket:(JS::NativeBlobModule::BlobDescriptor &)blob socketID:(double)socketID { + NSString *blobId = blob.blobId(); + NSInteger offset = (NSInteger)blob.offset(); + NSInteger size = (NSInteger)blob.size(); + dispatch_async(((RCTWebSocketModule *)[_moduleRegistry moduleForName:"WebSocketModule"]).methodQueue, ^{ - [[self->_moduleRegistry moduleForName:"WebSocketModule"] sendData:[self resolve:blob] forSocketID:@(socketID)]; + [[self->_moduleRegistry moduleForName:"WebSocketModule"] sendData:[self resolve:blobId offset:offset size:size] + forSocketID:@(socketID)]; }); } -- (void)createFromParts:(NSArray *> *)parts withId:(NSString *)blobId +- (void)appendBlobRange:(NSDictionary *)blobDescriptor toData:(NSMutableData *)destination +{ + NSString *blobId = [RCTConvert NSString:blobDescriptor[@"blobId"]]; + NSInteger offset = [RCTConvert NSInteger:blobDescriptor[@"offset"]]; + NSInteger size = [RCTConvert NSInteger:blobDescriptor[@"size"]]; + + NSData *stored; + { + std::lock_guard lock(_blobsMutex); + stored = _blobs[blobId]; + } + + if (!stored) { + [NSException raise:@"Invalid blob ID" format:@"blob %@ not found", blobId]; + return; + } + + NSInteger length = size == -1 ? (NSInteger)stored.length - offset : size; + if (offset < 0 || length < 0 || offset + length > (NSInteger)stored.length) { + [NSException raise:@"Invalid blob range" + format:@"offset %ld, length %ld exceeds blob size %lu", + (long)offset, + (long)length, + (unsigned long)stored.length]; + return; + } + + [destination appendBytes:(const uint8_t *)stored.bytes + offset length:(NSUInteger)length]; +} + +- (void)createFromParts:(NSArray *> *)parts + binaryParts:(NSArray *)binaryParts + withId:(NSString *)blobId { NSMutableData *data = [NSMutableData new]; for (NSDictionary *part in parts) { NSString *type = [RCTConvert NSString:part[@"type"]]; if ([type isEqualToString:@"blob"]) { - NSData *partData = [self resolve:part[@"data"]]; - [data appendData:partData]; + [self appendBlobRange:part[@"data"] toData:data]; } else if ([type isEqualToString:@"string"]) { NSData *partData = [[RCTConvert NSString:part[@"data"]] dataUsingEncoding:NSUTF8StringEncoding]; [data appendData:partData]; + } else if ([type isEqualToString:@"binaryPart"]) { + NSInteger index = [RCTConvert NSInteger:part[@"data"]]; + if (index < 0 || index >= (NSInteger)binaryParts.count) { + [NSException raise:@"Invalid binary part index for blob" + format:@"%ld is out of range for %lu binary parts", (long)index, (unsigned long)binaryParts.count]; + } + RCTArrayBuffer *binaryPart = binaryParts[index]; + if (![binaryPart isKindOfClass:[RCTArrayBuffer class]]) { + [NSException raise:@"Invalid binary part for blob" + format:@"binary part %ld is %@, expected RCTArrayBuffer", (long)index, [binaryPart class]]; + } + if (binaryPart.length > 0) { + [data appendBytes:binaryPart.mutableBytes length:binaryPart.length]; + } } else { [NSException raise:@"Invalid type for blob" format:@"%@ is invalid", type]; } diff --git a/packages/react-native/Libraries/Blob/React-RCTBlob.podspec b/packages/react-native/Libraries/Blob/React-RCTBlob.podspec index 344e63726777..db6158299a1f 100644 --- a/packages/react-native/Libraries/Blob/React-RCTBlob.podspec +++ b/packages/react-native/Libraries/Blob/React-RCTBlob.podspec @@ -39,6 +39,7 @@ Pod::Spec.new do |s| "HEADER_SEARCH_PATHS" => header_search_paths.join(' ') } + s.dependency "RCTTypeSafety" s.dependency "React-jsi" s.dependency "React-Core/RCTBlobHeaders" s.dependency "React-Core/RCTWebSocket" diff --git a/packages/react-native/Libraries/Blob/__mocks__/BlobModule.js b/packages/react-native/Libraries/Blob/__mocks__/BlobModule.js index da54d38609d1..80981ed3d28f 100644 --- a/packages/react-native/Libraries/Blob/__mocks__/BlobModule.js +++ b/packages/react-native/Libraries/Blob/__mocks__/BlobModule.js @@ -8,9 +8,12 @@ * @format */ -const BlobModule = { - createFromParts() {}, - release() {}, +const BlobModule: { + createFromParts: JestMockFn<[Array<{...}>, Array, string], void>, + release: JestMockFn<[string], void>, +} = { + createFromParts: jest.fn(), + release: jest.fn(), }; export default BlobModule; diff --git a/packages/react-native/Libraries/Blob/__tests__/Blob-test.js b/packages/react-native/Libraries/Blob/__tests__/Blob-test.js index 3375467b4831..47b6d31c0ea5 100644 --- a/packages/react-native/Libraries/Blob/__tests__/Blob-test.js +++ b/packages/react-native/Libraries/Blob/__tests__/Blob-test.js @@ -17,9 +17,14 @@ jest.mock('../../BatchedBridge/NativeModules', () => ({ }, })); +const MockBlobModule = require('../__mocks__/BlobModule').default; const Blob = require('../Blob').default; describe('Blob', function () { + beforeEach(() => { + MockBlobModule.createFromParts.mockClear(); + }); + it('should create empty blob', () => { const blob = new Blob(); expect(blob).toBeInstanceOf(Blob); @@ -58,6 +63,86 @@ describe('Blob', function () { expect(blob.type).toBe(''); }); + it('should send array buffer and typed array parts as binary parts', () => { + const bytes = Uint8Array.from([10, 20, 30, 40]); + const blob = new Blob([bytes.buffer, bytes.subarray(1, 3)]); + + expect(blob.size).toBe(6); + + const [parts, binaryParts] = MockBlobModule.createFromParts.mock.calls[0]; + + expect(parts).toEqual([ + {type: 'binaryPart', data: 0}, + {type: 'binaryPart', data: 1}, + ]); + expect(binaryParts.map(b => Array.from(new Uint8Array(b)))).toEqual([ + [10, 20, 30, 40], + [20, 30], + ]); + }); + + it('should preserve part ordering across mixed types', () => { + const inner = new Blob(['D']); + MockBlobModule.createFromParts.mockClear(); + + const blob = new Blob(['A', Uint8Array.from([66, 67]), inner]); + expect(blob.size).toBe(4); + + const [parts, binaryParts] = MockBlobModule.createFromParts.mock.calls[0]; + + expect(parts).toEqual([ + {type: 'string', data: 'A'}, + {type: 'binaryPart', data: 0}, + {type: 'blob', data: inner.data}, + ]); + expect(binaryParts.map(b => Array.from(new Uint8Array(b)))).toEqual([ + [66, 67], + ]); + }); + + it('should handle empty array buffer parts', () => { + expect(new Blob([new ArrayBuffer(0)]).size).toBe(0); + }); + + it('should count Float64Array and DataView parts in bytes', () => { + const f64 = new Float64Array([1.5, 2.5, 3.5]); + expect(new Blob([f64]).size).toBe(24); + expect(new Blob([new DataView(f64.buffer, 8, 8)]).size).toBe(8); + }); + + it('should treat a detached ArrayBuffer as an empty blob part', () => { + const ab = new ArrayBuffer(8); + // $FlowFixMe[cannot-resolve-name] Node's structuredClone is not in RN's Flow libs. + structuredClone(ab, {transfer: [ab]}); + const blob = new Blob([ab]); + expect(blob.size).toBe(0); + }); + + it('should treat a detached ArrayBufferView as an empty blob part', () => { + const ab = new ArrayBuffer(8); + const view = new Uint8Array(ab, 2, 4); + // $FlowFixMe[cannot-resolve-name] Node's structuredClone is not in RN's Flow libs. + structuredClone(ab, {transfer: [ab]}); + const blob = new Blob([view]); + expect(blob.size).toBe(0); + }); + + it('stringifies parts that are neither Blob nor BufferSource (W3C: USVString)', () => { + // $FlowExpectedError[incompatible-type] + expect(new Blob([42]).size).toBe(2); + // $FlowExpectedError[incompatible-type] + expect(new Blob([null]).size).toBe(4); + // $FlowExpectedError[incompatible-type] + expect(new Blob([undefined]).size).toBe(9); + // $FlowExpectedError[incompatible-type] + expect(new Blob([{}]).size).toBe(15); + // $FlowExpectedError[incompatible-type] + expect(new Blob([new String('abc')]).size).toBe(3); // eslint-disable-line no-new-wrappers + + const [parts] = MockBlobModule.createFromParts.mock.calls[0]; + expect(parts).toEqual([{type: 'string', data: '42'}]); + }); + it('should slice a blob', () => { const blob = new Blob(); diff --git a/packages/react-native/Libraries/Blob/__tests__/BlobManager-test.js b/packages/react-native/Libraries/Blob/__tests__/BlobManager-test.js index fdb70b6ff4f5..4cdd44069ccd 100644 --- a/packages/react-native/Libraries/Blob/__tests__/BlobManager-test.js +++ b/packages/react-native/Libraries/Blob/__tests__/BlobManager-test.js @@ -17,10 +17,19 @@ jest.mock('../../BatchedBridge/NativeModules', () => ({ }, })); -const Blob = require('../Blob').default; -const BlobManager = require('../BlobManager').default; - describe('BlobManager', function () { + let Blob; + let BlobManager; + let MockBlobModule; + + beforeEach(() => { + jest.resetModules(); + Blob = require('../Blob').default; + BlobManager = require('../BlobManager').default; + MockBlobModule = require('../__mocks__/BlobModule').default; + MockBlobModule.createFromParts.mockClear(); + }); + it('should create blob from parts', () => { const blob = BlobManager.createFromParts([], { lastModified: 0, @@ -29,4 +38,147 @@ describe('BlobManager', function () { expect(blob).toBeInstanceOf(Blob); expect(blob.type).toBe('text/html'); }); + + it('should pass ArrayBuffer parts as binaryParts to createFromParts', () => { + const bytes = Uint8Array.from([1, 2, 3, 4]); + const buffer = bytes.buffer; + + const blob = BlobManager.createFromParts([buffer]); + + expect(blob.size).toBe(4); + + expect(MockBlobModule.createFromParts).toHaveBeenCalledTimes(1); + const [parts, binaryParts, blobId] = + MockBlobModule.createFromParts.mock.calls[0]; + + expect(binaryParts).toHaveLength(1); + // Forwarded uncopied; a JS-heap buffer is copied during argument conversion. + expect(binaryParts[0]).toBe(buffer); + expect(Array.from(new Uint8Array(binaryParts[0]))).toEqual([1, 2, 3, 4]); + + expect(parts).toHaveLength(1); + expect(parts[0]).toEqual({ + type: 'binaryPart', + data: 0, + }); + + expect(blob.data.blobId).toBe(blobId); + }); + + it('should forward a full-length ArrayBufferView without copying', () => { + const bytes = Uint8Array.from([1, 2, 3, 4]); + + const blob = BlobManager.createFromParts([bytes]); + + expect(blob.size).toBe(4); + + expect(MockBlobModule.createFromParts).toHaveBeenCalledTimes(1); + const [, binaryParts] = MockBlobModule.createFromParts.mock.calls[0]; + + expect(binaryParts).toHaveLength(1); + expect(binaryParts[0]).toBe(bytes.buffer); + expect(Array.from(new Uint8Array(binaryParts[0]))).toEqual([1, 2, 3, 4]); + }); + + // Whether mutating the source after createFromParts changes the forwarded + // bytes is decided in C++ (convertJSIArrayBufferToJArrayBuffer and + // convertJSIArrayBufferToRCTArrayBuffer): a JS-heap buffer is copied on this + // asynchronous call, a native-backed one is aliased per the zero-copy + // contract. Neither is observable from Jest. + + it('should preserve ArrayBufferView offset and size when storing binary parts', () => { + const bytes = Uint8Array.from([9, 8, 7, 6, 5]); + const view = bytes.subarray(1, 4); + + const blob = BlobManager.createFromParts([view]); + + expect(blob.size).toBe(3); + + expect(MockBlobModule.createFromParts).toHaveBeenCalledTimes(1); + const [parts, binaryParts] = MockBlobModule.createFromParts.mock.calls[0]; + + expect(binaryParts).toHaveLength(1); + // Partial views must be sliced; native receives whole buffers only. + expect(binaryParts[0]).not.toBe(bytes.buffer); + expect(Array.from(new Uint8Array(binaryParts[0]))).toEqual([8, 7, 6]); + + expect(parts[0]).toEqual({ + type: 'binaryPart', + data: 0, + }); + }); + + it('should store each binary part as a separate entry in binaryParts', () => { + const blob = BlobManager.createFromParts([ + 'A', + Uint8Array.from([66, 67]).buffer, + ]); + + expect(blob.size).toBe(3); + + expect(MockBlobModule.createFromParts).toHaveBeenCalledTimes(1); + const [parts, binaryParts] = MockBlobModule.createFromParts.mock.calls[0]; + + // One binary part for the ArrayBuffer, none for the string + expect(binaryParts).toHaveLength(1); + expect(binaryParts[0].byteLength).toBe(2); + + expect(parts).toHaveLength(2); + expect(parts[0]).toEqual({type: 'string', data: 'A'}); + expect(parts[1]).toEqual({type: 'binaryPart', data: 0}); + }); + + it('should use native createFromParts when parts include blobs', () => { + const binaryBlob = BlobManager.createFromParts([ + Uint8Array.from([1, 2, 3]).buffer, + ]); + + MockBlobModule.createFromParts.mockClear(); + + const blob = BlobManager.createFromParts([binaryBlob, 'A']); + + expect(blob.size).toBe(4); + expect(MockBlobModule.createFromParts).toHaveBeenCalledTimes(1); + const [parts, binaryParts] = MockBlobModule.createFromParts.mock.calls[0]; + + expect(binaryParts).toEqual([]); + expect(parts).toHaveLength(2); + expect(parts[0]).toMatchObject({ + type: 'blob', + data: binaryBlob.data, + }); + expect(parts[1]).toEqual({ + data: 'A', + type: 'string', + }); + }); + + it('should omit empty ArrayBuffer parts', () => { + const blob = BlobManager.createFromParts([ + new ArrayBuffer(0), + Uint8Array.from([1, 2]), + ]); + + expect(blob.size).toBe(2); + expect(MockBlobModule.createFromParts).toHaveBeenCalledTimes(1); + const [parts, binaryParts] = MockBlobModule.createFromParts.mock.calls[0]; + + expect(binaryParts).toHaveLength(1); + expect(parts).toEqual([{type: 'binaryPart', data: 0}]); + }); + + it('should omit a detached ArrayBuffer part', () => { + const ab = new ArrayBuffer(8); + // $FlowFixMe[cannot-resolve-name] Node's structuredClone is not in RN's Flow libs. + structuredClone(ab, {transfer: [ab]}); + + const blob = BlobManager.createFromParts([ab]); + + expect(blob.size).toBe(0); + expect(MockBlobModule.createFromParts).toHaveBeenCalledTimes(1); + const [parts, binaryParts] = MockBlobModule.createFromParts.mock.calls[0]; + + expect(binaryParts).toEqual([]); + expect(parts).toEqual([]); + }); }); diff --git a/packages/react-native/Libraries/Blob/__tests__/File-test.js b/packages/react-native/Libraries/Blob/__tests__/File-test.js index 20d8f2a75f63..fdc238a3053c 100644 --- a/packages/react-native/Libraries/Blob/__tests__/File-test.js +++ b/packages/react-native/Libraries/Blob/__tests__/File-test.js @@ -86,4 +86,10 @@ describe('File', function () { // $FlowExpectedError[incompatible-type] expect(() => new File([])).toThrow(); }); + + it('should create file from array buffer parts', () => { + const file = new File([new Uint8Array([1, 2, 3]).buffer], 'a.bin'); + expect(file.size).toBe(3); + expect(file.name).toBe('a.bin'); + }); }); diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index a78d8fbd869e..bf466e05ff7a 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -2500,7 +2500,7 @@ public final class com/facebook/react/modules/blob/BlobModule : com/facebook/fbr public fun (Lcom/facebook/react/bridge/ReactApplicationContext;)V public fun addNetworkingHandler ()V public fun addWebSocketHandler (D)V - public fun createFromParts (Lcom/facebook/react/bridge/ReadableArray;Ljava/lang/String;)V + public fun createFromParts (Lcom/facebook/react/bridge/ReadableArray;[Lcom/facebook/react/bridge/ArrayBuffer;Ljava/lang/String;)V public fun getBindingsInstaller ()Lcom/facebook/react/turbomodule/core/interfaces/BindingsInstallerHolder; public final fun getLengthOfBlob (Ljava/lang/String;)J public fun getTypedExportedConstants ()Ljava/util/Map; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobModule.kt index abed9481bb7e..5474b51d9e06 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobModule.kt @@ -15,6 +15,7 @@ import android.webkit.MimeTypeMap import com.facebook.fbreact.specs.NativeBlobModuleSpec import com.facebook.proguard.annotations.DoNotStrip import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ArrayBuffer import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap @@ -31,10 +32,6 @@ import java.io.File import java.io.FileNotFoundException import java.io.IOException import java.nio.ByteBuffer -import java.nio.charset.Charset -import java.util.ArrayList -import java.util.Arrays -import java.util.HashMap import java.util.UUID import okhttp3.MediaType import okhttp3.RequestBody @@ -190,7 +187,7 @@ public class BlobModule(reactContext: ReactApplicationContext) : newSize = data.size - offset } if (offset > 0 || newSize != data.size) { - return Arrays.copyOfRange(data, offset, offset + newSize) + return data.copyOfRange(offset, offset + newSize) } return data } @@ -295,33 +292,52 @@ public class BlobModule(reactContext: ReactApplicationContext) : } } - public override fun createFromParts(parts: ReadableArray, blobId: String) { - var totalBlobSize = 0 - val partList = ArrayList(parts.size()) + /** A read-only window onto stored blob bytes, without copying them. */ + private fun wrapBlob(blobId: String?, offset: Int, size: Int): ByteBuffer { + synchronized(blobs) { + val data = checkNotNull(blobs[blobId]) { "Invalid blob: $blobId" } + val length = if (size == -1) data.size - offset else size + return ByteBuffer.wrap(data, offset, length) + } + } + + public override fun createFromParts( + parts: ReadableArray, + binaryParts: Array, + blobId: String, + ) { + val chunks = ArrayList(parts.size()) for (i in 0 until parts.size()) { val part = checkNotNull(parts.getMap(i)) - val type = checkNotNull(part.getString("type")) - when (type) { + when (val type = checkNotNull(part.getString("type"))) { "blob" -> { val blob = checkNotNull(part.getMap("data")) - totalBlobSize += blob.getInt("size") - partList.add(i, checkNotNull(resolve(blob))) + chunks.add( + wrapBlob( + blob.getString("blobId"), + blob.getInt("offset"), + blob.getInt("size"), + ) + ) } "string" -> { val data = checkNotNull(part.getString("data")) - val bytes = data.toByteArray(Charset.forName("UTF-8")) - totalBlobSize += bytes.size - partList.add(i, bytes) + chunks.add(ByteBuffer.wrap(data.toByteArray(Charsets.UTF_8))) + } + "binaryPart" -> { + val index = part.getInt("data") + require(index in binaryParts.indices) { + "Invalid binaryPart index $index for blob: ${binaryParts.size} binary parts provided" + } + chunks.add(binaryParts[index].bytes.duplicate()) } - else -> throw IllegalArgumentException("Invalid type for blob: ${part.getString("type")}") + else -> throw IllegalArgumentException("Invalid type for blob: $type") } } - val buffer = ByteBuffer.allocate(totalBlobSize) - for (bytes in partList) { - buffer.put(bytes) - } + val buffer = ByteBuffer.allocate(chunks.sumOf { it.remaining() }) + chunks.forEach { buffer.put(it) } store(buffer.array(), blobId) } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/blob/BlobModuleTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/blob/BlobModuleTest.kt index b64f1595c5be..1053ddaf8092 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/blob/BlobModuleTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/blob/BlobModuleTest.kt @@ -8,17 +8,22 @@ package com.facebook.react.modules.blob import android.net.Uri +import com.facebook.react.bridge.ArrayBuffer import com.facebook.react.bridge.JavaOnlyArray import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactTestHelper import com.facebook.soloader.SoLoader import com.facebook.testutils.shadows.ShadowArguments +import com.facebook.testutils.shadows.ShadowArrayBuffer +import com.facebook.testutils.shadows.ShadowNativeLoader +import com.facebook.testutils.shadows.ShadowSoLoader import java.io.ByteArrayInputStream import java.nio.ByteBuffer import java.util.UUID import kotlin.random.Random import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.After import org.junit.Before import org.junit.Test @@ -30,7 +35,16 @@ import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) -@Config(manifest = Config.NONE, shadows = [ShadowArguments::class]) +@Config( + manifest = Config.NONE, + shadows = + [ + ShadowArguments::class, + ShadowArrayBuffer::class, + ShadowSoLoader::class, + ShadowNativeLoader::class, + ], +) class BlobModuleTest { private lateinit var bytes: ByteArray private lateinit var blobId: String @@ -66,6 +80,24 @@ class BlobModuleTest { assertThat(blobModule.resolve(blobId, 30, bytes.size - 30)).isEqualTo(expectedRange) } + @Test + fun testCreateFromPartsBinaryPartIsCopiedNotAliased() { + val id = UUID.randomUUID().toString() + val binaryData = byteArrayOf(1, 2, 3, 4) + val source = ArrayBuffer.arrayBufferWithCopiedBytes(binaryData) + + val binaryPart = + JavaOnlyMap().apply { + putInt("data", 0) + putString("type", "binaryPart") + } + val parts = JavaOnlyArray().apply { pushMap(binaryPart) } + blobModule.createFromParts(parts, arrayOf(source), id) + + source.bytes.put(0, (binaryData[0] + 1).toByte()) + assertThat(blobModule.resolve(id, 0, binaryData.size)).isEqualTo(binaryData) + } + @Test fun testResolveUri() { val uri = @@ -129,10 +161,9 @@ class BlobModuleTest { pushMap(string) } - blobModule.createFromParts(parts, id) + blobModule.createFromParts(parts, arrayOf(), id) val resultSize = bytes.size + stringBytes.size - val result = blobModule.resolve(id, 0, resultSize) val buffer = @@ -144,6 +175,122 @@ class BlobModuleTest { assertThat(result).isEqualTo(buffer.array()) } + @Test + fun testCreateFromPartsWithBinaryPart() { + val id = UUID.randomUUID().toString() + val binaryData = byteArrayOf(1, 2, 3, 4) + val buffer = ArrayBuffer.arrayBufferWithCopiedBytes(binaryData) + + val binaryPart = + JavaOnlyMap().apply { + putInt("data", 0) + putString("type", "binaryPart") + } + + val parts = JavaOnlyArray().apply { pushMap(binaryPart) } + + blobModule.createFromParts(parts, arrayOf(buffer), id) + + assertThat(blobModule.resolve(id, 0, 4)).isEqualTo(binaryData) + } + + @Test + fun testCreateFromPartsOrdersMixedParts() { + val id = UUID.randomUUID().toString() + val binaryData = byteArrayOf(66, 67) + val buffer = ArrayBuffer.arrayBufferWithCopiedBytes(binaryData) + + val stringPart = + JavaOnlyMap().apply { + putString("data", "A") + putString("type", "string") + } + val binaryPart = + JavaOnlyMap().apply { + putInt("data", 0) + putString("type", "binaryPart") + } + val blobData = + JavaOnlyMap().apply { + putString("blobId", blobId) + putInt("offset", 0) + putInt("size", bytes.size) + } + val blobPart = + JavaOnlyMap().apply { + putMap("data", blobData) + putString("type", "blob") + } + + val parts = + JavaOnlyArray().apply { + pushMap(stringPart) + pushMap(binaryPart) + pushMap(blobPart) + } + + blobModule.createFromParts(parts, arrayOf(buffer), id) + + val expected = + ByteBuffer.allocate(1 + binaryData.size + bytes.size) + .apply { + put("A".encodeToByteArray()) + put(binaryData) + put(bytes) + } + .array() + + assertThat(blobModule.resolve(id, 0, expected.size)).isEqualTo(expected) + } + + @Test + fun testCreateFromPartsWithMultipleBinaryParts() { + val id = UUID.randomUUID().toString() + val first = byteArrayOf(10, 20) + val second = byteArrayOf(30, 40) + val buffers = + arrayOf( + ArrayBuffer.arrayBufferWithCopiedBytes(first), + ArrayBuffer.arrayBufferWithCopiedBytes(second), + ) + + // parts reference data: 1 then data: 0 — output must follow parts order. + val part1 = + JavaOnlyMap().apply { + putInt("data", 1) + putString("type", "binaryPart") + } + val part0 = + JavaOnlyMap().apply { + putInt("data", 0) + putString("type", "binaryPart") + } + val parts = + JavaOnlyArray().apply { + pushMap(part1) + pushMap(part0) + } + + blobModule.createFromParts(parts, buffers, id) + + assertThat(blobModule.resolve(id, 0, 4)).isEqualTo(byteArrayOf(30, 40, 10, 20)) + } + + @Test + fun testCreateFromPartsRejectsOutOfRangeBinaryPartIndex() { + val part = + JavaOnlyMap().apply { + putInt("data", 3) + putString("type", "binaryPart") + } + val parts = JavaOnlyArray().apply { pushMap(part) } + + assertThatThrownBy { + blobModule.createFromParts(parts, arrayOf(), UUID.randomUUID().toString()) + } + .isInstanceOf(IllegalArgumentException::class.java) + } + @Test fun testRelease() { assertThat(blobModule.resolve(blobId, 0, bytes.size)).isNotNull() diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index c1ac9628efe7..8b5582c46b58 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<2199d280ef42e2a63ec4d6c405af893a>> + * @generated SignedSource<> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -1628,7 +1628,7 @@ declare function beginAsyncEvent(eventName: EventName, args?: EventArgs): number declare function beginEvent(eventName: EventName, args?: EventArgs): void declare class Blob_default { close(): void - constructor(parts?: Array, options?: BlobOptions) + constructor(parts?: Array, options?: BlobOptions) set data(data: BlobData | null | undefined) get data(): BlobData get size(): number @@ -1647,6 +1647,7 @@ declare type BlobOptions = { lastModified: number type: string } +declare type BlobPart = ArrayBuffer | ArrayBufferView | Blob_default | string declare type BlurEvent = NativeSyntheticEvent declare type BoxShadowValue = { blurRadius?: number | string @@ -5901,7 +5902,7 @@ export { NativeSyntheticEvent, // 534aaa92 NativeTouchEvent, // 59b676df NativeUIEvent, // 44ac26ac - Networking, // bbc5be42 + Networking, // e7b06e57 OpaqueColorValue, // 25f3fa5b PackagerAsset, // d1c88cf4 PanResponder, // e4df325a diff --git a/packages/react-native/__typetests__/globals.tsx b/packages/react-native/__typetests__/globals.tsx index 66dc216176dc..bb5fa79dc727 100644 --- a/packages/react-native/__typetests__/globals.tsx +++ b/packages/react-native/__typetests__/globals.tsx @@ -209,6 +209,9 @@ const blobA = new Blob(); const textA = 'i \u2665 dogs'; const blob = new Blob([blobA, textA]); +const blobFromArrayBuffer = new Blob([new ArrayBuffer(8)]); +const blobFromTypedArray = new Blob([new Uint8Array([1, 2, 3])]); +const blobFromMixed = new Blob(['text', new ArrayBuffer(4), new Blob([])]); const reader = new FileReader(); diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeBlobModule.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeBlobModule.js index 929a1cf098d2..900d4a4f4743 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeBlobModule.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeBlobModule.js @@ -14,13 +14,32 @@ import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboMod export type Constants = {BLOB_URI_SCHEME: ?string, BLOB_URI_HOST: ?string}; +type BlobDescriptor = { + blobId: string, + offset: number, + size: number, + name?: string, + type?: string, + lastModified?: number, + ... +}; + +type BlobStringPart = {type: 'string', data: string, ...}; +type BlobReferencePart = {type: 'blob', data: BlobDescriptor, ...}; +type BlobBinaryPart = {type: 'binaryPart', data: number, ...}; +export type BlobPart = BlobReferencePart | BlobStringPart | BlobBinaryPart; + export interface Spec extends TurboModule { readonly getConstants: () => Constants; readonly addNetworkingHandler: () => void; readonly addWebSocketHandler: (id: number) => void; readonly removeWebSocketHandler: (id: number) => void; - readonly sendOverSocket: (blob: Object, socketID: number) => void; - readonly createFromParts: (parts: Array, withId: string) => void; + readonly sendOverSocket: (blob: BlobDescriptor, socketID: number) => void; + readonly createFromParts: ( + parts: ReadonlyArray, + binaryParts: ReadonlyArray, + withId: string, + ) => void; readonly release: (blobId: string) => void; } @@ -46,11 +65,15 @@ if (NativeModule != null) { removeWebSocketHandler(id: number): void { NativeModule.removeWebSocketHandler(id); }, - sendOverSocket(blob: Object, socketID: number): void { + sendOverSocket(blob: BlobDescriptor, socketID: number): void { NativeModule.sendOverSocket(blob, socketID); }, - createFromParts(parts: Array, withId: string): void { - NativeModule.createFromParts(parts, withId); + createFromParts( + parts: ReadonlyArray, + binaryParts: ReadonlyArray, + withId: string, + ): void { + NativeModule.createFromParts(parts, binaryParts, withId); }, release(blobId: string): void { NativeModule.release(blobId); diff --git a/packages/react-native/src/types/globals.d.ts b/packages/react-native/src/types/globals.d.ts index f9820c6fc5b4..03951003e153 100644 --- a/packages/react-native/src/types/globals.d.ts +++ b/packages/react-native/src/types/globals.d.ts @@ -174,7 +174,7 @@ declare global { var Blob: { prototype: Blob; - new (blobParts?: Array, options?: BlobOptions): Blob; + new (blobParts?: BlobPart[], options?: BlobOptions): Blob; }; interface FilePropertyBag extends BlobPropertyBag { diff --git a/packages/rn-tester/RNTesterUnitTests/RCTBlobManagerTests.m b/packages/rn-tester/RNTesterUnitTests/RCTBlobManagerTests.m index 4754b1e3bea5..daf171b09637 100644 --- a/packages/rn-tester/RNTesterUnitTests/RCTBlobManagerTests.m +++ b/packages/rn-tester/RNTesterUnitTests/RCTBlobManagerTests.m @@ -7,6 +7,7 @@ #import +#import #import #import @@ -99,7 +100,7 @@ - (void)testCreateFromParts NSString *resultId = [NSUUID UUID].UUIDString; NSArray *parts = @[ blob, string ]; - [_module createFromParts:parts withId:resultId]; + [_module createFromParts:parts binaryParts:@[] withId:resultId]; NSMutableData *expectedData = [NSMutableData new]; [expectedData appendData:_data]; @@ -112,4 +113,70 @@ - (void)testCreateFromParts RCT_MOCK_RESET(RCTBlobManager, dispatch_async); } +- (void)testCreateFromPartsWithBinaryParts +{ + RCT_MOCK_SET(RCTBlobManager, dispatch_async, dispatch_async_mock); + + NSString *stringData = @"A"; + NSDictionary *string = @{ + @"data" : stringData, + @"type" : @"string", + }; + + uint8_t binaryBytes[] = {66, 67}; + NSData *binaryData = [NSData dataWithBytes:binaryBytes length:sizeof(binaryBytes)]; + RCTArrayBuffer *binaryBuffer = [RCTArrayBuffer arrayBufferWithCopiedBytes:binaryData.bytes length:binaryData.length]; + NSDictionary *binaryPart = @{ + @"data" : @0, + @"type" : @"binaryPart", + }; + + NSDictionary *blobData = @{ + @"blobId" : _blobId, + @"offset" : @0, + @"size" : @(_data.length), + }; + NSDictionary *blob = @{ + @"data" : blobData, + @"type" : @"blob", + }; + + NSString *resultId = [NSUUID UUID].UUIDString; + NSArray *parts = @[ string, binaryPart, blob ]; + + [_module createFromParts:parts binaryParts:@[ binaryBuffer ] withId:resultId]; + + NSMutableData *expectedData = [NSMutableData new]; + [expectedData appendData:[stringData dataUsingEncoding:NSUTF8StringEncoding]]; + [expectedData appendData:binaryData]; + [expectedData appendData:_data]; + + NSData *result = [_module resolve:resultId offset:0 size:expectedData.length]; + XCTAssertTrue([expectedData isEqualToData:result]); + + RCT_MOCK_RESET(RCTBlobManager, dispatch_async); +} + +- (void)testCreateFromPartsWithOutOfRangeBinaryPartIndex +{ + NSDictionary *binaryPart = @{ + @"data" : @3, + @"type" : @"binaryPart", + }; + NSString *resultId = [NSUUID UUID].UUIDString; + + XCTAssertThrows([_module createFromParts:@[ binaryPart ] binaryParts:@[] withId:resultId]); +} + +- (void)testCreateFromPartsWithInvalidBinaryPartType +{ + NSDictionary *binaryPart = @{ + @"data" : @0, + @"type" : @"binaryPart", + }; + NSString *resultId = [NSUUID UUID].UUIDString; + + XCTAssertThrows([_module createFromParts:@[ binaryPart ] binaryParts:@[ [NSNull null] ] withId:resultId]); +} + @end diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 8aa400d9f494..0e17d2cced7d 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -8556,6 +8556,17 @@ struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntr public bool operator==(const facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntry& other) const; } +template +struct facebook::react::NativeBlobModuleBlobDescriptor { + public P0 blobId; + public P1 offset; + public P2 size; + public P3 name; + public P4 type; + public P5 lastModified; + public bool operator==(const facebook::react::NativeBlobModuleBlobDescriptor& other) const; +} + template struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverObserveOptions { public P0 intersectionObserverId; @@ -8723,6 +8734,27 @@ struct facebook::react::NativeAnimatedTurboModuleEventMapping { public bool operator==(const facebook::react::NativeAnimatedTurboModuleEventMapping& other) const; } +template +struct facebook::react::NativeBlobModuleBlobBinaryPart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobBinaryPart& other) const; +} + +template +struct facebook::react::NativeBlobModuleBlobReferencePart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobReferencePart& other) const; +} + +template +struct facebook::react::NativeBlobModuleBlobStringPart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobStringPart& other) const; +} + template struct facebook::react::NativeBlobModuleConstants { public P0 BLOB_URI_SCHEME; @@ -9333,6 +9365,46 @@ struct facebook::react::NativeAppStateAppStateConstantsBridging { public static facebook::jsi::String initialAppStateToJs(facebook::jsi::Runtime& rt, decltype(types.initialAppState) value); } +template +struct facebook::react::NativeBlobModuleBlobBinaryPartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static double dataToJs(facebook::jsi::Runtime& rt, decltype(types.data) value); + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); + public static facebook::jsi::String typeToJs(facebook::jsi::Runtime& rt, decltype(types.type) value); +} + +template +struct facebook::react::NativeBlobModuleBlobDescriptorBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static double lastModifiedToJs(facebook::jsi::Runtime& rt, decltype(types.lastModified) value); + public static double offsetToJs(facebook::jsi::Runtime& rt, decltype(types.offset) value); + public static double sizeToJs(facebook::jsi::Runtime& rt, decltype(types.size) value); + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); + public static facebook::jsi::String blobIdToJs(facebook::jsi::Runtime& rt, decltype(types.blobId) value); + public static facebook::jsi::String nameToJs(facebook::jsi::Runtime& rt, decltype(types.name) value); + public static facebook::jsi::String typeToJs(facebook::jsi::Runtime& rt, decltype(types.type) value); +} + +template +struct facebook::react::NativeBlobModuleBlobReferencePartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object dataToJs(facebook::jsi::Runtime& rt, decltype(types.data) value); + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); + public static facebook::jsi::String typeToJs(facebook::jsi::Runtime& rt, decltype(types.type) value); +} + +template +struct facebook::react::NativeBlobModuleBlobStringPartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); + public static facebook::jsi::String dataToJs(facebook::jsi::Runtime& rt, decltype(types.data) value); + public static facebook::jsi::String typeToJs(facebook::jsi::Runtime& rt, decltype(types.type) value); +} + template struct facebook::react::NativeBlobModuleConstantsBridging { public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index f6c80cc91a20..88b3b313300f 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -8316,6 +8316,17 @@ struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntr public bool operator==(const facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntry& other) const; } +template +struct facebook::react::NativeBlobModuleBlobDescriptor { + public P0 blobId; + public P1 offset; + public P2 size; + public P3 name; + public P4 type; + public P5 lastModified; + public bool operator==(const facebook::react::NativeBlobModuleBlobDescriptor& other) const; +} + template struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverObserveOptions { public P0 intersectionObserverId; @@ -8483,6 +8494,27 @@ struct facebook::react::NativeAnimatedTurboModuleEventMapping { public bool operator==(const facebook::react::NativeAnimatedTurboModuleEventMapping& other) const; } +template +struct facebook::react::NativeBlobModuleBlobBinaryPart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobBinaryPart& other) const; +} + +template +struct facebook::react::NativeBlobModuleBlobReferencePart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobReferencePart& other) const; +} + +template +struct facebook::react::NativeBlobModuleBlobStringPart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobStringPart& other) const; +} + template struct facebook::react::NativeBlobModuleConstants { public P0 BLOB_URI_SCHEME; @@ -9071,6 +9103,34 @@ struct facebook::react::NativeAppStateAppStateConstantsBridging { public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); } +template +struct facebook::react::NativeBlobModuleBlobBinaryPartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); +} + +template +struct facebook::react::NativeBlobModuleBlobDescriptorBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); +} + +template +struct facebook::react::NativeBlobModuleBlobReferencePartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); +} + +template +struct facebook::react::NativeBlobModuleBlobStringPartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); +} + template struct facebook::react::NativeBlobModuleConstantsBridging { public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 61b12b3fe21f..4bfa3d6ca78e 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -8547,6 +8547,17 @@ struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntr public bool operator==(const facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntry& other) const; } +template +struct facebook::react::NativeBlobModuleBlobDescriptor { + public P0 blobId; + public P1 offset; + public P2 size; + public P3 name; + public P4 type; + public P5 lastModified; + public bool operator==(const facebook::react::NativeBlobModuleBlobDescriptor& other) const; +} + template struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverObserveOptions { public P0 intersectionObserverId; @@ -8714,6 +8725,27 @@ struct facebook::react::NativeAnimatedTurboModuleEventMapping { public bool operator==(const facebook::react::NativeAnimatedTurboModuleEventMapping& other) const; } +template +struct facebook::react::NativeBlobModuleBlobBinaryPart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobBinaryPart& other) const; +} + +template +struct facebook::react::NativeBlobModuleBlobReferencePart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobReferencePart& other) const; +} + +template +struct facebook::react::NativeBlobModuleBlobStringPart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobStringPart& other) const; +} + template struct facebook::react::NativeBlobModuleConstants { public P0 BLOB_URI_SCHEME; @@ -9302,6 +9334,34 @@ struct facebook::react::NativeAppStateAppStateConstantsBridging { public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); } +template +struct facebook::react::NativeBlobModuleBlobBinaryPartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); +} + +template +struct facebook::react::NativeBlobModuleBlobDescriptorBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); +} + +template +struct facebook::react::NativeBlobModuleBlobReferencePartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); +} + +template +struct facebook::react::NativeBlobModuleBlobStringPartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); +} + template struct facebook::react::NativeBlobModuleConstantsBridging { public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 5ed3eaf0a1d2..3b9be9352f8e 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -173,6 +173,10 @@ category RCTCxxConvert(NativeAnimatedTurboModule_EventMapping) { public virtual static RCTManagedPointer* JS_NativeAnimatedTurboModule_EventMapping:(id json); } +category RCTCxxConvert(NativeBlobModule_BlobDescriptor) { + public virtual static RCTManagedPointer* JS_NativeBlobModule_BlobDescriptor:(id json); +} + category RCTCxxConvert(NativeExceptionsManager_ExceptionData) { public virtual static RCTManagedPointer* JS_NativeExceptionsManager_ExceptionData:(id json); } @@ -686,7 +690,7 @@ interface RCTBlobManager : public NSObject *>* parts, NSString* blobId); + public virtual void createFromParts:binaryParts:withId:(NSArray*>* parts, NSArray* binaryParts, NSString* blobId); public virtual void remove:(NSString* blobId); public virtual void store:withId:(NSData* data, NSString* blobId); } @@ -2336,10 +2340,10 @@ protocol NativeBlobModuleSpec : public NSObjectRCTBridgeModule, public RCTTurboM public virtual facebook::react::ModuleConstants getConstants(); public virtual void addNetworkingHandler(); public virtual void addWebSocketHandler:(double id); - public virtual void createFromParts:withId:(NSArray* parts, NSString* withId); + public virtual void createFromParts:binaryParts:withId:(NSArray* parts, NSArray* binaryParts, NSString* withId); public virtual void release:(NSString* blobId); public virtual void removeWebSocketHandler:(double id); - public virtual void sendOverSocket:socketID:(NSDictionary* blob, double socketID); + public virtual void sendOverSocket:socketID:(JS::NativeBlobModule::BlobDescriptor& blob, double socketID); } protocol NativeClipboardSpec : public NSObjectRCTBridgeModule, public RCTTurboModule { @@ -10525,6 +10529,17 @@ struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntr public bool operator==(const facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntry& other) const; } +template +struct facebook::react::NativeBlobModuleBlobDescriptor { + public P0 blobId; + public P1 offset; + public P2 size; + public P3 name; + public P4 type; + public P5 lastModified; + public bool operator==(const facebook::react::NativeBlobModuleBlobDescriptor& other) const; +} + template struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverObserveOptions { public P0 intersectionObserverId; @@ -10681,6 +10696,27 @@ struct facebook::react::NativeAnimatedTurboModuleEventMapping { public bool operator==(const facebook::react::NativeAnimatedTurboModuleEventMapping& other) const; } +template +struct facebook::react::NativeBlobModuleBlobBinaryPart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobBinaryPart& other) const; +} + +template +struct facebook::react::NativeBlobModuleBlobReferencePart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobReferencePart& other) const; +} + +template +struct facebook::react::NativeBlobModuleBlobStringPart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobStringPart& other) const; +} + template struct facebook::react::NativeBlobModuleConstants { public P0 BLOB_URI_SCHEME; @@ -11248,6 +11284,46 @@ struct facebook::react::NativeAppStateAppStateConstantsBridging { public static facebook::jsi::String initialAppStateToJs(facebook::jsi::Runtime& rt, decltype(types.initialAppState) value); } +template +struct facebook::react::NativeBlobModuleBlobBinaryPartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static double dataToJs(facebook::jsi::Runtime& rt, decltype(types.data) value); + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); + public static facebook::jsi::String typeToJs(facebook::jsi::Runtime& rt, decltype(types.type) value); +} + +template +struct facebook::react::NativeBlobModuleBlobDescriptorBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static double lastModifiedToJs(facebook::jsi::Runtime& rt, decltype(types.lastModified) value); + public static double offsetToJs(facebook::jsi::Runtime& rt, decltype(types.offset) value); + public static double sizeToJs(facebook::jsi::Runtime& rt, decltype(types.size) value); + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); + public static facebook::jsi::String blobIdToJs(facebook::jsi::Runtime& rt, decltype(types.blobId) value); + public static facebook::jsi::String nameToJs(facebook::jsi::Runtime& rt, decltype(types.name) value); + public static facebook::jsi::String typeToJs(facebook::jsi::Runtime& rt, decltype(types.type) value); +} + +template +struct facebook::react::NativeBlobModuleBlobReferencePartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object dataToJs(facebook::jsi::Runtime& rt, decltype(types.data) value); + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); + public static facebook::jsi::String typeToJs(facebook::jsi::Runtime& rt, decltype(types.type) value); +} + +template +struct facebook::react::NativeBlobModuleBlobStringPartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); + public static facebook::jsi::String dataToJs(facebook::jsi::Runtime& rt, decltype(types.data) value); + public static facebook::jsi::String typeToJs(facebook::jsi::Runtime& rt, decltype(types.type) value); +} + template struct facebook::react::NativeBlobModuleConstantsBridging { public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); @@ -15796,6 +15872,16 @@ struct JS::NativeAppState::Constants::Builder::Input { } +struct JS::NativeBlobModule::BlobDescriptor { + protected BlobDescriptor(NSDictionary* const v); + protected NSString* blobId() const; + protected NSString* name() const; + protected NSString* type() const; + protected double offset() const; + protected double size() const; + protected std::optional lastModified() const; +} + struct JS::NativeBlobModule::Constants { protected NSDictionary* unsafeRawValue() const; protected static JS::NativeBlobModule::Constants fromUnsafeRawValue(NSDictionary* const v); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index a684210fc5d1..08e0a1fdf1b5 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -173,6 +173,10 @@ category RCTCxxConvert(NativeAnimatedTurboModule_EventMapping) { public virtual static RCTManagedPointer* JS_NativeAnimatedTurboModule_EventMapping:(id json); } +category RCTCxxConvert(NativeBlobModule_BlobDescriptor) { + public virtual static RCTManagedPointer* JS_NativeBlobModule_BlobDescriptor:(id json); +} + category RCTCxxConvert(NativeExceptionsManager_ExceptionData) { public virtual static RCTManagedPointer* JS_NativeExceptionsManager_ExceptionData:(id json); } @@ -686,7 +690,7 @@ interface RCTBlobManager : public NSObject *>* parts, NSString* blobId); + public virtual void createFromParts:binaryParts:withId:(NSArray*>* parts, NSArray* binaryParts, NSString* blobId); public virtual void remove:(NSString* blobId); public virtual void store:withId:(NSData* data, NSString* blobId); } @@ -2329,10 +2333,10 @@ protocol NativeBlobModuleSpec : public NSObjectRCTBridgeModule, public RCTTurboM public virtual facebook::react::ModuleConstants getConstants(); public virtual void addNetworkingHandler(); public virtual void addWebSocketHandler:(double id); - public virtual void createFromParts:withId:(NSArray* parts, NSString* withId); + public virtual void createFromParts:binaryParts:withId:(NSArray* parts, NSArray* binaryParts, NSString* withId); public virtual void release:(NSString* blobId); public virtual void removeWebSocketHandler:(double id); - public virtual void sendOverSocket:socketID:(NSDictionary* blob, double socketID); + public virtual void sendOverSocket:socketID:(JS::NativeBlobModule::BlobDescriptor& blob, double socketID); } protocol NativeClipboardSpec : public NSObjectRCTBridgeModule, public RCTTurboModule { @@ -10341,6 +10345,17 @@ struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntr public bool operator==(const facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntry& other) const; } +template +struct facebook::react::NativeBlobModuleBlobDescriptor { + public P0 blobId; + public P1 offset; + public P2 size; + public P3 name; + public P4 type; + public P5 lastModified; + public bool operator==(const facebook::react::NativeBlobModuleBlobDescriptor& other) const; +} + template struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverObserveOptions { public P0 intersectionObserverId; @@ -10497,6 +10512,27 @@ struct facebook::react::NativeAnimatedTurboModuleEventMapping { public bool operator==(const facebook::react::NativeAnimatedTurboModuleEventMapping& other) const; } +template +struct facebook::react::NativeBlobModuleBlobBinaryPart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobBinaryPart& other) const; +} + +template +struct facebook::react::NativeBlobModuleBlobReferencePart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobReferencePart& other) const; +} + +template +struct facebook::react::NativeBlobModuleBlobStringPart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobStringPart& other) const; +} + template struct facebook::react::NativeBlobModuleConstants { public P0 BLOB_URI_SCHEME; @@ -11042,6 +11078,34 @@ struct facebook::react::NativeAppStateAppStateConstantsBridging { public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); } +template +struct facebook::react::NativeBlobModuleBlobBinaryPartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); +} + +template +struct facebook::react::NativeBlobModuleBlobDescriptorBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); +} + +template +struct facebook::react::NativeBlobModuleBlobReferencePartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); +} + +template +struct facebook::react::NativeBlobModuleBlobStringPartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); +} + template struct facebook::react::NativeBlobModuleConstantsBridging { public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); @@ -15475,6 +15539,16 @@ struct JS::NativeAppState::Constants::Builder::Input { } +struct JS::NativeBlobModule::BlobDescriptor { + protected BlobDescriptor(NSDictionary* const v); + protected NSString* blobId() const; + protected NSString* name() const; + protected NSString* type() const; + protected double offset() const; + protected double size() const; + protected std::optional lastModified() const; +} + struct JS::NativeBlobModule::Constants { protected NSDictionary* unsafeRawValue() const; protected static JS::NativeBlobModule::Constants fromUnsafeRawValue(NSDictionary* const v); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index aba10ceba893..7fb9c964050c 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -173,6 +173,10 @@ category RCTCxxConvert(NativeAnimatedTurboModule_EventMapping) { public virtual static RCTManagedPointer* JS_NativeAnimatedTurboModule_EventMapping:(id json); } +category RCTCxxConvert(NativeBlobModule_BlobDescriptor) { + public virtual static RCTManagedPointer* JS_NativeBlobModule_BlobDescriptor:(id json); +} + category RCTCxxConvert(NativeExceptionsManager_ExceptionData) { public virtual static RCTManagedPointer* JS_NativeExceptionsManager_ExceptionData:(id json); } @@ -686,7 +690,7 @@ interface RCTBlobManager : public NSObject *>* parts, NSString* blobId); + public virtual void createFromParts:binaryParts:withId:(NSArray*>* parts, NSArray* binaryParts, NSString* blobId); public virtual void remove:(NSString* blobId); public virtual void store:withId:(NSData* data, NSString* blobId); } @@ -2336,10 +2340,10 @@ protocol NativeBlobModuleSpec : public NSObjectRCTBridgeModule, public RCTTurboM public virtual facebook::react::ModuleConstants getConstants(); public virtual void addNetworkingHandler(); public virtual void addWebSocketHandler:(double id); - public virtual void createFromParts:withId:(NSArray* parts, NSString* withId); + public virtual void createFromParts:binaryParts:withId:(NSArray* parts, NSArray* binaryParts, NSString* withId); public virtual void release:(NSString* blobId); public virtual void removeWebSocketHandler:(double id); - public virtual void sendOverSocket:socketID:(NSDictionary* blob, double socketID); + public virtual void sendOverSocket:socketID:(JS::NativeBlobModule::BlobDescriptor& blob, double socketID); } protocol NativeClipboardSpec : public NSObjectRCTBridgeModule, public RCTTurboModule { @@ -10516,6 +10520,17 @@ struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntr public bool operator==(const facebook::react::NativeIntersectionObserverNativeIntersectionObserverEntry& other) const; } +template +struct facebook::react::NativeBlobModuleBlobDescriptor { + public P0 blobId; + public P1 offset; + public P2 size; + public P3 name; + public P4 type; + public P5 lastModified; + public bool operator==(const facebook::react::NativeBlobModuleBlobDescriptor& other) const; +} + template struct facebook::react::NativeIntersectionObserverNativeIntersectionObserverObserveOptions { public P0 intersectionObserverId; @@ -10672,6 +10687,27 @@ struct facebook::react::NativeAnimatedTurboModuleEventMapping { public bool operator==(const facebook::react::NativeAnimatedTurboModuleEventMapping& other) const; } +template +struct facebook::react::NativeBlobModuleBlobBinaryPart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobBinaryPart& other) const; +} + +template +struct facebook::react::NativeBlobModuleBlobReferencePart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobReferencePart& other) const; +} + +template +struct facebook::react::NativeBlobModuleBlobStringPart { + public P0 type; + public P1 data; + public bool operator==(const facebook::react::NativeBlobModuleBlobStringPart& other) const; +} + template struct facebook::react::NativeBlobModuleConstants { public P0 BLOB_URI_SCHEME; @@ -11217,6 +11253,34 @@ struct facebook::react::NativeAppStateAppStateConstantsBridging { public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); } +template +struct facebook::react::NativeBlobModuleBlobBinaryPartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); +} + +template +struct facebook::react::NativeBlobModuleBlobDescriptorBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); +} + +template +struct facebook::react::NativeBlobModuleBlobReferencePartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); +} + +template +struct facebook::react::NativeBlobModuleBlobStringPartBridging { + public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); + public static T types; + public static facebook::jsi::Object toJs(facebook::jsi::Runtime& rt, const T& value, const std::shared_ptr& jsInvoker); +} + template struct facebook::react::NativeBlobModuleConstantsBridging { public static T fromJs(facebook::jsi::Runtime& rt, const facebook::jsi::Object& value, const std::shared_ptr& jsInvoker); @@ -15650,6 +15714,16 @@ struct JS::NativeAppState::Constants::Builder::Input { } +struct JS::NativeBlobModule::BlobDescriptor { + protected BlobDescriptor(NSDictionary* const v); + protected NSString* blobId() const; + protected NSString* name() const; + protected NSString* type() const; + protected double offset() const; + protected double size() const; + protected std::optional lastModified() const; +} + struct JS::NativeBlobModule::Constants { protected NSDictionary* unsafeRawValue() const; protected static JS::NativeBlobModule::Constants fromUnsafeRawValue(NSDictionary* const v);