From 407fb5c2384b13abba677789b8d86b21b6ace190 Mon Sep 17 00:00:00 2001 From: Ruslan Shestopalyuk Date: Tue, 21 Feb 2023 15:15:31 -0800 Subject: [PATCH] Implement ability to emit device events from C++ TurboModules Summary: [Changelog][Internal] This adds a method, `emitDeviceEvent` to the C++ API of TurboModules, which allows to make calls to JS's `RCTDeviceEventEmitter.emit` from a C++ TurboModules. This is a very common pattern, specifically for the VR apps, but not only for them - e.g. Desktop fork also has a [custom implementation for this](https://www.internalfb.com/code/fbsource/third-party/microsoft-fork-of-react-native/react-native-utils/RCTEventEmitter.cpp). Note that my original intent was to actually backport the latter, however there are some complications with wiring things in a robust way, without exposing too much stuff and relying on singletons or folly::dynamic. So I ended up adding it to the TurboModule API itself and use the scheduler/JSI facilities instead. This approach is arguably well self-contained, uses high level APIs, and shouldn't be abusable much. Since I was trying to avoid usage of folly::dynamic in this case, I used a kind of "value factory" pattern instead in order to send the arguments to the JS thread in a thread safe way (see [the discussion here](https://fb.workplace.com/groups/rn.fabric/permalink/1398711453593610/)). Reviewed By: christophpurrer Differential Revision: D43466326 fbshipit-source-id: a3cb8359d08a46421559edd0f854772863cb5c39 --- .../EventEmitter/RCTDeviceEventEmitter.js | 10 +++++++- .../core/ReactCommon/TurboModule.cpp | 24 +++++++++++++++++++ .../core/ReactCommon/TurboModule.h | 20 ++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/Libraries/EventEmitter/RCTDeviceEventEmitter.js b/Libraries/EventEmitter/RCTDeviceEventEmitter.js index 65fe57da7b3a3c..256983291888be 100644 --- a/Libraries/EventEmitter/RCTDeviceEventEmitter.js +++ b/Libraries/EventEmitter/RCTDeviceEventEmitter.js @@ -21,4 +21,12 @@ type RCTDeviceEventDefinitions = $FlowFixMe; * * NativeModules that emit events should instead subclass `NativeEventEmitter`. */ -export default (new EventEmitter(): IEventEmitter); +const RCTDeviceEventEmitter: IEventEmitter = + new EventEmitter(); + +Object.defineProperty(global, '__rctDeviceEventEmitter', { + configurable: true, + value: RCTDeviceEventEmitter, +}); + +export default (RCTDeviceEventEmitter: IEventEmitter); diff --git a/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.cpp b/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.cpp index 89ac90bcd8362a..87e0eabcd55f51 100644 --- a/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.cpp +++ b/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.cpp @@ -36,5 +36,29 @@ jsi::Value TurboModule::get( return result; } +void TurboModule::emitDeviceEvent( + jsi::Runtime &runtime, + const std::string &eventName, + ArgFactory argFactory) { + jsInvoker_->invokeAsync([&runtime, eventName, argFactory]() { + jsi::Value emitter = + runtime.global().getProperty(runtime, "__rctDeviceEventEmitter"); + if (!emitter.isUndefined()) { + jsi::Object emitterObject = emitter.asObject(runtime); + // TODO: consider caching these + jsi::Function emitFunction = + emitterObject.getPropertyAsFunction(runtime, "emit"); + std::vector args; + args.emplace_back( + jsi::String::createFromAscii(runtime, eventName.c_str())); + if (argFactory) { + argFactory(runtime, args); + } + emitFunction.callWithThis( + runtime, emitterObject, (const jsi::Value *)args.data(), args.size()); + } + }); +} + } // namespace react } // namespace facebook diff --git a/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.h b/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.h index 866a0b4d4921d5..6a8bba7b5fa742 100644 --- a/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.h +++ b/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.h @@ -83,6 +83,26 @@ class JSI_EXPORT TurboModule : public facebook::jsi::HostObject { }; std::unordered_map methodMap_; + using ArgFactory = + std::function &args)>; + + /** + * Calls RCTDeviceEventEmitter.emit to JavaScript, with given event name and + * an optional list of arguments. + * If present, argFactory is a callback used to construct extra arguments, + * e.g. + * + * emitDeviceEvent(rt, "myCustomEvent", + * [](jsi::Runtime& rt, std::vector& args) { + * args.emplace_back(jsi::Value(true)); + * args.emplace_back(jsi::Value(42)); + * }); + */ + void emitDeviceEvent( + jsi::Runtime &runtime, + const std::string &eventName, + ArgFactory argFactory = nullptr); + private: friend class TurboCxxModule; friend class TurboModuleBinding;