From 749b18dbc944945532fc11dea41821010dcff5e4 Mon Sep 17 00:00:00 2001 From: Marc Horowitz Date: Thu, 18 Oct 2018 00:47:04 -0700 Subject: [PATCH] Add JSI-based JSExecutor for the bridge Summary: This is similar in function to the old JSCExecutor, but uses the more abstract JSI API to interact with the JSVM. @public Reviewed By: axe-fb Differential Revision: D9328241 fbshipit-source-id: 3212ff4f43d0589a70d7bebc4d463d4433590f1d --- .../java/com/facebook/react/testing/BUCK | 1 + .../java/com/facebook/react/jscexecutor/BUCK | 35 ++ .../react/jscexecutor/JSCExecutor.java | 32 ++ .../react/jscexecutor/JSCExecutorFactory.java | 36 ++ .../com/facebook/react/jscexecutor/OnLoad.cpp | 70 +++ ReactCommon/jsiexecutor/BUCK | 39 ++ .../jsiexecutor/jsireact/JSIExecutor.cpp | 405 ++++++++++++++++++ .../jsiexecutor/jsireact/JSIExecutor.h | 149 +++++++ .../jsiexecutor/jsireact/JSINativeModules.cpp | 86 ++++ .../jsiexecutor/jsireact/JSINativeModules.h | 35 ++ 10 files changed, 888 insertions(+) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/jscexecutor/BUCK create mode 100644 ReactAndroid/src/main/java/com/facebook/react/jscexecutor/JSCExecutor.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/jscexecutor/JSCExecutorFactory.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/jscexecutor/OnLoad.cpp create mode 100644 ReactCommon/jsiexecutor/BUCK create mode 100644 ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp create mode 100644 ReactCommon/jsiexecutor/jsireact/JSIExecutor.h create mode 100644 ReactCommon/jsiexecutor/jsireact/JSINativeModules.cpp create mode 100644 ReactCommon/jsiexecutor/jsireact/JSINativeModules.h diff --git a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/BUCK b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/BUCK index ce91a2147..a88b8ccb4 100644 --- a/ReactAndroid/src/androidTest/java/com/facebook/react/testing/BUCK +++ b/ReactAndroid/src/androidTest/java/com/facebook/react/testing/BUCK @@ -28,6 +28,7 @@ rn_android_library( react_native_target("java/com/facebook/react/common:common"), react_native_target("java/com/facebook/react/devsupport:interfaces"), react_native_target("java/com/facebook/react/fabric:fabric"), + react_native_target("java/com/facebook/react/jscexecutor:jscexecutor"), react_native_target("java/com/facebook/react/module/annotations:annotations"), react_native_target("java/com/facebook/react/module/model:model"), react_native_target("java/com/facebook/react/modules/core:core"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/jscexecutor/BUCK b/ReactAndroid/src/main/java/com/facebook/react/jscexecutor/BUCK new file mode 100644 index 000000000..788857470 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/jscexecutor/BUCK @@ -0,0 +1,35 @@ +load("//tools/build_defs/oss:rn_defs.bzl", "ANDROID", "FBJNI_TARGET", "rn_xplat_cxx_library", "react_native_xplat_target", "react_native_target", "react_native_dep", "rn_android_library") + +rn_android_library( + name = "jscexecutor", + srcs = glob(["*.java"]), + visibility = [ + "PUBLIC", + ], + deps = [ + ":jni", + react_native_dep("libraries/soloader/java/com/facebook/soloader:soloader"), + react_native_target("java/com/facebook/react/bridge:bridge"), + ], +) + +rn_xplat_cxx_library( + name = "jni", + srcs = glob(["*.cpp"]), + headers = glob(["*.h"]), + header_namespace = "", + compiler_flags = ["-fexceptions"], + platforms = ANDROID, + fbandroid_allow_jni_merging = True, + soname = "libjscexecutor.$(ext)", + visibility = [ + react_native_target("java/com/facebook/react/jscexecutor:jscexecutor"), + ], + deps = [ + "xplat//folly:molly", + FBJNI_TARGET, + react_native_target("jni/react/jni:jni"), + react_native_xplat_target("jsi:JSCRuntime"), + react_native_xplat_target("jsiexecutor:jsiexecutor"), + ], +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/jscexecutor/JSCExecutor.java b/ReactAndroid/src/main/java/com/facebook/react/jscexecutor/JSCExecutor.java new file mode 100644 index 000000000..7455dffb4 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/jscexecutor/JSCExecutor.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + *

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.jscexecutor; + +import com.facebook.jni.HybridData; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.bridge.JavaScriptExecutor; +import com.facebook.react.bridge.ReadableNativeMap; +import com.facebook.soloader.SoLoader; + +@DoNotStrip +/* package */ class JSCExecutor extends JavaScriptExecutor { + static { + SoLoader.loadLibrary("jscexecutor"); + } + + /* package */ JSCExecutor(ReadableNativeMap jscConfig) { + super(initHybrid(jscConfig)); + } + + @Override + public String getName() { + return "JSCExecutor"; + } + + private static native HybridData initHybrid(ReadableNativeMap jscConfig); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/jscexecutor/JSCExecutorFactory.java b/ReactAndroid/src/main/java/com/facebook/react/jscexecutor/JSCExecutorFactory.java new file mode 100644 index 000000000..a71b9bcc8 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/jscexecutor/JSCExecutorFactory.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * + * 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.jscexecutor; + +import com.facebook.react.bridge.JavaScriptExecutor; +import com.facebook.react.bridge.JavaScriptExecutorFactory; +import com.facebook.react.bridge.WritableNativeMap; + +public class JSCExecutorFactory implements JavaScriptExecutorFactory { + private final String mAppName; + private final String mDeviceName; + + public JSCExecutorFactory(String appName, String deviceName) { + this.mAppName = appName; + this.mDeviceName = deviceName; + } + + @Override + public JavaScriptExecutor create() throws Exception { + WritableNativeMap jscConfig = new WritableNativeMap(); + jscConfig.putString("OwnerIdentity", "ReactNative"); + jscConfig.putString("AppIdentity", mAppName); + jscConfig.putString("DeviceIdentity", mDeviceName); + return new JSCExecutor(jscConfig); + } + + @Override + public String toString() { + return "JSIExecutor+JSCRuntime"; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/jscexecutor/OnLoad.cpp b/ReactAndroid/src/main/java/com/facebook/react/jscexecutor/OnLoad.cpp new file mode 100644 index 000000000..c6f185aac --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/jscexecutor/OnLoad.cpp @@ -0,0 +1,70 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook { +namespace react { + +namespace { + +class JSCExecutorFactory : public JSExecutorFactory { +public: + std::unique_ptr createJSExecutor( + std::shared_ptr delegate, + std::shared_ptr jsQueue) override { + return folly::make_unique( + jsc::makeJSCRuntime(), + delegate, + [](const std::string& message, unsigned int logLevel) { + reactAndroidLoggingHook(message, logLevel); + }, + JSIExecutor::defaultTimeoutInvoker, + nullptr); + } +}; + +} + +// This is not like JSCJavaScriptExecutor, which calls JSC directly. This uses +// JSIExecutor with JSCRuntime. +class JSCExecutorHolder + : public jni::HybridClass { + public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/jscexecutor/JSCExecutor;"; + + static jni::local_ref initHybrid( + jni::alias_ref, ReadableNativeMap*) { + // This is kind of a weird place for stuff, but there's no other + // good place for initialization which is specific to JSC on + // Android. + JReactMarker::setLogPerfMarkerIfNeeded(); + // TODO mhorowitz T28461666 fill in some missing nice to have glue + return makeCxxInstance(folly::make_unique()); + } + + static void registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", JSCExecutorHolder::initHybrid), + }); + } + + private: + friend HybridBase; + using HybridBase::HybridBase; +}; + +} // namespace react +} // namespace facebook + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { + return facebook::jni::initialize(vm, [] { + facebook::react::JSCExecutorHolder::registerNatives(); + }); +} diff --git a/ReactCommon/jsiexecutor/BUCK b/ReactCommon/jsiexecutor/BUCK new file mode 100644 index 000000000..e6e6334be --- /dev/null +++ b/ReactCommon/jsiexecutor/BUCK @@ -0,0 +1,39 @@ +load("//tools/build_defs/oss:rn_defs.bzl", "cxx_library", "react_native_xplat_dep", "react_native_xplat_target") + +cxx_library( + name = "jsiexecutor", + srcs = [ + "jsireact/JSIExecutor.cpp", + "jsireact/JSINativeModules.cpp", + ], + header_namespace = "", + exported_headers = { + "jsireact/JSIExecutor.h": "jsireact/JSIExecutor.h", + "jsireact/JSINativeModules.h": "jsireact/JSINativeModules.h", + }, + compiler_flags = [ + "-fexceptions", + "-frtti", + ], + fbandroid_deps = [ + "xplat//folly:molly", + "xplat//third-party/glog:glog", + "xplat//third-party/linker_lib:atomic", + ], + fbobjc_force_static = True, + fbobjc_header_path_prefix = "", + platforms = (ANDROID, APPLE), + preprocessor_flags = [ + "-DLOG_TAG=\"ReactNative\"", + "-DWITH_FBSYSTRACE=1", + ], + visibility = [ + "PUBLIC", + ], + xcode_public_headers_symlinks = True, + deps = [ + react_native_xplat_dep("jsi:jsi"), + react_native_xplat_dep("jsi:JSIDynamic"), + react_native_xplat_target("cxxreact:bridge"), + ], +) diff --git a/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp b/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp new file mode 100644 index 000000000..f243d8d7e --- /dev/null +++ b/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp @@ -0,0 +1,405 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "jsireact/JSIExecutor.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace facebook::jsi; + +namespace facebook { +namespace react { + +JSIExecutorFactory::JSIExecutorFactory( + std::shared_ptr runtime, + JSIExecutor::Logger logger, + JSIExecutor::RuntimeInstaller runtimeInstaller) + : runtime_(runtime), + logger_(logger), + runtimeInstaller_(runtimeInstaller) {} + +std::unique_ptr JSIExecutorFactory::createJSExecutor( + std::shared_ptr delegate, + std::shared_ptr) { + return std::make_unique( + runtime_, + delegate, + logger_, + JSIExecutor::defaultTimeoutInvoker, + runtimeInstaller_); +} + +class JSIExecutor::NativeModuleProxy : public jsi::HostObject { + public: + NativeModuleProxy(JSIExecutor& executor) : executor_(executor) {} + + Value get(Runtime& rt, const PropNameID& name) override { + if (name.utf8(rt) == "name") { + return jsi::String::createFromAscii(rt, "NativeModules"); + } + + return executor_.nativeModules_.getModule(rt, name); + } + + void set(Runtime&, const PropNameID&, const Value&) override { + throw std::runtime_error( + "Unable to put on NativeModules: Operation unsupported"); + } + + private: + JSIExecutor& executor_; +}; + +namespace { + +// basename_r isn't in all iOS SDKs, so use this simple version instead. +std::string simpleBasename(const std::string& path) { + size_t pos = path.rfind("/"); + return (pos != std::string::npos) ? path.substr(pos) : path; +} + +} // namespace + +JSIExecutor::JSIExecutor( + std::shared_ptr runtime, + std::shared_ptr delegate, + Logger logger, + const JSIScopedTimeoutInvoker& scopedTimeoutInvoker, + RuntimeInstaller runtimeInstaller) + : runtime_(runtime), + delegate_(delegate), + nativeModules_(delegate ? delegate->getModuleRegistry() : nullptr), + logger_(logger), + scopedTimeoutInvoker_(scopedTimeoutInvoker), + runtimeInstaller_(runtimeInstaller) { + runtime_->global().setProperty( + *runtime, "__jsiExecutorDescription", runtime->description()); +} + +void JSIExecutor::loadApplicationScript( + std::unique_ptr script, + std::string sourceURL) { + SystraceSection s("JSIExecutor::loadApplicationScript"); + + // TODO: check for and use precompiled HBC + + runtime_->global().setProperty( + *runtime_, + "nativeModuleProxy", + Object::createFromHostObject( + *runtime_, std::make_shared(*this))); + + runtime_->global().setProperty( + *runtime_, + "nativeFlushQueueImmediate", + Function::createFromHostFunction( + *runtime_, + PropNameID::forAscii(*runtime_, "nativeFlushQueueImmediate"), + 1, + [this]( + jsi::Runtime&, + const jsi::Value&, + const jsi::Value* args, + size_t count) { + if (count != 1) { + throw std::invalid_argument( + "nativeFlushQueueImmediate arg count must be 1"); + } + callNativeModules(args[0], false); + return Value::undefined(); + })); + + runtime_->global().setProperty( + *runtime_, + "nativeCallSyncHook", + Function::createFromHostFunction( + *runtime_, + PropNameID::forAscii(*runtime_, "nativeCallSyncHook"), + 1, + [this]( + jsi::Runtime&, + const jsi::Value&, + const jsi::Value* args, + size_t count) { return nativeCallSyncHook(args, count); })); + + if (logger_) { + // Only inject the logging function if it was supplied by the caller. + runtime_->global().setProperty( + *runtime_, + "nativeLoggingHook", + Function::createFromHostFunction( + *runtime_, + PropNameID::forAscii(*runtime_, "nativeLoggingHook"), + 2, + [this]( + jsi::Runtime&, + const jsi::Value&, + const jsi::Value* args, + size_t count) { + if (count != 2) { + throw std::invalid_argument( + "nativeLoggingHook takes 2 arguments"); + } + logger_( + args[0].asString(*runtime_).utf8(*runtime_), + folly::to(args[1].asNumber())); + return Value::undefined(); + })); + } + + if (runtimeInstaller_) { + runtimeInstaller_(*runtime_); + } + + bool hasLogger(ReactMarker::logTaggedMarker); + std::string scriptName = simpleBasename(sourceURL); + if (hasLogger) { + ReactMarker::logTaggedMarker( + ReactMarker::RUN_JS_BUNDLE_START, scriptName.c_str()); + } + runtime_->evaluateJavaScript( + std::make_unique(std::move(script)), sourceURL); + flush(); + if (hasLogger) { + ReactMarker::logMarker(ReactMarker::CREATE_REACT_CONTEXT_STOP); + ReactMarker::logTaggedMarker( + ReactMarker::RUN_JS_BUNDLE_STOP, scriptName.c_str()); + } +} + +void JSIExecutor::setBundleRegistry(std::unique_ptr r) { + if (!bundleRegistry_) { + runtime_->global().setProperty( + *runtime_, + "nativeRequire", + Function::createFromHostFunction( + *runtime_, + PropNameID::forAscii(*runtime_, "nativeRequire"), + 2, + [this]( + Runtime& rt, + const facebook::jsi::Value&, + const facebook::jsi::Value* args, + size_t count) { return nativeRequire(args, count); })); + } + bundleRegistry_ = std::move(r); +} + +void JSIExecutor::registerBundle( + uint32_t bundleId, + const std::string& bundlePath) { + const auto tag = folly::to(bundleId); + ReactMarker::logTaggedMarker( + ReactMarker::REGISTER_JS_SEGMENT_START, tag.c_str()); + if (bundleRegistry_) { + bundleRegistry_->registerBundle(bundleId, bundlePath); + } else { + auto script = JSBigFileString::fromPath(bundlePath); + runtime_->evaluateJavaScript( + std::make_unique(std::move(script)), + JSExecutor::getSyntheticBundlePath(bundleId, bundlePath)); + } + ReactMarker::logTaggedMarker( + ReactMarker::REGISTER_JS_SEGMENT_STOP, tag.c_str()); +} + +void JSIExecutor::callFunction( + const std::string& moduleId, + const std::string& methodId, + const folly::dynamic& arguments) { + SystraceSection s( + "JSIExecutor::callFunction", "moduleId", moduleId, "methodId", methodId); + if (!callFunctionReturnFlushedQueue_) { + bindBridge(); + } + + // Construct the error message producer in case this times out. + // This is executed on a background thread, so it must capture its parameters + // by value. + auto errorProducer = [=] { + std::stringstream ss; + ss << "moduleID: " << moduleId << " methodID: " << methodId + << " arguments: " << folly::toJson(arguments); + return ss.str(); + }; + + Value ret = Value::undefined(); + try { + scopedTimeoutInvoker_( + [&] { + ret = callFunctionReturnFlushedQueue_->call( + *runtime_, + moduleId, + methodId, + valueFromDynamic(*runtime_, arguments)); + }, + std::move(errorProducer)); + } catch (...) { + std::throw_with_nested( + std::runtime_error("Error calling " + moduleId + "." + methodId)); + } + + callNativeModules(ret, true); +} + +void JSIExecutor::invokeCallback( + const double callbackId, + const folly::dynamic& arguments) { + SystraceSection s("JSIExecutor::invokeCallback", "callbackId", callbackId); + if (!invokeCallbackAndReturnFlushedQueue_) { + bindBridge(); + } + Value ret; + try { + ret = invokeCallbackAndReturnFlushedQueue_->call( + *runtime_, callbackId, valueFromDynamic(*runtime_, arguments)); + } catch (...) { + std::throw_with_nested(std::runtime_error( + folly::to("Error invoking callback ", callbackId))); + } + + callNativeModules(ret, true); +} + +void JSIExecutor::setGlobalVariable( + std::string propName, + std::unique_ptr jsonValue) { + SystraceSection s("JSIExecutor::setGlobalVariable", "propName", propName); + runtime_->global().setProperty( + *runtime_, + propName.c_str(), + Value::createFromJsonUtf8( + *runtime_, + reinterpret_cast(jsonValue->c_str()), + jsonValue->size())); +} + +std::string JSIExecutor::getDescription() { + return "JSI " + runtime_->description(); +} + +void* JSIExecutor::getJavaScriptContext() { + return runtime_.get(); +} + +bool JSIExecutor::isInspectable() { + return runtime_->isInspectable(); +} + +void JSIExecutor::bindBridge() { + std::call_once(bindFlag_, [this] { + SystraceSection s("JSIExecutor::bindBridge (once)"); + Value batchedBridgeValue = + runtime_->global().getProperty(*runtime_, "__fbBatchedBridge"); + if (batchedBridgeValue.isUndefined()) { + Function requireBatchedBridge = runtime_->global().getPropertyAsFunction( + *runtime_, "__fbRequireBatchedBridge"); + batchedBridgeValue = requireBatchedBridge.call(*runtime_); + if (batchedBridgeValue.isUndefined()) { + throw JSINativeException( + "Could not get BatchedBridge, make sure your bundle is packaged correctly"); + } + } + + Object batchedBridge = batchedBridgeValue.asObject(*runtime_); + callFunctionReturnFlushedQueue_ = batchedBridge.getPropertyAsFunction( + *runtime_, "callFunctionReturnFlushedQueue"); + invokeCallbackAndReturnFlushedQueue_ = batchedBridge.getPropertyAsFunction( + *runtime_, "invokeCallbackAndReturnFlushedQueue"); + flushedQueue_ = + batchedBridge.getPropertyAsFunction(*runtime_, "flushedQueue"); + callFunctionReturnResultAndFlushedQueue_ = + batchedBridge.getPropertyAsFunction( + *runtime_, "callFunctionReturnResultAndFlushedQueue"); + }); +} + +void JSIExecutor::callNativeModules(const Value& queue, bool isEndOfBatch) { + SystraceSection s("JSIExecutor::callNativeModules"); + // If this fails, you need to pass a fully functional delegate with a + // module registry to the factory/ctor. + CHECK(delegate_) << "Attempting to use native modules without a delegate"; +#if 0 // maybe useful for debugging + std::string json = runtime_->global().getPropertyAsObject(*runtime_, "JSON") + .getPropertyAsFunction(*runtime_, "stringify").call(*runtime_, queue) + .getString(*runtime_).utf8(*runtime_); +#endif + delegate_->callNativeModules( + *this, dynamicFromValue(*runtime_, queue), isEndOfBatch); +} + +void JSIExecutor::flush() { + SystraceSection s("JSIExecutor::flush"); + if (flushedQueue_) { + callNativeModules(flushedQueue_->call(*runtime_), true); + return; + } + + // When a native module is called from JS, BatchedBridge.enqueueNativeCall() + // is invoked. For that to work, require('BatchedBridge') has to be called, + // and when that happens, __fbBatchedBridge is set as a side effect. + Value batchedBridge = + runtime_->global().getProperty(*runtime_, "__fbBatchedBridge"); + // So here, if __fbBatchedBridge doesn't exist, then we know no native calls + // have happened, and we were able to determine this without forcing + // BatchedBridge to be loaded as a side effect. + if (!batchedBridge.isUndefined()) { + // If calls were made, we bind to the JS bridge methods, and use them to + // get the pending queue of native calls. + bindBridge(); + callNativeModules(flushedQueue_->call(*runtime_), true); + } else if (delegate_) { + // If we have a delegate, we need to call it; we pass a null list to + // callNativeModules, since we know there are no native calls, without + // calling into JS again. If no calls were made and there's no delegate, + // nothing happens, which is correct. + callNativeModules(nullptr, true); + } +} + +Value JSIExecutor::nativeRequire(const Value* args, size_t count) { + if (count > 2 || count == 0) { + throw std::invalid_argument("Got wrong number of args"); + } + + uint32_t moduleId = folly::to(args[0].getNumber()); + uint32_t bundleId = count == 2 ? folly::to(args[1].getNumber()) : 0; + auto module = bundleRegistry_->getModule(bundleId, moduleId); + + runtime_->evaluateJavaScript( + std::make_unique(module.code), module.name); + return facebook::jsi::Value(); +} + +Value JSIExecutor::nativeCallSyncHook(const Value* args, size_t count) { + if (count != 3) { + throw std::invalid_argument("nativeCallSyncHook arg count must be 3"); + } + + if (!args[2].asObject(*runtime_).isArray(*runtime_)) { + throw std::invalid_argument( + folly::to("method parameters should be array")); + } + + MethodCallResult result = delegate_->callSerializableNativeHook( + *this, + static_cast(args[0].getNumber()), // moduleId + static_cast(args[1].getNumber()), // methodId + dynamicFromValue(*runtime_, args[2])); // args + + if (!result.hasValue()) { + return Value::undefined(); + } + return valueFromDynamic(*runtime_, result.value()); +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/jsiexecutor/jsireact/JSIExecutor.h b/ReactCommon/jsiexecutor/jsireact/JSIExecutor.h new file mode 100644 index 000000000..51c1d86b8 --- /dev/null +++ b/ReactCommon/jsiexecutor/jsireact/JSIExecutor.h @@ -0,0 +1,149 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include "JSINativeModules.h" + +#include +#include +#include +#include +#include +#include + +namespace facebook { +namespace react { + +// A JSIScopedTimeoutInvoker is a trampoline-type function for introducing +// timeouts. Call the TimeoutInvoker with a function to execute, the invokee. +// The TimeoutInvoker will immediately invoke it, synchronously on the same +// thread. If the invokee fails to return after some timeout (private to the +// TimeoutInvoker), a soft error may be reported. +// +// If a soft error is reported, the second parameter errorMessageProducer will +// be invoked to produce an error message, which will be included in the soft +// error report. Note that the errorMessageProducer will be invoked +// asynchronously on a different thread. +// +// The timeout behavior does NOT caues the invokee to aborted. If the invokee +// blocks forever, so will the ScopedTimeoutInvoker (but the soft error may +// still be reported). +// +// The invokee is passed by const ref because it is executed synchronously, but +// the errorMessageProducer is passed by value because it must be copied or +// moved for async execution. +// +// Example usage: +// +// int param = ...; +// timeoutInvoker( +// [&]{ someBigWork(param); }, +// [=] -> std::string { +// return "someBigWork, param " + std::to_string(param); +// }) +// +using JSIScopedTimeoutInvoker = std::function& invokee, + std::function errorMessageProducer)>; + +class BigStringBuffer : public jsi::Buffer { + public: + BigStringBuffer(std::unique_ptr script) + : script_(std::move(script)) {} + + size_t size() const override { + return script_->size(); + } + + const uint8_t* data() const override { + return reinterpret_cast(script_->c_str()); + } + + private: + std::unique_ptr script_; +}; + +class JSIExecutor : public JSExecutor { + public: + using Logger = + std::function; + + using RuntimeInstaller = std::function; + + JSIExecutor( + std::shared_ptr runtime, + std::shared_ptr delegate, + Logger logger, + const JSIScopedTimeoutInvoker& timeoutInvoker, + RuntimeInstaller runtimeInstaller); + void loadApplicationScript( + std::unique_ptr script, + std::string sourceURL) override; + void setBundleRegistry(std::unique_ptr) override; + void registerBundle(uint32_t bundleId, const std::string& bundlePath) + override; + void callFunction( + const std::string& moduleId, + const std::string& methodId, + const folly::dynamic& arguments) override; + void invokeCallback(const double callbackId, const folly::dynamic& arguments) + override; + void setGlobalVariable( + std::string propName, + std::unique_ptr jsonValue) override; + std::string getDescription() override; + void* getJavaScriptContext() override; + bool isInspectable() override; + + // An implementation of JSIScopedTimeoutInvoker that simply runs the + // invokee, with no timeout. + static void defaultTimeoutInvoker( + const std::function& invokee, + std::function errorMessageProducer) { + (void)errorMessageProducer; + invokee(); + } + + private: + class NativeModuleProxy; + + void flush(); + void bindBridge(); + void callNativeModules(const jsi::Value& queue, bool isEndOfBatch); + jsi::Value nativeCallSyncHook(const jsi::Value* args, size_t count); + jsi::Value nativeRequire(const jsi::Value* args, size_t count); + + std::shared_ptr runtime_; + std::shared_ptr delegate_; + JSINativeModules nativeModules_; + std::once_flag bindFlag_; + std::unique_ptr bundleRegistry_; + Logger logger_; + JSIScopedTimeoutInvoker scopedTimeoutInvoker_; + RuntimeInstaller runtimeInstaller_; + + folly::Optional callFunctionReturnFlushedQueue_; + folly::Optional invokeCallbackAndReturnFlushedQueue_; + folly::Optional flushedQueue_; + folly::Optional callFunctionReturnResultAndFlushedQueue_; +}; + +class JSIExecutorFactory : public JSExecutorFactory { + public: + explicit JSIExecutorFactory( + std::shared_ptr runtime, + JSIExecutor::Logger logger, + JSIExecutor::RuntimeInstaller); + + std::unique_ptr createJSExecutor( + std::shared_ptr delegate, + std::shared_ptr jsQueue) override; + + private: + std::shared_ptr runtime_; + JSIExecutor::Logger logger_; + JSIExecutor::RuntimeInstaller runtimeInstaller_; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/jsiexecutor/jsireact/JSINativeModules.cpp b/ReactCommon/jsiexecutor/jsireact/JSINativeModules.cpp new file mode 100644 index 000000000..7a5e0673c --- /dev/null +++ b/ReactCommon/jsiexecutor/jsireact/JSINativeModules.cpp @@ -0,0 +1,86 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "jsireact/JSINativeModules.h" + +#include + +#include + +#include + +using namespace facebook::jsi; + +namespace facebook { +namespace react { + +JSINativeModules::JSINativeModules( + std::shared_ptr moduleRegistry) + : m_moduleRegistry(std::move(moduleRegistry)) {} + +Value JSINativeModules::getModule(Runtime& rt, const PropNameID& name) { + if (!m_moduleRegistry) { + return nullptr; + } + + std::string moduleName = name.utf8(rt); + + const auto it = m_objects.find(moduleName); + if (it != m_objects.end()) { + return Value(rt, it->second); + } + + auto module = createModule(rt, moduleName); + if (!module.hasValue()) { + // Allow lookup to continue in the objects own properties, which allows for + // overrides of NativeModules + return nullptr; + } + + auto result = + m_objects.emplace(std::move(moduleName), std::move(*module)).first; + return Value(rt, result->second); +} + +void JSINativeModules::reset() { + m_genNativeModuleJS = nullptr; + m_objects.clear(); +} + +folly::Optional JSINativeModules::createModule( + Runtime& rt, + const std::string& name) { + bool hasLogger(ReactMarker::logTaggedMarker); + if (hasLogger) { + ReactMarker::logTaggedMarker( + ReactMarker::NATIVE_MODULE_SETUP_START, name.c_str()); + } + + if (!m_genNativeModuleJS) { + m_genNativeModuleJS = + rt.global().getPropertyAsFunction(rt, "__fbGenNativeModule"); + } + + auto result = m_moduleRegistry->getConfig(name); + if (!result.hasValue()) { + return nullptr; + } + + Value moduleInfo = m_genNativeModuleJS->call( + rt, + valueFromDynamic(rt, result->config), + static_cast(result->index)); + CHECK(!moduleInfo.isNull()) << "Module returned from genNativeModule is null"; + + folly::Optional module( + moduleInfo.asObject(rt).getPropertyAsObject(rt, "module")); + + if (hasLogger) { + ReactMarker::logTaggedMarker( + ReactMarker::NATIVE_MODULE_SETUP_STOP, name.c_str()); + } + + return module; +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/jsiexecutor/jsireact/JSINativeModules.h b/ReactCommon/jsiexecutor/jsireact/JSINativeModules.h new file mode 100644 index 000000000..6b7ff731a --- /dev/null +++ b/ReactCommon/jsiexecutor/jsireact/JSINativeModules.h @@ -0,0 +1,35 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include + +#include +#include +#include + +namespace facebook { +namespace react { + +/** + * Holds and creates JS representations of the modules in ModuleRegistry + */ +class JSINativeModules { + public: + explicit JSINativeModules(std::shared_ptr moduleRegistry); + jsi::Value getModule(jsi::Runtime& rt, const jsi::PropNameID& name); + void reset(); + + private: + folly::Optional m_genNativeModuleJS; + std::shared_ptr m_moduleRegistry; + std::unordered_map m_objects; + + folly::Optional createModule( + jsi::Runtime& rt, + const std::string& name); +}; + +} // namespace react +} // namespace facebook