diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js index 9dc0b42ce8f6..a025086811eb 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js @@ -28,7 +28,7 @@ const {parseValidUnionType, toPascalCase} = require('../Utils'); const { createAliasResolver, getModules, - throwIfUnsupportedPromiseArrayBuffer, + isArrayBufferElementType, } = require('./Utils'); type FilesOutput = Map; @@ -276,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': @@ -599,11 +603,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..f6eb7a12bf2d 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js @@ -27,7 +27,7 @@ const {parseValidUnionType} = require('../Utils'); const { createAliasResolver, getModules, - throwIfUnsupportedPromiseArrayBuffer, + isArrayBufferElementType, } = require('./Utils'); type FilesOutput = Map; @@ -47,15 +47,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}); }`; }; @@ -307,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': @@ -406,6 +413,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 +477,6 @@ function translateMethodForImplementation( unwrapNullable(property.typeAnnotation); const {returnTypeAnnotation} = propertyTypeAnnotation; - throwIfUnsupportedPromiseArrayBuffer(property.name, returnTypeAnnotation); - if ( property.name === 'getConstants' && returnTypeAnnotation.type === 'ObjectTypeAnnotation' && @@ -468,6 +490,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..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,7 +26,7 @@ const { } = require('../../../parsers/parsers-commons'); const {wrapOptional} = require('../../TypeUtils/Objective-C'); const {capitalize, parseValidUnionType} = require('../../Utils'); -const {throwIfUnsupportedPromiseArrayBuffer} = require('../Utils'); +const {isArrayBufferElementType} = require('../Utils'); const {getNamespacedStructName} = require('./Utils'); const invariant = require('invariant'); @@ -104,11 +104,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( @@ -224,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 8cd8d37ff096..21aca9c6fd95 100644 --- a/packages/react-native-codegen/src/generators/modules/Utils.js +++ b/packages/react-native-codegen/src/generators/modules/Utils.js @@ -12,12 +12,13 @@ import type { NativeModuleAliasMap, + NativeModuleBaseTypeAnnotation, NativeModuleObjectTypeAnnotation, - NativeModuleReturnTypeAnnotation, NativeModuleSchema, NativeModuleTypeAnnotation, Nullable, SchemaType, + UnsafeAnyTypeAnnotation, } from '../../CodegenSchema'; const {unwrapNullable} = require('../../parsers/parsers-commons'); @@ -78,44 +79,23 @@ 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; +/** + * 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; } - 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.', - ); + return false; } + return elementType.type === 'ArrayBufferTypeAnnotation'; } module.exports = { @@ -123,5 +103,5 @@ module.exports = { getModules, isDirectRecursiveMember, isArrayRecursiveMember, - throwIfUnsupportedPromiseArrayBuffer, + 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 83cc98bef054..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 @@ -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,48 @@ 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: [], + }, + }, + { + name: 'arrayBufferArray', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'NumberTypeAnnotation', + }, + params: [ + { + name: 'values', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'ArrayBufferTypeAnnotation', + }, + }, + }, + ], + }, + }, ], }, moduleName: 'SampleTurboModule', - excludedPlatforms: ['android', 'iOS'], }, }, }; @@ -2896,7 +2915,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..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 @@ -10,8 +10,6 @@ 'use strict'; -import type {SchemaType} from '../../../CodegenSchema'; - const fixtures = require('../__test_fixtures__/fixtures.js'); const generator = require('../GenerateModuleObjCpp'); @@ -34,41 +32,15 @@ describe('GenerateModuleHObjCpp', () => { }); }); - 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/); + 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 3cbcf9747179..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 @@ -10,8 +10,6 @@ 'use strict'; -import type {SchemaType} from '../../../CodegenSchema'; - const fixtures = require('../__test_fixtures__/fixtures.js'); const generator = require('../GenerateModuleJavaSpec.js'); @@ -32,36 +30,59 @@ describe('GenerateModuleJavaSpec', () => { }); }); - it('throws for a method returning Promise (unsupported on Android)', () => { - const schema: SchemaType = { + 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: 'getAsyncBuffer', + name: 'nullableElements', optional: false, typeAnnotation: { type: 'FunctionTypeAnnotation', - returnTypeAnnotation: { - type: 'PromiseTypeAnnotation', - elementType: {type: 'ArrayBufferTypeAnnotation'}, - }, - params: [], + returnTypeAnnotation: {type: 'NumberTypeAnnotation'}, + params: [ + { + name: 'values', + optional: false, + typeAnnotation: { + type: 'ArrayTypeAnnotation', + elementType: { + type: 'NullableTypeAnnotation', + typeAnnotation: {type: 'ArrayBufferTypeAnnotation'}, + }, + }, + }, + ], }, }, ], }, - moduleName: 'SampleTurboModule', }, }, }; - expect(() => - generator.generate('array_buffer_promise_throws', schema), - ).toThrow(/Promise is not supported/); + 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 72e173904c6a..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 @@ -10,8 +10,6 @@ 'use strict'; -import type {SchemaType} from '../../../CodegenSchema'; - const fixtures = require('../__test_fixtures__/fixtures.js'); const generator = require('../GenerateModuleJniCpp.js'); @@ -32,40 +30,14 @@ describe('GenerateModuleJniCpp', () => { }); }); - 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/); + 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 f2b1d6ad49ce..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 @@ -67,6 +67,9 @@ 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}; + methodMap_[\\"arrayBufferArray\\"] = MethodMetadata {.argCount = 1, .invoker = __arrayBufferArray}; } private: @@ -92,49 +95,28 @@ 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)); + } + + 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 4726cdcd0d96..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 @@ -107,6 +107,11 @@ 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; +- (NSNumber *)arrayBufferArray:(NSArray *)values; @end @@ -134,48 +139,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..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 @@ -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,23 @@ 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); + + @ReactMethod(isBlockingSynchronousMethod = true) + @DoNotStrip + public abstract double arrayBufferArray(ArrayBuffer[] values); } ", } `; -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..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 @@ -66,11 +66,29 @@ 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); +} + +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}; 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}; + methodMap_[\\"arrayBufferArray\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleSpecJSI_arrayBufferArray}; } std::shared_ptr array_buffer_native_module_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams ¶ms) { @@ -85,34 +103,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..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 @@ -82,6 +82,18 @@ 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); + } + + 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) { @@ -93,34 +105,21 @@ namespace facebook::react { 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}; + } } // 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-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/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..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,13 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -#include #include #include +#include #include #include -#include #include #include #include @@ -24,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -134,20 +134,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 +172,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)); + }); }); } @@ -304,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 @@ -460,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."); - } - - // 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; + "JavaTurboModule::convertJSIArgsToJNIArgs: Array exceeds maximum size."); } - 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); @@ -595,7 +777,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 +1145,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/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 4c97e1140156..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 @@ -186,6 +186,44 @@ 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) + } + + // 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 6ec73228f76e..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 @@ -16,6 +16,7 @@ #import #import +#include #include using namespace facebook::react; @@ -176,6 +177,41 @@ - (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); +} + +- (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 c458c91a2204..3f4973553f81 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,8 @@ 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 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 ddfcc31fb04f..48a1da11f1f4 100644 --- a/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js +++ b/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js @@ -53,6 +53,8 @@ type Examples = | 'getArrayBuffer' | 'createNativeBuffer' | 'processAsyncBuffer' + | 'getAsyncBuffer' + | 'arrayBufferArray' | 'promise' | 'rejectPromise' | 'voidFunc' @@ -109,6 +111,18 @@ 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))), + ), + 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/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..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); @@ -2521,6 +2522,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..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); @@ -2514,6 +2515,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..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); @@ -2521,6 +2522,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);