diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.cpp b/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.cpp index 75b7c5d4696a..ebc01af6ec7e 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.cpp @@ -15,21 +15,6 @@ TurboModule::TurboModule( std::shared_ptr jsInvoker) : name_(std::move(name)), jsInvoker_(std::move(jsInvoker)) {} -jsi::Value TurboModule::createHostFunction( - jsi::Runtime &runtime, - const jsi::PropNameID &propName, - const MethodMetadata &meta) { - return jsi::Function::createFromHostFunction( - runtime, - propName, - static_cast(meta.argCount), - [this, meta]( - jsi::Runtime &rt, - const jsi::Value &thisVal, - const jsi::Value *args, - size_t count) { return meta.invoker(rt, *this, args, count); }); -} - void TurboModule::emitDeviceEvent( jsi::Runtime &runtime, const std::string &eventName, diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.h b/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.h index 8ca27a22b2d7..b76f8436df91 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.h +++ b/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.h @@ -48,22 +48,15 @@ class JSI_EXPORT TurboModule : public facebook::jsi::HostObject { facebook::jsi::Runtime &runtime, const facebook::jsi::PropNameID &propName) override { { - std::string propNameUtf8 = propName.utf8(runtime); - auto p = methodMap_.find(propNameUtf8); - if (p == methodMap_.end()) { - // Method was not found, let JS decide what to do. - return facebook::jsi::Value::undefined(); - } else { - auto moduleMethod = createHostFunction(runtime, propName, p->second); - // If we have a JS wrapper, cache the result of this lookup - // We don't cache misses, to allow for methodMap_ to dynamically be - // extended - if (jsRepresentation_) { - jsRepresentation_->lock(runtime).asObject(runtime).setProperty( - runtime, propName, moduleMethod); - } - return moduleMethod; + auto prop = create(runtime, propName); + // If we have a JS wrapper, cache the result of this lookup + // We don't cache misses, to allow for methodMap_ to dynamically be + // extended + if (jsRepresentation_ && !prop.isUndefined()) { + jsRepresentation_->lock(runtime).asObject(runtime).setProperty( + runtime, propName, prop); } + return prop; } } @@ -111,15 +104,32 @@ class JSI_EXPORT TurboModule : public facebook::jsi::HostObject { const std::string &eventName, ArgFactory argFactory = nullptr); + virtual jsi::Value create( + jsi::Runtime &runtime, + const jsi::PropNameID &propName) { + std::string propNameUtf8 = propName.utf8(runtime); + auto p = methodMap_.find(propNameUtf8); + if (p == methodMap_.end()) { + // Method was not found, let JS decide what to do. + return facebook::jsi::Value::undefined(); + } else { + const MethodMetadata &meta = p->second; + return jsi::Function::createFromHostFunction( + runtime, + propName, + static_cast(meta.argCount), + [this, meta]( + jsi::Runtime &rt, + const jsi::Value &thisVal, + const jsi::Value *args, + size_t count) { return meta.invoker(rt, *this, args, count); }); + } + } + private: friend class TurboCxxModule; friend class TurboModuleBinding; std::unique_ptr jsRepresentation_; - - facebook::jsi::Value createHostFunction( - facebook::jsi::Runtime &runtime, - const facebook::jsi::PropNameID &propName, - const MethodMetadata &meta); }; /** diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp b/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp index cc08d03c028e..b6e9d728cc52 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp @@ -18,6 +18,64 @@ using namespace facebook; namespace facebook { namespace react { +namespace { +class BridgelessNativeModuleProxy : public jsi::HostObject { + public: + jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name) override { + /** + * BatchedBridge/NativeModules.js contains this line: + * + * module.exports = global.nativeModuleProxy + * + * This means that NativeModuleProxy is exported as a module from + * 'NativeModules.js'. Whenever some JavaScript requires 'NativeModule.js', + * Metro checks this module's __esModule property to see if the module is an + * ES6 module. + * + * We return false from this property access, so that we can fail on the + * actual NativeModule require that happens later, which is more actionable. + */ + if (name.utf8(runtime) == "__esModule") { + return jsi::Value(false); + } + throw jsi::JSError( + runtime, + "Tried to access NativeModule \"" + name.utf8(runtime) + + "\" from the bridge. This isn't allowed in Bridgeless mode."); + } + + void set( + jsi::Runtime &runtime, + const jsi::PropNameID & /*name*/, + const jsi::Value & /*value*/) override { + throw jsi::JSError( + runtime, + "Tried to insert a NativeModule into the bridge's NativeModule proxy."); + } +}; +} // namespace + +// TODO(148359183): Merge this with the Bridgeless defineReadOnlyGlobal util +static void defineReadOnlyGlobal( + jsi::Runtime &runtime, + std::string propName, + jsi::Value &&value) { + jsi::Object jsObject = + runtime.global().getProperty(runtime, "Object").asObject(runtime); + jsi::Function defineProperty = jsObject.getProperty(runtime, "defineProperty") + .asObject(runtime) + .asFunction(runtime); + + jsi::Object descriptor = jsi::Object(runtime); + descriptor.setProperty(runtime, "value", std::move(value)); + defineProperty.callWithThis( + runtime, + jsObject, + runtime.global(), + jsi::String::createFromUtf8(runtime, propName), + descriptor); +} + /** * Public API to install the TurboModule system. */ @@ -51,6 +109,14 @@ void TurboModuleBinding::install( std::string moduleName = args[0].getString(rt).utf8(rt); return binding.getModule(rt, moduleName); })); + + if (runtime.global().hasProperty(runtime, "RN$Bridgeless")) { + defineReadOnlyGlobal( + runtime, + "nativeModuleProxy", + jsi::Object::createFromHostObject( + runtime, std::make_shared())); + } } TurboModuleBinding::~TurboModuleBinding() { @@ -72,6 +138,13 @@ jsi::Value TurboModuleBinding::getModule( return jsi::Object::createFromHostObject(runtime, std::move(module)); } + // What is jsRepresentation? A cache for the TurboModule's properties + // Henceforth, always return the cache (i.e: jsRepresentation) to JavaScript + // + // If a jsRepresentation is found on the TurboModule, return it. + // + // Note: TurboModules are cached by name in TurboModuleManagers. Hence, + // jsRepresentation is also cached by by name by the TurboModuleManager auto &weakJsRepresentation = module->jsRepresentation_; if (weakJsRepresentation) { auto jsRepresentation = weakJsRepresentation->lock(runtime); @@ -80,20 +153,28 @@ jsi::Value TurboModuleBinding::getModule( } } - // No JS representation found, or object has been collected + // Status: No jsRepresentation found on TurboModule + // Create a brand new jsRepresentation, and attach it to TurboModule jsi::Object jsRepresentation(runtime); weakJsRepresentation = std::make_unique(runtime, jsRepresentation); if (bindingMode_ == TurboModuleBindingMode::Prototype) { - // Option 1: create plain object, with it's prototype mapped back to the - // hostobject. Any properties accessed are stored on the plain object + // Option 1: Lazily populate the jsRepresentation, on property access. + // + // How does this work? + // 1. Initially jsRepresentation is empty: {} + // 2. If property lookup on jsRepresentation fails, the JS runtime will + // search jsRepresentation's prototype: jsi::Object(TurboModule). + // 3. TurboModule::get(runtime, propKey) executes. This creates the + // property, caches it on jsRepresentation, then returns it to + // JavaScript. auto hostObject = jsi::Object::createFromHostObject(runtime, std::move(module)); jsRepresentation.setProperty(runtime, "__proto__", std::move(hostObject)); } else { - // Option 2: eagerly install all hostfunctions at this point, avoids - // prototype + // Option 2: Eagerly populate the jsRepresentation, on create. + // Object.assign(jsRepresentation, jsi::Object(TurboModule)) for (auto &propName : module->getPropertyNames(runtime)) { module->get(runtime, propName); }