From 96394cc67b623023f23f809b9b2f7a20a7496851 Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Wed, 12 Jun 2024 09:52:38 -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 --- .../src/parsers/error-utils.js | 4 +-- .../src/parsers/errors.js | 3 -- .../src/parsers/parsers-commons.js | 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 ++++++++++++++++ 11 files changed, 126 insertions(+), 9 deletions(-) diff --git a/packages/react-native-codegen/src/parsers/error-utils.js b/packages/react-native-codegen/src/parsers/error-utils.js index 049920939922bc..29f54d61dcc5ca 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 dde9cf45ac54a7..91b9c827b24575 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/parsers-commons.js b/packages/react-native-codegen/src/parsers/parsers-commons.js index ec60b17dcc64fa..5cc1a16e1b9b17 100644 --- a/packages/react-native-codegen/src/parsers/parsers-commons.js +++ b/packages/react-native-codegen/src/parsers/parsers-commons.js @@ -506,7 +506,6 @@ function buildEventEmitterSchema( parser, typeAnnotationNullable, typeAnnotationUntyped, - cxxOnly, ); const eventTypeResolutionStatus = resolveTypeAnnotationFN( typeAnnotation.typeParameters.params[0], 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 0d2f21e209be92..25f14f70370980 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 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"; + @Nullable protected CxxCallbackImpl mEventEmitterCallback; + private final @Nullable ReactApplicationContext mReactApplicationContext; public BaseJavaModule() { @@ -129,4 +132,9 @@ protected final ReactApplicationContext getReactApplicationContext() { } 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 1b4d747e98f7a6..1fcf29ac86f5c6 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 15a8265645df5c..3a9d542e898b74 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 81a230fccdcc6c..49cfee3180b59e 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 @@ -35,6 +35,22 @@ public NativeSampleTurboModuleSpec(ReactApplicationContext reactContext) { super(reactContext); } + 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); + } + @Override public @Nonnull String getName() { return NAME; 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 72f9d1320b3f74..f5ff8843423308 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 a0f3b16e42d10c..f74faf4b4dd1e9 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 double getRootTag(double arg) { @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 4c1fab3c4946c1..0cecad05b3dbe1 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 bcfc81b582cecc..2d590df3907a3d 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 {