From 84a9f5e6c8e3af97e7d084ccb8ffdef44dcc87fb Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Fri, 28 Jun 2024 09:41:02 -0700 Subject: [PATCH] Add Java Turbo Module Event Emitter example (#44906) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/44906 Shows a proof of concept how '*strongly typed Turbo Module scoped*' `EventEmitters` can be used in a Java Turbo Module. ## Changelog: [Android] [Added] - Add Java Turbo Module Event Emitter example Reviewed By: javache Differential Revision: D57530807 fbshipit-source-id: 04261d8885760f0e3b3c8c1931e0d56a5d33a0df --- .../src/parsers/error-utils.js | 4 +-- .../src/parsers/errors.js | 3 -- .../modules/__test_fixtures__/fixtures.js | 2 +- .../module-parser-snapshot-test.js.snap | 6 +--- .../src/parsers/parsers-commons.js | 1 - .../modules/__test_fixtures__/fixtures.js | 2 +- ...script-module-parser-snapshot-test.js.snap | 6 +--- .../ReactAndroid/api/ReactAndroid.api | 1 + .../facebook/react/bridge/BaseJavaModule.java | 8 +++++ .../android/ReactCommon/JavaTurboModule.cpp | 30 +++++++++++++++++++ .../android/ReactCommon/JavaTurboModule.h | 2 ++ .../android/NativeSampleTurboModuleSpec.java | 16 ++++++++++ .../ReactCommon/SampleTurboModuleSpec.cpp | 9 ++++++ .../platform/android/SampleTurboModule.java | 21 ++++++++++++- .../specs/modules/NativeSampleTurboModule.js | 15 +++++++++- .../TurboModule/SampleTurboModuleExample.js | 26 ++++++++++++++++ 16 files changed, 131 insertions(+), 21 deletions(-) diff --git a/packages/react-native-codegen/src/parsers/error-utils.js b/packages/react-native-codegen/src/parsers/error-utils.js index 04992093992..29f54d61dcc 100644 --- a/packages/react-native-codegen/src/parsers/error-utils.js +++ b/packages/react-native-codegen/src/parsers/error-utils.js @@ -164,9 +164,8 @@ function throwIfEventEmitterTypeIsUnsupported( parser: Parser, nullable: boolean, untyped: boolean, - cxxOnly: boolean, ) { - if (nullable || untyped || !cxxOnly) { + if (nullable || untyped) { throw new UnsupportedModuleEventEmitterPropertyParserError( nativeModuleName, propertyName, @@ -174,7 +173,6 @@ function throwIfEventEmitterTypeIsUnsupported( parser.language(), nullable, untyped, - cxxOnly, ); } } diff --git a/packages/react-native-codegen/src/parsers/errors.js b/packages/react-native-codegen/src/parsers/errors.js index 101138a7e10..91b9c827b24 100644 --- a/packages/react-native-codegen/src/parsers/errors.js +++ b/packages/react-native-codegen/src/parsers/errors.js @@ -100,15 +100,12 @@ class UnsupportedModuleEventEmitterPropertyParserError extends ParserError { language: ParserType, nullable: boolean, untyped: boolean, - cxxOnly: boolean, ) { let message = `${language} interfaces extending TurboModule must only contain 'FunctionTypeAnnotation's or non nullable 'EventEmitter's. Further the EventEmitter property `; if (nullable) { message += `'${propertyValue}' must non nullable.`; } else if (untyped) { message += `'${propertyValue}' must have a concrete or void eventType.`; - } else if (!cxxOnly) { - message += `'${propertyValue}' is only supported in C++ Turbo Modules.`; } super(nativeModuleName, propertyValue, message); } 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 4b1efc38eed..25c692ccd5b 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 @@ -652,7 +652,7 @@ export interface Spec extends TurboModule { +onEvent6: EventEmitter; } -export default TurboModuleRegistry.getEnforcing('SampleTurboModuleCxx'); +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 17684eeea46..48dbda72558 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 @@ -1531,11 +1531,7 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_EVENT_EM ], 'methods': [] }, - 'moduleName': 'SampleTurboModuleCxx', - 'excludedPlatforms': [ - 'iOS', - 'android' - ] + 'moduleName': 'SampleTurboModule' } } }" diff --git a/packages/react-native-codegen/src/parsers/parsers-commons.js b/packages/react-native-codegen/src/parsers/parsers-commons.js index f012943eade..8dd3f24346e 100644 --- a/packages/react-native-codegen/src/parsers/parsers-commons.js +++ b/packages/react-native-codegen/src/parsers/parsers-commons.js @@ -513,7 +513,6 @@ function buildEventEmitterSchema( parser, typeAnnotationNullable, typeAnnotationUntyped, - cxxOnly, ); const eventTypeResolutionStatus = resolveTypeAnnotationFN( typeAnnotation.typeParameters.params[0], 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 56a3a87a074..7faff4f0a69 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 @@ -751,7 +751,7 @@ export interface Spec extends TurboModule { readonly onEvent6: EventEmitter; } -export default TurboModuleRegistry.getEnforcing('SampleTurboModuleCxx'); +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 6eeda94587b..bc7eb2ba00b 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 @@ -1731,11 +1731,7 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_EV ], 'methods': [] }, - 'moduleName': 'SampleTurboModuleCxx', - 'excludedPlatforms': [ - 'iOS', - 'android' - ] + 'moduleName': 'SampleTurboModule' } } }" diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 2827d31f901..3004783bfb3 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -543,6 +543,7 @@ public abstract class com/facebook/react/bridge/BaseJavaModule : com/facebook/re public static final field METHOD_TYPE_ASYNC Ljava/lang/String; public static final field METHOD_TYPE_PROMISE Ljava/lang/String; public static final field METHOD_TYPE_SYNC Ljava/lang/String; + protected field mEventEmitterCallback Lcom/facebook/react/bridge/CxxCallbackImpl; public fun ()V public fun (Lcom/facebook/react/bridge/ReactApplicationContext;)V public fun canOverrideExistingModule ()Z diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java index 0d2f21e209b..c9b635eb36f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java @@ -13,6 +13,7 @@ import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; import com.facebook.infer.annotation.ThreadConfined; +import com.facebook.proguard.annotations.DoNotStrip; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.annotations.DeprecatedInNewArchitecture; import com.facebook.react.common.annotations.StableReactNativeAPI; @@ -54,6 +55,8 @@ public abstract class BaseJavaModule implements NativeModule { public static final String METHOD_TYPE_PROMISE = "promise"; public static final String METHOD_TYPE_SYNC = "sync"; + protected @Nullable CxxCallbackImpl mEventEmitterCallback; + private final @Nullable ReactApplicationContext mReactApplicationContext; public BaseJavaModule() { @@ -129,4 +132,9 @@ public abstract class BaseJavaModule implements NativeModule { } return null; } + + @DoNotStrip + private final void setEventEmitterCallback(CxxCallbackImpl eventEmitterCallback) { + mEventEmitterCallback = eventEmitterCallback; + } } 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 1b4d747e98f..1fcf29ac86f 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 @@ -992,4 +992,34 @@ jsi::Value JavaTurboModule::invokeJavaMethod( } } +void JavaTurboModule::setEventEmitterCallback( + jni::alias_ref jinstance) { + JNIEnv* env = jni::Environment::current(); + auto instance = jinstance.get(); + static jmethodID cachedMethodId = nullptr; + if (cachedMethodId == nullptr) { + jclass cls = env->GetObjectClass(instance); + cachedMethodId = env->GetMethodID( + cls, + "setEventEmitterCallback", + "(Lcom/facebook/react/bridge/CxxCallbackImpl;)V"); + } + + auto eventEmitterLookup = + [&](const std::string& eventName) -> AsyncEventEmitter& { + return static_cast&>( + *eventEmitterMap_[eventName].get()); + }; + + jvalue arg; + arg.l = JCxxCallbackImpl::newObjectCxxArgs([eventEmitterLookup = std::move( + eventEmitterLookup)]( + folly::dynamic args) { + auto eventName = args.at(0).asString(); + auto eventArgs = args.size() > 1 ? args.at(1) : nullptr; + eventEmitterLookup(eventName).emit(std::move(eventArgs)); + }).release(); + env->CallVoidMethod(instance, cachedMethodId, arg); +} + } // namespace facebook::react 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 15a8265645d..3a9d542e898 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 @@ -51,6 +51,8 @@ class JSI_EXPORT JavaTurboModule : public TurboModule { size_t argCount, jmethodID& cachedMethodID); + void setEventEmitterCallback(jni::alias_ref instance); + private: // instance_ can be of type JTurboModule, or JNativeModule jni::global_ref instance_; diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/NativeSampleTurboModuleSpec.java b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/NativeSampleTurboModuleSpec.java index 81a230fccdc..f2104c737ec 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/NativeSampleTurboModuleSpec.java +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/NativeSampleTurboModuleSpec.java @@ -40,6 +40,22 @@ public abstract class NativeSampleTurboModuleSpec extends ReactContextBaseJavaMo return NAME; } + protected final void emitOnPress() { + mEventEmitterCallback.invoke("onPress"); + } + + protected final void emitOnClick(String value) { + mEventEmitterCallback.invoke("onClick", value); + } + + protected final void emitOnChange(ReadableMap value) { + mEventEmitterCallback.invoke("onChange", value); + } + + protected void emitOnSubmit(ReadableArray value) { + mEventEmitterCallback.invoke("onSubmit", value); + } + protected abstract Map getTypedExportedConstants(); @Override diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/ReactCommon/SampleTurboModuleSpec.cpp b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/ReactCommon/SampleTurboModuleSpec.cpp index 72f9d1320b3..f5ff8843423 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/ReactCommon/SampleTurboModuleSpec.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/ReactCommon/SampleTurboModuleSpec.cpp @@ -351,6 +351,15 @@ NativeSampleTurboModuleSpecJSI::NativeSampleTurboModuleSpecJSI( 1, __hostFunction_NativeSampleTurboModuleSpecJSI_getObjectAssert}; methodMap_["promiseAssert"] = MethodMetadata{ 0, __hostFunction_NativeSampleTurboModuleSpecJSI_promiseAssert}; + eventEmitterMap_["onPress"] = + std::make_shared>(); + eventEmitterMap_["onClick"] = + std::make_shared>(); + eventEmitterMap_["onChange"] = + std::make_shared>(); + eventEmitterMap_["onSubmit"] = + std::make_shared>(); + setEventEmitterCallback(params.instance); } std::shared_ptr SampleTurboModuleSpec_ModuleProvider( diff --git a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.java b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.java index a0f3b16e42d..f74faf4b4dd 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.java +++ b/packages/react-native/ReactCommon/react/nativemodule/samples/platform/android/SampleTurboModule.java @@ -104,7 +104,26 @@ public class SampleTurboModule extends NativeSampleTurboModuleSpec @Override public void voidFunc() { log("voidFunc", "", ""); - return; + emitOnPress(); + emitOnClick("click"); + { + WritableNativeMap map = new WritableNativeMap(); + map.putInt("a", 1); + map.putString("b", "two"); + emitOnChange(map); + } + { + WritableNativeArray array = new WritableNativeArray(); + WritableNativeMap map = new WritableNativeMap(); + map.putInt("a", 1); + map.putString("b", "two"); + array.pushMap(map); + WritableNativeMap map1 = new WritableNativeMap(); + map1.putInt("a", 3); + map1.putString("b", "four"); + array.pushMap(map1); + emitOnSubmit(array); + } } // This function returns {@link WritableMap} instead of {@link Map} for backward compat with diff --git a/packages/react-native/src/private/specs/modules/NativeSampleTurboModule.js b/packages/react-native/src/private/specs/modules/NativeSampleTurboModule.js index 4c1fab3c494..0cecad05b3d 100644 --- a/packages/react-native/src/private/specs/modules/NativeSampleTurboModule.js +++ b/packages/react-native/src/private/specs/modules/NativeSampleTurboModule.js @@ -12,7 +12,10 @@ import type { RootTag, TurboModule, } from '../../../../Libraries/TurboModule/RCTExport'; -import type {UnsafeObject} from '../../../../Libraries/Types/CodegenTypes'; +import type { + EventEmitter, + UnsafeObject, +} from '../../../../Libraries/Types/CodegenTypes'; import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboModuleRegistry'; @@ -21,7 +24,17 @@ export enum EnumInt { B = 42, } +export type ObjectStruct = { + a: number, + b: string, + c?: ?string, +}; + export interface Spec extends TurboModule { + +onPress: EventEmitter; + +onClick: EventEmitter; + +onChange: EventEmitter; + +onSubmit: EventEmitter; // Exported methods. +getConstants: () => {| const1: boolean, diff --git a/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js b/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js index bcfc81b582c..2d590df3907 100644 --- a/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js +++ b/packages/rn-tester/js/examples/TurboModule/SampleTurboModuleExample.js @@ -9,6 +9,7 @@ */ import type {RootTag} from 'react-native/Libraries/ReactNative/RootTag'; +import type {EventSubscription} from 'react-native/Libraries/vendor/emitter/EventEmitter'; import styles from './TurboModuleExampleCommon'; import * as React from 'react'; @@ -68,6 +69,7 @@ type ErrorExamples = class SampleTurboModuleExample extends React.Component<{||}, State> { static contextType: React$Context = RootTagContext; + eventSubscriptions: EventSubscription[] = []; state: State = { testResults: {}, @@ -218,6 +220,30 @@ class SampleTurboModuleExample extends React.Component<{||}, State> { 'The JSI bindings for SampleTurboModule are not installed.', ); } + this.eventSubscriptions.push( + NativeSampleTurboModule.onPress(value => console.log('onPress: ()')), + ); + this.eventSubscriptions.push( + NativeSampleTurboModule.onClick(value => + console.log(`onClick: (${value})`), + ), + ); + this.eventSubscriptions.push( + NativeSampleTurboModule.onChange(value => + console.log(`onChange: (${JSON.stringify(value)})`), + ), + ); + this.eventSubscriptions.push( + NativeSampleTurboModule.onSubmit(value => + console.log(`onSubmit: (${JSON.stringify(value)})`), + ), + ); + } + + componentWillUnmount() { + for (const subscription of this.eventSubscriptions) { + subscription.remove(); + } } render(): React.Node {