diff --git a/ReactCommon/bridge/BUCK b/ReactCommon/bridge/BUCK new file mode 100644 index 00000000000..b306db8fa39 --- /dev/null +++ b/ReactCommon/bridge/BUCK @@ -0,0 +1,85 @@ +def kwargs_add(base_kwargs, **new_kwargs): + ret_kwargs = dict(base_kwargs) + for name, add_value in new_kwargs.iteritems(): + if name in ret_kwargs: + # Don't use +=, it will modify base_kwargs + ret_kwargs[name] = ret_kwargs[name] + add_value + else: + ret_kwargs[name] = add_value + return ret_kwargs + +if THIS_IS_FBANDROID: + include_defs('//ReactAndroid/DEFS') + + def react_library(**kwargs): + kwargs = kwargs_add( + kwargs, + # We depend on JSC, support the same platforms + supported_platforms_regex = '^android-(armv7|x86)$', + compiler_flags = [ + '-Wno-pessimizing-move', + ], + deps = [ + '//xplat/folly:molly', + ]) + + cxx_library( + name = 'bridge', + **kwargs_add( + kwargs, + preprocessor_flags = [ + '-DWITH_JSC_EXTRA_TRACING=1', + '-DWITH_JSC_MEMORY_PRESSURE=1', + '-DWITH_REACT_INTERNAL_SETTINGS=1', + '-DWITH_FB_MEMORY_PROFILING=1', + ], + deps = JSC_DEPS + ) + ) + +elif THIS_IS_FBOBJC: + + def react_library(**kwargs): + ios_library( + name = 'bridge', + header_path_prefix = "cxxreact", + frameworks = [ + '$SDKROOT/System/Library/Frameworks/JavaScriptCore.framework', + ], + **kwargs_add( + kwargs, + deps = [ + '//xplat/folly:molly', + ] + ) + ) + +LOCAL_HEADERS = [ + 'JSCTracing.h', + 'JSCLegacyProfiler.h', + 'JSCMemory.h', + 'JSCPerfStats.h', +] + +react_library( + soname = 'libreactnativefb.so', + header_namespace = 'cxxreact', + force_static = True, + srcs = glob(['*.cpp']), + headers = LOCAL_HEADERS, + preprocessor_flags = [ + '-DLOG_TAG="ReactNative"', + '-DWITH_FBSYSTRACE=1', + ], + compiler_flags = [ + '-Wall', + '-fexceptions', + '-fvisibility=hidden', + '-frtti', + ], + exported_headers = glob(['*.h'], excludes=LOCAL_HEADERS), + deps = [ + '//xplat/fbsystrace:fbsystrace', + ], + visibility = [ 'PUBLIC' ], +) diff --git a/ReactCommon/bridge/Bridge.cpp b/ReactCommon/bridge/Bridge.cpp new file mode 100644 index 00000000000..adaf8ac0f1a --- /dev/null +++ b/ReactCommon/bridge/Bridge.cpp @@ -0,0 +1,269 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "Bridge.h" + +#ifdef WITH_FBSYSTRACE +#include +using fbsystrace::FbSystraceSection; +using fbsystrace::FbSystraceAsyncFlow; +#endif +#include +#include +#include + +#include "Platform.h" + +namespace facebook { +namespace react { + +Bridge::Bridge( + JSExecutorFactory* jsExecutorFactory, + std::shared_ptr jsQueue, + std::unique_ptr executorTokenFactory, + std::unique_ptr callback) : + m_callback(std::move(callback)), + m_destroyed(std::make_shared(false)), + m_executorTokenFactory(std::move(executorTokenFactory)) { + std::unique_ptr mainExecutor = jsExecutorFactory->createJSExecutor(this, jsQueue); + // cached to avoid locked map lookup in the common case + m_mainExecutor = mainExecutor.get(); + m_mainExecutorToken = folly::make_unique(registerExecutor( + std::move(mainExecutor), jsQueue)); +} + +// This must be called on the same thread on which the constructor was called. +Bridge::~Bridge() { + CHECK(*m_destroyed) << "Bridge::destroy() must be called before deallocating the Bridge!"; +} + +void Bridge::loadApplicationScript(const std::string& script, const std::string& sourceURL) { + runOnExecutorQueue(*m_mainExecutorToken, [=] (JSExecutor* executor) { + executor->loadApplicationScript(script, sourceURL); + }); +} + +void Bridge::loadApplicationUnbundle( + std::unique_ptr unbundle, + const std::string& startupScript, + const std::string& startupScriptSourceURL) { + runOnExecutorQueue( + *m_mainExecutorToken, + [unbundle=folly::makeMoveWrapper(std::move(unbundle)), startupScript, startupScriptSourceURL] + (JSExecutor* executor) mutable { + + executor->setJSModulesUnbundle(unbundle.move()); + executor->loadApplicationScript(startupScript, startupScriptSourceURL); + }); +} + +void Bridge::callFunction( + ExecutorToken executorToken, + const std::string& moduleId, + const std::string& methodId, + const folly::dynamic& arguments, + const std::string& tracingName) { + #ifdef WITH_FBSYSTRACE + int systraceCookie = m_systraceCookie++; + FbSystraceAsyncFlow::begin( + TRACE_TAG_REACT_CXX_BRIDGE, + tracingName.c_str(), + systraceCookie); + #endif + + runOnExecutorQueue(executorToken, [moduleId, methodId, arguments, tracingName, systraceCookie] (JSExecutor* executor) { + #ifdef WITH_FBSYSTRACE + FbSystraceAsyncFlow::end( + TRACE_TAG_REACT_CXX_BRIDGE, + tracingName.c_str(), + systraceCookie); + FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, tracingName.c_str()); + #endif + + // This is safe because we are running on the executor's thread: it won't + // destruct until after it's been unregistered (which we check above) and + // that will happen on this thread + executor->callFunction(moduleId, methodId, arguments); + }); +} + +void Bridge::invokeCallback(ExecutorToken executorToken, const double callbackId, const folly::dynamic& arguments) { + #ifdef WITH_FBSYSTRACE + int systraceCookie = m_systraceCookie++; + FbSystraceAsyncFlow::begin( + TRACE_TAG_REACT_CXX_BRIDGE, + "", + systraceCookie); + #endif + + runOnExecutorQueue(executorToken, [callbackId, arguments, systraceCookie] (JSExecutor* executor) { + #ifdef WITH_FBSYSTRACE + FbSystraceAsyncFlow::end( + TRACE_TAG_REACT_CXX_BRIDGE, + "", + systraceCookie); + FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "Bridge.invokeCallback"); + #endif + + executor->invokeCallback(callbackId, arguments); + }); +} + +void Bridge::setGlobalVariable(const std::string& propName, const std::string& jsonValue) { + runOnExecutorQueue(*m_mainExecutorToken, [=] (JSExecutor* executor) { + executor->setGlobalVariable(propName, jsonValue); + }); +} + +void* Bridge::getJavaScriptContext() { + // TODO(cjhopman): this seems unsafe unless we require that it is only called on the main js queue. + return m_mainExecutor->getJavaScriptContext(); +} + +bool Bridge::supportsProfiling() { + // Intentionally doesn't post to jsqueue. supportsProfiling() can be called from any thread. + return m_mainExecutor->supportsProfiling(); +} + +void Bridge::startProfiler(const std::string& title) { + runOnExecutorQueue(*m_mainExecutorToken, [=] (JSExecutor* executor) { + executor->startProfiler(title); + }); +} + +void Bridge::stopProfiler(const std::string& title, const std::string& filename) { + runOnExecutorQueue(*m_mainExecutorToken, [=] (JSExecutor* executor) { + executor->stopProfiler(title, filename); + }); +} + +void Bridge::handleMemoryPressureModerate() { + runOnExecutorQueue(*m_mainExecutorToken, [=] (JSExecutor* executor) { + executor->handleMemoryPressureModerate(); + }); +} + +void Bridge::handleMemoryPressureCritical() { + runOnExecutorQueue(*m_mainExecutorToken, [=] (JSExecutor* executor) { + executor->handleMemoryPressureCritical(); + }); +} + +void Bridge::callNativeModules(JSExecutor& executor, const std::string& callJSON, bool isEndOfBatch) { + // This is called by the executor and thus runs on the executor's own queue. + // This means that the executor has not yet been unregistered (and we are + // guaranteed to be able to get the token). + m_callback->onCallNativeModules(getTokenForExecutor(executor), callJSON, isEndOfBatch); +} + +MethodCallResult Bridge::callSerializableNativeHook(unsigned int moduleId, unsigned int methodId, const std::string& argsJSON) { + return m_callback->callSerializableNativeHook(*m_mainExecutorToken, moduleId, methodId, folly::parseJson(argsJSON)); +} + +ExecutorToken Bridge::getMainExecutorToken() const { + return *m_mainExecutorToken.get(); +} + +ExecutorToken Bridge::registerExecutor( + std::unique_ptr executor, + std::shared_ptr messageQueueThread) { + auto token = m_executorTokenFactory->createExecutorToken(); + + std::lock_guard registrationGuard(m_registrationMutex); + + CHECK(m_executorTokenMap.find(executor.get()) == m_executorTokenMap.end()) + << "Trying to register an already registered executor!"; + + m_executorTokenMap.emplace(executor.get(), token); + m_executorMap.emplace( + token, + folly::make_unique(std::move(executor), std::move(messageQueueThread))); + + return token; +} + +std::unique_ptr Bridge::unregisterExecutor(ExecutorToken executorToken) { + std::unique_ptr executor; + + { + std::lock_guard registrationGuard(m_registrationMutex); + + auto it = m_executorMap.find(executorToken); + CHECK(it != m_executorMap.end()) + << "Trying to unregister an executor that was never registered!"; + + executor = std::move(it->second->executor_); + m_executorMap.erase(it); + m_executorTokenMap.erase(executor.get()); + } + + m_callback->onExecutorUnregistered(executorToken); + + return executor; +} + +MessageQueueThread* Bridge::getMessageQueueThread(const ExecutorToken& executorToken) { + std::lock_guard registrationGuard(m_registrationMutex); + auto it = m_executorMap.find(executorToken); + if (it == m_executorMap.end()) { + return nullptr; + } + return it->second->messageQueueThread_.get(); +} + +JSExecutor* Bridge::getExecutor(const ExecutorToken& executorToken) { + std::lock_guard registrationGuard(m_registrationMutex); + auto it = m_executorMap.find(executorToken); + if (it == m_executorMap.end()) { + return nullptr; + } + return it->second->executor_.get(); +} + +ExecutorToken Bridge::getTokenForExecutor(JSExecutor& executor) { + std::lock_guard registrationGuard(m_registrationMutex); + return m_executorTokenMap.at(&executor); +} + +void Bridge::destroy() { + auto executorMessageQueueThread = getMessageQueueThread(*m_mainExecutorToken); + executorMessageQueueThread->runOnQueueSync([this, &executorMessageQueueThread] { + executorMessageQueueThread->quitSynchronous(); + *m_destroyed = true; + m_mainExecutor = nullptr; + std::unique_ptr mainExecutor = unregisterExecutor(*m_mainExecutorToken); + mainExecutor->destroy(); + }); +} + +void Bridge::runOnExecutorQueue(ExecutorToken executorToken, std::function task) { + if (*m_destroyed) { + return; + } + + auto executorMessageQueueThread = getMessageQueueThread(executorToken); + if (executorMessageQueueThread == nullptr) { + LOG(WARNING) << "Dropping JS action for executor that has been unregistered..."; + return; + } + + std::shared_ptr isDestroyed = m_destroyed; + executorMessageQueueThread->runOnQueue([this, isDestroyed, executorToken, task=std::move(task)] { + if (*isDestroyed) { + return; + } + + JSExecutor *executor = getExecutor(executorToken); + if (executor == nullptr) { + LOG(WARNING) << "Dropping JS call for executor that has been unregistered..."; + return; + } + + // The executor is guaranteed to be valid for the duration of the task because: + // 1. the executor is only destroyed after it is unregistered + // 2. the executor is unregistered on this queue + // 3. we just confirmed that the executor hasn't been unregistered above + task(executor); + }); +} + +} } diff --git a/ReactCommon/bridge/Bridge.h b/ReactCommon/bridge/Bridge.h new file mode 100644 index 00000000000..13bf3afaa9a --- /dev/null +++ b/ReactCommon/bridge/Bridge.h @@ -0,0 +1,173 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include +#include + +#include + +#include "Executor.h" +#include "ExecutorToken.h" +#include "ExecutorTokenFactory.h" +#include "JSModulesUnbundle.h" +#include "MessageQueueThread.h" +#include "MethodCall.h" +#include "NativeModule.h" +#include "Value.h" + +namespace folly { + +struct dynamic; + +} + +namespace facebook { +namespace react { + +class BridgeCallback { +public: + virtual ~BridgeCallback() {}; + + virtual void onCallNativeModules( + ExecutorToken executorToken, + const std::string& callJSON, + bool isEndOfBatch) = 0; + + virtual void onExecutorUnregistered(ExecutorToken executorToken) = 0; + + virtual MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int moduleId, unsigned int methodId, folly::dynamic&& args) = 0; +}; + +class Bridge; +class ExecutorRegistration { +public: + ExecutorRegistration( + std::unique_ptr executor, + std::shared_ptr executorMessageQueueThread) : + executor_(std::move(executor)), + messageQueueThread_(executorMessageQueueThread) {} + + std::unique_ptr executor_; + std::shared_ptr messageQueueThread_; +}; + +class Bridge { +public: + /** + * This must be called on the main JS thread. + */ + Bridge( + JSExecutorFactory* jsExecutorFactory, + std::shared_ptr jsQueue, + std::unique_ptr executorTokenFactory, + std::unique_ptr callback); + virtual ~Bridge(); + + /** + * Executes a function with the module ID and method ID and any additional + * arguments in JS. + */ + void callFunction( + ExecutorToken executorToken, + const std::string& moduleId, + const std::string& methodId, + const folly::dynamic& args, + const std::string& tracingName); + + /** + * Invokes a callback with the cbID, and optional additional arguments in JS. + */ + void invokeCallback(ExecutorToken executorToken, const double callbackId, const folly::dynamic& args); + + /** + * Starts the JS application from an "bundle", i.e. a JavaScript file that + * contains code for all modules and a runtime that resolves and + * executes modules. + */ + void loadApplicationScript(const std::string& script, const std::string& sourceURL); + + /** + * An "unbundle" is a backend that stores and injects JavaScript modules as + * individual scripts, rather than bundling all of them into a single scrupt. + * + * Loading an unbundle means setting the storage backend and executing the + * startup script. + */ + void loadApplicationUnbundle( + std::unique_ptr unbundle, + const std::string& startupCode, + const std::string& sourceURL); + void setGlobalVariable(const std::string& propName, const std::string& jsonValue); + void* getJavaScriptContext(); + bool supportsProfiling(); + void startProfiler(const std::string& title); + void stopProfiler(const std::string& title, const std::string& filename); + void handleMemoryPressureModerate(); + void handleMemoryPressureCritical(); + + /** + * Invokes a set of native module calls on behalf of the given executor. + * + * TODO: get rid of isEndOfBatch + */ + void callNativeModules(JSExecutor& executor, const std::string& callJSON, bool isEndOfBatch); + + MethodCallResult callSerializableNativeHook(unsigned int moduleId, unsigned int methodId, const std::string& argsJSON); + + /** + * Returns the ExecutorToken corresponding to the main JSExecutor. + */ + ExecutorToken getMainExecutorToken() const; + + /** + * Registers the given JSExecutor which runs on the given MessageQueueThread + * with the Bridge. Part of this registration is transfering ownership of this + * JSExecutor to the Bridge for the duration of the registration. + * + * Returns a ExecutorToken which can be used to refer to this JSExecutor + * in the Bridge. + */ + ExecutorToken registerExecutor( + std::unique_ptr executor, + std::shared_ptr executorMessageQueueThread); + + /** + * Unregisters a JSExecutor that was previously registered with this Bridge + * using registerExecutor. Use the ExecutorToken returned from this + * registerExecutor call. This method will return ownership of the unregistered + * executor to the caller for it to retain or tear down. + * + * Returns ownership of the unregistered executor. + */ + std::unique_ptr unregisterExecutor(ExecutorToken executorToken); + + /** + * Synchronously tears down the bridge and the main executor. + */ + void destroy(); +private: + void runOnExecutorQueue(ExecutorToken token, std::function task); + std::unique_ptr m_callback; + // This is used to avoid a race condition where a proxyCallback gets queued after ~Bridge(), + // on the same thread. In that case, the callback will try to run the task on m_callback which + // will have been destroyed within ~Bridge(), thus causing a SIGSEGV. + std::shared_ptr m_destroyed; + JSExecutor* m_mainExecutor; + std::unique_ptr m_mainExecutorToken; + std::unique_ptr m_executorTokenFactory; + std::unordered_map m_executorTokenMap; + std::unordered_map> m_executorMap; + std::mutex m_registrationMutex; + #ifdef WITH_FBSYSTRACE + std::atomic_uint_least32_t m_systraceCookie = ATOMIC_VAR_INIT(); + #endif + + MessageQueueThread* getMessageQueueThread(const ExecutorToken& executorToken); + JSExecutor* getExecutor(const ExecutorToken& executorToken); + inline ExecutorToken getTokenForExecutor(JSExecutor& executor); +}; + +} } diff --git a/ReactCommon/bridge/Executor.h b/ReactCommon/bridge/Executor.h new file mode 100644 index 00000000000..9ea35c4fde2 --- /dev/null +++ b/ReactCommon/bridge/Executor.h @@ -0,0 +1,80 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include +#include + +#include "JSModulesUnbundle.h" + +namespace folly { + +struct dynamic; + +} + +namespace facebook { +namespace react { + +class Bridge; +class JSExecutor; +class MessageQueueThread; + +class JSExecutorFactory { +public: + virtual std::unique_ptr createJSExecutor( + Bridge *bridge, std::shared_ptr jsQueue) = 0; + virtual ~JSExecutorFactory() {}; +}; + +class JSExecutor { +public: + /** + * Execute an application script bundle in the JS context. + */ + virtual void loadApplicationScript( + const std::string& script, + const std::string& sourceURL) = 0; + + /** + * Add an application "unbundle" file + */ + virtual void setJSModulesUnbundle(std::unique_ptr bundle) = 0; + + /** + * Executes BatchedBridge.callFunctionReturnFlushedQueue with the module ID, + * method ID and optional additional arguments in JS. The executor is responsible + * for using Bridge->callNativeModules to invoke any necessary native modules methods. + */ + virtual void callFunction(const std::string& moduleId, const std::string& methodId, const folly::dynamic& arguments) = 0; + + /** + * Executes BatchedBridge.invokeCallbackAndReturnFlushedQueue with the cbID, + * and optional additional arguments in JS and returns the next queue. The executor + * is responsible for using Bridge->callNativeModules to invoke any necessary + * native modules methods. + */ + virtual void invokeCallback(const double callbackId, const folly::dynamic& arguments) = 0; + + virtual void setGlobalVariable( + const std::string& propName, + const std::string& jsonValue) = 0; + virtual void* getJavaScriptContext() { + return nullptr; + }; + virtual bool supportsProfiling() { + return false; + }; + virtual void startProfiler(const std::string &titleString) {}; + virtual void stopProfiler(const std::string &titleString, const std::string &filename) {}; + virtual void handleMemoryPressureModerate() {}; + virtual void handleMemoryPressureCritical() { + handleMemoryPressureModerate(); + }; + virtual void destroy() {}; + virtual ~JSExecutor() {}; +}; + +} } diff --git a/ReactCommon/bridge/ExecutorToken.h b/ReactCommon/bridge/ExecutorToken.h new file mode 100644 index 00000000000..a630e90d34a --- /dev/null +++ b/ReactCommon/bridge/ExecutorToken.h @@ -0,0 +1,54 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include "Executor.h" + +namespace facebook { +namespace react { + +/** + * This class exists so that we have a type for the shared_ptr on ExecutorToken + * that implements a virtual destructor. + */ +class PlatformExecutorToken { +public: + virtual ~PlatformExecutorToken() {} +}; + +/** + * Class corresponding to a JS VM that can call into native modules. This is + * passed to native modules to allow their JS module calls/callbacks to be + * routed back to the proper JS VM on the proper thread. + */ +class ExecutorToken { +public: + /** + * This should only be used by the implementation of the platform ExecutorToken. + * Do not use as a client of ExecutorToken. + */ + explicit ExecutorToken(std::shared_ptr platformToken) : + platformToken_(platformToken) {} + + std::shared_ptr getPlatformExecutorToken() const { + return platformToken_; + } + + bool operator==(const ExecutorToken& other) const { + return platformToken_.get() == other.platformToken_.get(); + } + +private: + std::shared_ptr platformToken_; +}; + +} } + +namespace std { + template<> + struct hash { + const size_t operator()(const facebook::react::ExecutorToken& token) const { + return (size_t) token.getPlatformExecutorToken().get(); + } + }; +} diff --git a/ReactCommon/bridge/ExecutorTokenFactory.h b/ReactCommon/bridge/ExecutorTokenFactory.h new file mode 100644 index 00000000000..5d2d4217907 --- /dev/null +++ b/ReactCommon/bridge/ExecutorTokenFactory.h @@ -0,0 +1,25 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include "ExecutorToken.h" +#include "Executor.h" + +namespace facebook { +namespace react { + +/** + * Class that knows how to create the platform-specific implementation + * of ExecutorToken. + */ +class ExecutorTokenFactory { +public: + virtual ~ExecutorTokenFactory() {} + + /** + * Creates a new ExecutorToken. + */ + virtual ExecutorToken createExecutorToken() const = 0; +}; + +} } diff --git a/ReactCommon/bridge/Instance.cpp b/ReactCommon/bridge/Instance.cpp new file mode 100644 index 00000000000..3982916ffeb --- /dev/null +++ b/ReactCommon/bridge/Instance.cpp @@ -0,0 +1,226 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "Instance.h" + +#include "Executor.h" +#include "MethodCall.h" + +#ifdef WITH_FBSYSTRACE +#include +using fbsystrace::FbSystraceSection; +#endif + +#include +#include + +#include + +#include +#include +#include +#include + +namespace facebook { +namespace react { + +namespace { +struct ExecutorTokenFactoryImpl : ExecutorTokenFactory { + ExecutorTokenFactoryImpl(InstanceCallback* callback): callback_(callback) {} + virtual ExecutorToken createExecutorToken() const { + return callback_->createExecutorToken(); + } + private: + InstanceCallback* callback_; +}; +} + +class Instance::BridgeCallbackImpl : public BridgeCallback { + public: + explicit BridgeCallbackImpl(Instance* instance) : instance_(instance) {} + virtual void onCallNativeModules( + ExecutorToken executorToken, + const std::string& calls, + bool isEndOfBatch) override { + instance_->callNativeModules(executorToken, calls, isEndOfBatch); + } + + virtual void onExecutorUnregistered(ExecutorToken executorToken) override { + // TODO(cjhopman): implement this. + } + + virtual MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int moduleId, unsigned int hookId, folly::dynamic&& params) override { + return instance_->callSerializableNativeHook(token, moduleId, hookId, std::move(params)); + } + private: + Instance* instance_; +}; + +Instance::~Instance() { + if (nativeQueue_) { + nativeQueue_->quitSynchronous(); + bridge_->destroy(); + } +} + +void Instance::initializeBridge( + std::unique_ptr callback, + std::shared_ptr jsef, + std::shared_ptr jsQueue, + std::unique_ptr nativeQueue, + std::shared_ptr moduleRegistry, + folly::dynamic jsModuleDescriptions) { + callback_ = std::move(callback); + nativeQueue_ = std::move(nativeQueue); + jsQueue_ = jsQueue; + moduleRegistry_ = moduleRegistry; + + jsQueue_->runOnQueueSync([this, &jsef] { + bridge_ = folly::make_unique( + jsef.get(), jsQueue_, folly::make_unique(callback_.get()), folly::make_unique(this)); + }); +#ifdef WITH_FBSYSTRACE + FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "setBatchedBridgeConfig"); +#endif + + CHECK(bridge_); + + folly::dynamic nativeModuleDescriptions = folly::dynamic::array(); + { +#ifdef WITH_FBSYSTRACE + FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "collectNativeModuleDescriptions"); +#endif + nativeModuleDescriptions = moduleRegistry_->moduleDescriptions(); + } + + folly::dynamic config = + folly::dynamic::object + ("remoteModuleConfig", std::move(nativeModuleDescriptions)) + ("localModulesConfig", std::move(jsModuleDescriptions)); + +#ifdef WITH_FBSYSTRACE + FbSystraceSection t(TRACE_TAG_REACT_CXX_BRIDGE, "setGlobalVariable"); +#endif + setGlobalVariable( + "__fbBatchedBridgeConfig", + folly::toJson(config)); +} + +void Instance::loadScriptFromString(const std::string& string, + const std::string& sourceURL) { + callback_->incrementPendingJSCalls(); +#ifdef WITH_FBSYSTRACE + FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, + "reactbridge_xplat_loadScriptFromString", + "sourceURL", sourceURL); +#endif + // TODO mhorowitz: ReactMarker around loadApplicationScript + bridge_->loadApplicationScript(string, sourceURL); +} + +void Instance::loadScriptFromFile(const std::string& filename, + const std::string& sourceURL) { + // TODO mhorowitz: ReactMarker around file read + std::string script; + { +#ifdef WITH_FBSYSTRACE + FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, + "reactbridge_xplat_loadScriptFromFile", + "fileName", filename); +#endif + + std::ifstream jsfile(filename); + if (!jsfile) { + LOG(ERROR) << "Unable to load script from file" << filename; + } else { + jsfile.seekg(0, std::ios::end); + script.reserve(jsfile.tellg()); + jsfile.seekg(0, std::ios::beg); + script.assign( + std::istreambuf_iterator(jsfile), + std::istreambuf_iterator()); + } + } + + loadScriptFromString(script, sourceURL); +} + +void Instance::loadUnbundle(std::unique_ptr unbundle, + const std::string& startupScript, + const std::string& startupScriptSourceURL) { + callback_->incrementPendingJSCalls(); +#ifdef WITH_FBSYSTRACE + FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, + "reactbridge_xplat_setJSModulesUnbundle"); +#endif + bridge_->loadApplicationUnbundle(std::move(unbundle), startupScript, startupScriptSourceURL); +} + +bool Instance::supportsProfiling() { + return bridge_->supportsProfiling(); +} + +void Instance::startProfiler(const std::string& title) { + return bridge_->startProfiler(title); +} + +void Instance::stopProfiler(const std::string& title, const std::string& filename) { + return bridge_->stopProfiler(title, filename); +} + +void Instance::setGlobalVariable(const std::string& propName, + const std::string& jsonValue) { + bridge_->setGlobalVariable(propName, jsonValue); +} + +void Instance::callJSFunction(ExecutorToken token, const std::string& module, const std::string& method, + folly::dynamic&& params, const std::string& tracingName) { +#ifdef WITH_FBSYSTRACE + FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, tracingName.c_str()); +#endif + callback_->incrementPendingJSCalls(); + bridge_->callFunction(token, module, method, std::move(params), tracingName); +} + +void Instance::callJSCallback(ExecutorToken token, uint64_t callbackId, folly::dynamic&& params) { +#ifdef WITH_FBSYSTRACE + FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, ""); +#endif + callback_->incrementPendingJSCalls(); + bridge_->invokeCallback(token, (double) callbackId, std::move(params)); +} + +ExecutorToken Instance::getMainExecutorToken() { + return bridge_->getMainExecutorToken(); +} + +void Instance::callNativeModules(ExecutorToken token, const std::string& calls, bool isEndOfBatch) { + // TODO mhorowitz: avoid copying calls here. + nativeQueue_->runOnQueue([this, token, calls, isEndOfBatch] { + try { + // An exception anywhere in here stops processing of the batch. This + // was the behavior of the Android bridge, and since exception handling + // terminates the whole bridge, there's not much point in continuing. + for (auto& call : react::parseMethodCalls(calls)) { + moduleRegistry_->callNativeMethod( + token, call.moduleId, call.methodId, std::move(call.arguments), call.callId); + } + if (isEndOfBatch) { + callback_->onBatchComplete(); + callback_->decrementPendingJSCalls(); + } + } catch (const std::exception& e) { + LOG(ERROR) << folly::exceptionStr(e).toStdString(); + callback_->onNativeException(folly::exceptionStr(e).toStdString()); + } catch (...) { + LOG(ERROR) << "Unknown exception"; + callback_->onNativeException("Unknown exception"); + } + }); +} + +MethodCallResult Instance::callSerializableNativeHook(ExecutorToken token, unsigned int moduleId, unsigned int methodId, folly::dynamic&& params) { + return moduleRegistry_->callSerializableNativeHook(token, moduleId, methodId, std::move(params)); +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/bridge/Instance.h b/ReactCommon/bridge/Instance.h new file mode 100644 index 00000000000..7d8b57d2904 --- /dev/null +++ b/ReactCommon/bridge/Instance.h @@ -0,0 +1,68 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include + +#include "Bridge.h" +#include "ModuleRegistry.h" +#include "NativeModule.h" + +namespace facebook { +namespace react { + +class JSExecutorFactory; + +struct InstanceCallback { + virtual ~InstanceCallback() {} + virtual void onBatchComplete() = 0; + virtual void incrementPendingJSCalls() = 0; + virtual void decrementPendingJSCalls() = 0; + virtual void onNativeException(const std::string& what) = 0; + virtual ExecutorToken createExecutorToken() = 0; +}; + +class Instance { + public: + ~Instance(); + void initializeBridge( + std::unique_ptr callback, + std::shared_ptr jsef, + std::shared_ptr jsQueue, + std::unique_ptr nativeQueue, + std::shared_ptr moduleRegistry, + folly::dynamic jsModuleDescriptions); + void loadScriptFromString(const std::string& string, const std::string& sourceURL); + void loadScriptFromFile(const std::string& filename, const std::string& sourceURL); + void loadUnbundle( + std::unique_ptr unbundle, + const std::string& startupScript, + const std::string& startupScriptSourceURL); + bool supportsProfiling(); + void startProfiler(const std::string& title); + void stopProfiler(const std::string& title, const std::string& filename); + void setGlobalVariable(const std::string& propName, const std::string& jsonValue); + void callJSFunction(ExecutorToken token, const std::string& module, const std::string& method, + folly::dynamic&& params, const std::string& tracingName); + void callJSCallback(ExecutorToken token, uint64_t callbackId, folly::dynamic&& params); + MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int moduleId, unsigned int methodId, folly::dynamic&& args); + ExecutorToken getMainExecutorToken(); + + private: + class BridgeCallbackImpl; + + void callNativeModules(ExecutorToken token, const std::string& calls, bool isEndOfBatch); + + std::unique_ptr callback_; + std::shared_ptr moduleRegistry_; + // TODO #10487027: clean up the ownership of this. + std::shared_ptr jsQueue_; + std::unique_ptr nativeQueue_; + + std::unique_ptr bridge_; +}; + +} +} diff --git a/ReactCommon/bridge/JSCExecutor.cpp b/ReactCommon/bridge/JSCExecutor.cpp new file mode 100644 index 00000000000..b708b0dfe4e --- /dev/null +++ b/ReactCommon/bridge/JSCExecutor.cpp @@ -0,0 +1,653 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "JSCExecutor.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Bridge.h" +#include "JSCHelpers.h" +#include "Platform.h" +#include "Value.h" + +#ifdef WITH_JSC_EXTRA_TRACING +#include "JSCTracing.h" +#include "JSCLegacyProfiler.h" +#include +#endif + +#ifdef WITH_JSC_MEMORY_PRESSURE +#include +#endif + +#ifdef WITH_FBSYSTRACE +#include +using fbsystrace::FbSystraceSection; +#endif + +#ifdef WITH_FB_MEMORY_PROFILING +#include "JSCMemory.h" +#endif + +#ifdef WITH_FB_JSC_TUNING +#include +#endif + +#ifdef JSC_HAS_PERF_STATS_API +#include "JSCPerfStats.h" +#endif + +namespace facebook { +namespace react { + +static std::unordered_map s_globalContextRefToJSCExecutor; + +static JSValueRef nativeInjectHMRUpdate( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception); + +static std::string executeJSCallWithJSC( + JSGlobalContextRef ctx, + const std::string& methodName, + const std::vector& arguments) { + #ifdef WITH_FBSYSTRACE + FbSystraceSection s( + TRACE_TAG_REACT_CXX_BRIDGE, "JSCExecutor.executeJSCall", + "method", methodName); + #endif + + // Evaluate script with JSC + folly::dynamic jsonArgs(arguments.begin(), arguments.end()); + auto js = folly::to( + "__fbBatchedBridge.", methodName, ".apply(null, ", + folly::toJson(jsonArgs), ")"); + auto result = evaluateScript(ctx, String(js.c_str()), nullptr); + return Value(ctx, result).toJSONString(); +} + +std::unique_ptr JSCExecutorFactory::createJSExecutor( + Bridge *bridge, std::shared_ptr jsQueue) { + return std::unique_ptr( + new JSCExecutor(bridge, jsQueue, cacheDir_, m_jscConfig)); +} + +JSCExecutor::JSCExecutor(Bridge *bridge, std::shared_ptr messageQueueThread, + const std::string& cacheDir, const folly::dynamic& jscConfig) : + m_bridge(bridge), + m_deviceCacheDir(cacheDir), + m_messageQueueThread(messageQueueThread), + m_jscConfig(jscConfig) { + initOnJSVMThread(); +} + +JSCExecutor::JSCExecutor( + Bridge *bridge, + std::shared_ptr messageQueueThread, + int workerId, + JSCExecutor *owner, + const std::string& script, + const std::unordered_map& globalObjAsJSON, + const folly::dynamic& jscConfig) : + m_bridge(bridge), + m_workerId(workerId), + m_owner(owner), + m_deviceCacheDir(owner->m_deviceCacheDir), + m_messageQueueThread(messageQueueThread), + m_jscConfig(jscConfig) { + // We post initOnJSVMThread here so that the owner doesn't have to wait for + // initialization on its own thread + m_messageQueueThread->runOnQueue([this, script, globalObjAsJSON] () { + initOnJSVMThread(); + + installGlobalFunction(m_context, "postMessage", nativePostMessage); + + for (auto& it : globalObjAsJSON) { + setGlobalVariable(it.first, it.second); + } + + // Try to load the script from the network if script is a URL + // NB: For security, this will only work in debug builds + std::string scriptSrc; + if (script.find("http://") == 0 || script.find("https://") == 0) { + std::stringstream outfileBuilder; + outfileBuilder << m_deviceCacheDir << "/workerScript" << m_workerId << ".js"; + scriptSrc = WebWorkerUtil::loadScriptFromNetworkSync(script, outfileBuilder.str()); + } else { + // TODO(9604438): Protect against script does not exist + scriptSrc = WebWorkerUtil::loadScriptFromAssets(script); + } + + // TODO(9994180): Throw on error + loadApplicationScript(scriptSrc, script); + }); +} + +JSCExecutor::~JSCExecutor() { + CHECK(*m_isDestroyed) << "JSCExecutor::destroy() must be called before its destructor!"; +} + +void JSCExecutor::destroy() { + *m_isDestroyed = true; + m_messageQueueThread->runOnQueueSync([this] () { + terminateOnJSVMThread(); + }); +} + +void JSCExecutor::initOnJSVMThread() { + #if defined(WITH_FB_JSC_TUNING) + configureJSCForAndroid(m_jscConfig); + #endif + m_context = JSGlobalContextCreateInGroup(nullptr, nullptr); + s_globalContextRefToJSCExecutor[m_context] = this; + installGlobalFunction(m_context, "nativeFlushQueueImmediate", nativeFlushQueueImmediate); + installGlobalFunction(m_context, "nativeStartWorker", nativeStartWorker); + installGlobalFunction(m_context, "nativePostMessageToWorker", nativePostMessageToWorker); + installGlobalFunction(m_context, "nativeTerminateWorker", nativeTerminateWorker); + installGlobalFunction(m_context, "nativeInjectHMRUpdate", nativeInjectHMRUpdate); + installGlobalFunction(m_context, "nativeCallSyncHook", nativeCallSyncHook); + + installGlobalFunction(m_context, "nativeLoggingHook", JSNativeHooks::loggingHook); + installGlobalFunction(m_context, "nativePerformanceNow", JSNativeHooks::nowHook); + + #ifdef WITH_JSC_EXTRA_TRACING + addNativeTracingHooks(m_context); + addNativeProfilingHooks(m_context); + PerfLogging::installNativeHooks(m_context); + #endif + + #ifdef WITH_FB_MEMORY_PROFILING + addNativeMemoryHooks(m_context); + #endif + + #ifdef JSC_HAS_PERF_STATS_API + addJSCPerfStatsHooks(m_context); + #endif + + #if defined(WITH_FB_JSC_TUNING) + configureJSContextForAndroid(m_context, m_jscConfig, m_deviceCacheDir); + #endif +} + +void JSCExecutor::terminateOnJSVMThread() { + // terminateOwnedWebWorker mutates m_ownedWorkers so collect all the workers + // to terminate first + std::vector workerIds; + for (auto& it : m_ownedWorkers) { + workerIds.push_back(it.first); + } + for (int workerId : workerIds) { + terminateOwnedWebWorker(workerId); + } + + s_globalContextRefToJSCExecutor.erase(m_context); + JSGlobalContextRelease(m_context); + m_context = nullptr; +} + +void JSCExecutor::loadApplicationScript( + const std::string& script, + const std::string& sourceURL) { + ReactMarker::logMarker("loadApplicationScript_startStringConvert"); + String jsScript = String::createExpectingAscii(script); + ReactMarker::logMarker("loadApplicationScript_endStringConvert"); + + String jsSourceURL(sourceURL.c_str()); + #ifdef WITH_FBSYSTRACE + FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "JSCExecutor::loadApplicationScript", + "sourceURL", sourceURL); + #endif + evaluateScript(m_context, jsScript, jsSourceURL); + flush(); + ReactMarker::logMarker("CREATE_REACT_CONTEXT_END"); +} + +void JSCExecutor::setJSModulesUnbundle(std::unique_ptr unbundle) { + if (!m_unbundle) { + installGlobalFunction(m_context, "nativeRequire", nativeRequire); + } + m_unbundle = std::move(unbundle); +} + +void JSCExecutor::flush() { + // TODO: Make this a first class function instead of evaling. #9317773 + std::string calls = executeJSCallWithJSC(m_context, "flushedQueue", std::vector()); + m_bridge->callNativeModules(*this, calls, true); +} + +void JSCExecutor::callFunction(const std::string& moduleId, const std::string& methodId, const folly::dynamic& arguments) { + // TODO: Make this a first class function instead of evaling. #9317773 + std::vector call{ + moduleId, + methodId, + std::move(arguments), + }; + std::string calls = executeJSCallWithJSC(m_context, "callFunctionReturnFlushedQueue", std::move(call)); + m_bridge->callNativeModules(*this, calls, true); +} + +void JSCExecutor::invokeCallback(const double callbackId, const folly::dynamic& arguments) { + // TODO: Make this a first class function instead of evaling. #9317773 + std::vector call{ + (double) callbackId, + std::move(arguments) + }; + std::string calls = executeJSCallWithJSC(m_context, "invokeCallbackAndReturnFlushedQueue", std::move(call)); + m_bridge->callNativeModules(*this, calls, true); +} + +void JSCExecutor::setGlobalVariable(const std::string& propName, const std::string& jsonValue) { + auto globalObject = JSContextGetGlobalObject(m_context); + String jsPropertyName(propName.c_str()); + + String jsValueJSON(jsonValue.c_str()); + auto valueToInject = JSValueMakeFromJSONString(m_context, jsValueJSON); + + JSObjectSetProperty(m_context, globalObject, jsPropertyName, valueToInject, 0, NULL); +} + +void* JSCExecutor::getJavaScriptContext() { + return m_context; +} + +bool JSCExecutor::supportsProfiling() { + #ifdef WITH_FBSYSTRACE + return true; + #else + return false; + #endif +} + +void JSCExecutor::startProfiler(const std::string &titleString) { + #ifdef WITH_JSC_EXTRA_TRACING + JSStringRef title = JSStringCreateWithUTF8CString(titleString.c_str()); + #if WITH_REACT_INTERNAL_SETTINGS + JSStartProfiling(m_context, title, false); + #else + JSStartProfiling(m_context, title); + #endif + JSStringRelease(title); + #endif +} + +void JSCExecutor::stopProfiler(const std::string &titleString, const std::string& filename) { + #ifdef WITH_JSC_EXTRA_TRACING + JSStringRef title = JSStringCreateWithUTF8CString(titleString.c_str()); + facebook::react::stopAndOutputProfilingFile(m_context, title, filename.c_str()); + JSStringRelease(title); + #endif +} + +void JSCExecutor::handleMemoryPressureModerate() { + #ifdef WITH_JSC_MEMORY_PRESSURE + JSHandleMemoryPressure(this, m_context, JSMemoryPressure::MODERATE); + #endif +} + +void JSCExecutor::handleMemoryPressureCritical() { + #ifdef WITH_JSC_MEMORY_PRESSURE + JSHandleMemoryPressure(this, m_context, JSMemoryPressure::CRITICAL); + #endif +} + +void JSCExecutor::flushQueueImmediate(std::string queueJSON) { + m_bridge->callNativeModules(*this, queueJSON, false); +} + +void JSCExecutor::loadModule(uint32_t moduleId) { + auto module = m_unbundle->getModule(moduleId); + auto sourceUrl = String::createExpectingAscii(module.name); + auto source = String::createExpectingAscii(module.code); + evaluateScript(m_context, source, sourceUrl); +} + +int JSCExecutor::addWebWorker( + const std::string& script, + JSValueRef workerRef, + JSValueRef globalObjRef) { + static std::atomic_int nextWorkerId(1); + int workerId = nextWorkerId++; + + Object globalObj = Value(m_context, globalObjRef).asObject(); + + std::shared_ptr workerMQT = + WebWorkerUtil::createWebWorkerThread(workerId, m_messageQueueThread.get()); + std::unique_ptr worker; + workerMQT->runOnQueueSync([this, &worker, &workerMQT, &script, &globalObj, workerId] () { + worker.reset(new JSCExecutor(m_bridge, workerMQT, workerId, this, script, + globalObj.toJSONMap(), m_jscConfig)); + }); + + Object workerObj = Value(m_context, workerRef).asObject(); + workerObj.makeProtected(); + + JSCExecutor *workerPtr = worker.get(); + std::shared_ptr sharedMessageQueueThread = worker->m_messageQueueThread; + ExecutorToken token = m_bridge->registerExecutor( + std::move(worker), + std::move(sharedMessageQueueThread)); + + m_ownedWorkers.emplace( + std::piecewise_construct, + std::forward_as_tuple(workerId), + std::forward_as_tuple(workerPtr, token, std::move(workerObj))); + + return workerId; +} + +void JSCExecutor::postMessageToOwnedWebWorker(int workerId, JSValueRef message, JSValueRef *exn) { + auto worker = m_ownedWorkers.at(workerId).executor; + std::string msgString = Value(m_context, message).toJSONString(); + + std::shared_ptr isWorkerDestroyed = worker->m_isDestroyed; + worker->m_messageQueueThread->runOnQueue([isWorkerDestroyed, worker, msgString] () { + if (*isWorkerDestroyed) { + return; + } + worker->receiveMessageFromOwner(msgString); + }); +} + +void JSCExecutor::postMessageToOwner(JSValueRef msg) { + std::string msgString = Value(m_context, msg).toJSONString(); + std::shared_ptr ownerIsDestroyed = m_owner->m_isDestroyed; + m_owner->m_messageQueueThread->runOnQueue([workerId=m_workerId, owner=m_owner, ownerIsDestroyed, msgString] () { + if (*ownerIsDestroyed) { + return; + } + owner->receiveMessageFromOwnedWebWorker(workerId, msgString); + }); +} + +void JSCExecutor::receiveMessageFromOwnedWebWorker(int workerId, const std::string& json) { + Object* workerObj; + try { + workerObj = &m_ownedWorkers.at(workerId).jsObj; + } catch (std::out_of_range& e) { + // Worker was already terminated + return; + } + + Value onmessageValue = workerObj->getProperty("onmessage"); + if (onmessageValue.isUndefined()) { + return; + } + + JSValueRef args[] = { createMessageObject(json) }; + onmessageValue.asObject().callAsFunction(1, args); + + flush(); +} + +void JSCExecutor::receiveMessageFromOwner(const std::string& msgString) { + CHECK(m_owner) << "Received message in a Executor that doesn't have an owner!"; + + JSValueRef args[] = { createMessageObject(msgString) }; + Value onmessageValue = Object::getGlobalObject(m_context).getProperty("onmessage"); + onmessageValue.asObject().callAsFunction(1, args); +} + +void JSCExecutor::terminateOwnedWebWorker(int workerId) { + auto& workerRegistration = m_ownedWorkers.at(workerId); + std::shared_ptr workerMQT = workerRegistration.executor->m_messageQueueThread; + ExecutorToken workerExecutorToken = workerRegistration.executorToken; + m_ownedWorkers.erase(workerId); + + workerMQT->runOnQueueSync([this, workerExecutorToken, &workerMQT] { + workerMQT->quitSynchronous(); + std::unique_ptr worker = m_bridge->unregisterExecutor(workerExecutorToken); + worker->destroy(); + worker.reset(); + }); +} + +Object JSCExecutor::createMessageObject(const std::string& msgJson) { + Value rebornJSMsg = Value::fromJSON(m_context, String(msgJson.c_str())); + Object messageObject = Object::create(m_context); + messageObject.setProperty("data", rebornJSMsg); + return messageObject; +} + +// Native JS hooks +JSValueRef JSCExecutor::nativePostMessage( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception) { + if (argumentCount != 1) { + *exception = makeJSCException(ctx, "postMessage got wrong number of arguments"); + return JSValueMakeUndefined(ctx); + } + JSValueRef msg = arguments[0]; + JSCExecutor *webWorker = s_globalContextRefToJSCExecutor.at(JSContextGetGlobalContext(ctx)); + + webWorker->postMessageToOwner(msg); + + return JSValueMakeUndefined(ctx); +} + +static JSValueRef makeInvalidModuleIdJSCException( + JSContextRef ctx, + const JSValueRef id, + JSValueRef *exception) { + std::string message = "Received invalid module ID: "; + message += String::adopt(JSValueToStringCopy(ctx, id, exception)).str(); + return makeJSCException(ctx, message.c_str()); +} + +JSValueRef JSCExecutor::nativeRequire( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception) { + + if (argumentCount != 1) { + *exception = makeJSCException(ctx, "Got wrong number of args"); + return JSValueMakeUndefined(ctx); + } + + JSCExecutor *executor; + try { + executor = s_globalContextRefToJSCExecutor.at(JSContextGetGlobalContext(ctx)); + } catch (std::out_of_range& e) { + *exception = makeJSCException(ctx, "Global JS context didn't map to a valid executor"); + return JSValueMakeUndefined(ctx); + } + + double moduleId = JSValueToNumber(ctx, arguments[0], exception); + if (moduleId <= (double) std::numeric_limits::max() && moduleId >= 0.0) { + try { + executor->loadModule(moduleId); + } catch (JSModulesUnbundle::ModuleNotFound&) { + *exception = makeInvalidModuleIdJSCException(ctx, arguments[0], exception); + } + } else { + *exception = makeInvalidModuleIdJSCException(ctx, arguments[0], exception); + } + return JSValueMakeUndefined(ctx); +} + +static JSValueRef createErrorString(JSContextRef ctx, const char *msg) { + return JSValueMakeString(ctx, String(msg)); +} + +JSValueRef JSCExecutor::nativeFlushQueueImmediate( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception) { + if (argumentCount != 1) { + *exception = createErrorString(ctx, "Got wrong number of args"); + return JSValueMakeUndefined(ctx); + } + + JSCExecutor *executor; + try { + executor = s_globalContextRefToJSCExecutor.at(JSContextGetGlobalContext(ctx)); + } catch (std::out_of_range& e) { + *exception = createErrorString(ctx, "Global JS context didn't map to a valid executor"); + return JSValueMakeUndefined(ctx); + } + + std::string resStr = Value(ctx, arguments[0]).toJSONString(); + + executor->flushQueueImmediate(resStr); + + return JSValueMakeUndefined(ctx); +} + +JSValueRef JSCExecutor::nativeStartWorker( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception) { + if (argumentCount != 3) { + *exception = createErrorString(ctx, "Got wrong number of args"); + return JSValueMakeUndefined(ctx); + } + + std::string scriptFile = Value(ctx, arguments[0]).toString().str(); + + JSValueRef worker = arguments[1]; + JSValueRef globalObj = arguments[2]; + + JSCExecutor *executor; + try { + executor = s_globalContextRefToJSCExecutor.at(JSContextGetGlobalContext(ctx)); + } catch (std::out_of_range& e) { + *exception = createErrorString(ctx, "Global JS context didn't map to a valid executor"); + return JSValueMakeUndefined(ctx); + } + + int workerId = executor->addWebWorker(scriptFile, worker, globalObj); + + return JSValueMakeNumber(ctx, workerId); +} + +JSValueRef JSCExecutor::nativePostMessageToWorker( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception) { + if (argumentCount != 2) { + *exception = createErrorString(ctx, "Got wrong number of args"); + return JSValueMakeUndefined(ctx); + } + + double workerDouble = JSValueToNumber(ctx, arguments[0], exception); + if (workerDouble != workerDouble) { + *exception = createErrorString(ctx, "Got invalid worker id"); + return JSValueMakeUndefined(ctx); + } + + JSCExecutor *executor; + try { + executor = s_globalContextRefToJSCExecutor.at(JSContextGetGlobalContext(ctx)); + } catch (std::out_of_range& e) { + *exception = createErrorString(ctx, "Global JS context didn't map to a valid executor"); + return JSValueMakeUndefined(ctx); + } + + executor->postMessageToOwnedWebWorker((int) workerDouble, arguments[1], exception); + + return JSValueMakeUndefined(ctx); +} + +JSValueRef JSCExecutor::nativeTerminateWorker( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception) { + if (argumentCount != 1) { + *exception = createErrorString(ctx, "Got wrong number of args"); + return JSValueMakeUndefined(ctx); + } + + double workerDouble = JSValueToNumber(ctx, arguments[0], exception); + if (workerDouble != workerDouble) { + *exception = createErrorString(ctx, "Got invalid worker id"); + return JSValueMakeUndefined(ctx); + } + + JSCExecutor *executor; + try { + executor = s_globalContextRefToJSCExecutor.at(JSContextGetGlobalContext(ctx)); + } catch (std::out_of_range& e) { + *exception = createErrorString(ctx, "Global JS context didn't map to a valid executor"); + return JSValueMakeUndefined(ctx); + } + + executor->terminateOwnedWebWorker((int) workerDouble); + + return JSValueMakeUndefined(ctx); +} + +JSValueRef JSCExecutor::nativeCallSyncHook( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception) { + if (argumentCount != 3) { + *exception = createErrorString(ctx, "Got wrong number of args for callSyncHook"); + return JSValueMakeUndefined(ctx); + } + + unsigned int moduleId = Value(ctx, arguments[0]).asUnsignedInteger(); + unsigned int methodId = Value(ctx, arguments[1]).asUnsignedInteger(); + std::string argsJson = Value(ctx, arguments[2]).toJSONString(); + + JSCExecutor *executor; + try { + executor = s_globalContextRefToJSCExecutor.at(JSContextGetGlobalContext(ctx)); + } catch (std::out_of_range& e) { + *exception = createErrorString(ctx, "Global JS context didn't map to a valid executor"); + return JSValueMakeUndefined(ctx); + } + + MethodCallResult result = executor->m_bridge->callSerializableNativeHook( + moduleId, + methodId, + argsJson); + if (result.isUndefined) { + return JSValueMakeUndefined(ctx); + } + return Value::fromJSON(ctx, String(folly::toJson(result.result).c_str())); +} + +static JSValueRef nativeInjectHMRUpdate( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], JSValueRef *exception) { + String execJSString = Value(ctx, arguments[0]).toString(); + String jsURL = Value(ctx, arguments[1]).toString(); + evaluateScript(ctx, execJSString, jsURL); + return JSValueMakeUndefined(ctx); +} + +} } diff --git a/ReactCommon/bridge/JSCExecutor.h b/ReactCommon/bridge/JSCExecutor.h new file mode 100644 index 00000000000..27baff8a05e --- /dev/null +++ b/ReactCommon/bridge/JSCExecutor.h @@ -0,0 +1,171 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include + +#include + +#include + +#include "Executor.h" +#include "ExecutorToken.h" +#include "JSCHelpers.h" +#include "Value.h" + +namespace facebook { +namespace react { + +class MessageQueueThread; + +class JSCExecutorFactory : public JSExecutorFactory { +public: + JSCExecutorFactory(const std::string& cacheDir, const folly::dynamic& jscConfig) : + cacheDir_(cacheDir), + m_jscConfig(jscConfig) {} + virtual std::unique_ptr createJSExecutor( + Bridge *bridge, std::shared_ptr jsQueue) override; +private: + std::string cacheDir_; + folly::dynamic m_jscConfig; +}; + +class JSCExecutor; +class WorkerRegistration : public noncopyable { +public: + explicit WorkerRegistration(JSCExecutor* executor_, ExecutorToken executorToken_, Object jsObj_) : + executor(executor_), + executorToken(executorToken_), + jsObj(std::move(jsObj_)) {} + + JSCExecutor *executor; + ExecutorToken executorToken; + Object jsObj; +}; + +class JSCExecutor : public JSExecutor { +public: + /** + * Must be invoked from thread this Executor will run on. + */ + explicit JSCExecutor(Bridge *bridge, std::shared_ptr messageQueueThread, + const std::string& cacheDir, const folly::dynamic& jscConfig); + ~JSCExecutor() override; + + virtual void loadApplicationScript( + const std::string& script, + const std::string& sourceURL) override; + virtual void setJSModulesUnbundle( + std::unique_ptr unbundle) override; + virtual void callFunction( + const std::string& moduleId, + const std::string& methodId, + const folly::dynamic& arguments) override; + virtual void invokeCallback( + const double callbackId, + const folly::dynamic& arguments) override; + virtual void setGlobalVariable( + const std::string& propName, + const std::string& jsonValue) override; + virtual void* getJavaScriptContext() override; + virtual bool supportsProfiling() override; + virtual void startProfiler(const std::string &titleString) override; + virtual void stopProfiler(const std::string &titleString, const std::string &filename) override; + virtual void handleMemoryPressureModerate() override; + virtual void handleMemoryPressureCritical() override; + virtual void destroy() override; + + void installNativeHook(const char *name, JSObjectCallAsFunctionCallback callback); + +private: + JSGlobalContextRef m_context; + Bridge *m_bridge; + int m_workerId = 0; // if this is a worker executor, this is non-zero + JSCExecutor *m_owner = nullptr; // if this is a worker executor, this is non-null + std::shared_ptr m_isDestroyed = std::shared_ptr(new bool(false)); + std::unordered_map m_ownedWorkers; + std::string m_deviceCacheDir; + std::shared_ptr m_messageQueueThread; + std::unique_ptr m_unbundle; + folly::dynamic m_jscConfig; + + /** + * WebWorker constructor. Must be invoked from thread this Executor will run on. + */ + JSCExecutor( + Bridge *bridge, + std::shared_ptr messageQueueThread, + int workerId, + JSCExecutor *owner, + const std::string& script, + const std::unordered_map& globalObjAsJSON, + const folly::dynamic& jscConfig); + + void initOnJSVMThread(); + void terminateOnJSVMThread(); + void flush(); + void flushQueueImmediate(std::string queueJSON); + void loadModule(uint32_t moduleId); + + int addWebWorker(const std::string& script, JSValueRef workerRef, JSValueRef globalObjRef); + void postMessageToOwnedWebWorker(int worker, JSValueRef message, JSValueRef *exn); + void postMessageToOwner(JSValueRef result); + void receiveMessageFromOwnedWebWorker(int workerId, const std::string& message); + void receiveMessageFromOwner(const std::string &msgString); + void terminateOwnedWebWorker(int worker); + Object createMessageObject(const std::string& msgData); + + static JSValueRef nativeStartWorker( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception); + static JSValueRef nativePostMessageToWorker( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception); + static JSValueRef nativeTerminateWorker( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception); + static JSValueRef nativePostMessage( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception); + static JSValueRef nativeRequire( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception); + static JSValueRef nativeFlushQueueImmediate( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception); + static JSValueRef nativeCallSyncHook( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception); +}; + +} } diff --git a/ReactCommon/bridge/JSCHelpers.cpp b/ReactCommon/bridge/JSCHelpers.cpp new file mode 100644 index 00000000000..f768bedaf9e --- /dev/null +++ b/ReactCommon/bridge/JSCHelpers.cpp @@ -0,0 +1,55 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "JSCHelpers.h" + +#include +#include + +#include "Value.h" + +namespace facebook { +namespace react { + +void installGlobalFunction( + JSGlobalContextRef ctx, + const char* name, + JSObjectCallAsFunctionCallback callback) { + JSStringRef jsName = JSStringCreateWithUTF8CString(name); + JSObjectRef functionObj = JSObjectMakeFunctionWithCallback( + ctx, jsName, callback); + JSObjectRef globalObject = JSContextGetGlobalObject(ctx); + JSObjectSetProperty(ctx, globalObject, jsName, functionObj, 0, NULL); + JSStringRelease(jsName); +} + +JSValueRef makeJSCException( + JSContextRef ctx, + const char* exception_text) { + JSStringRef message = JSStringCreateWithUTF8CString(exception_text); + JSValueRef exceptionString = JSValueMakeString(ctx, message); + JSStringRelease(message); + return JSValueToObject(ctx, exceptionString, NULL); +} + +JSValueRef evaluateScript(JSContextRef context, JSStringRef script, JSStringRef source) { + JSValueRef exn, result; + result = JSEvaluateScript(context, script, NULL, source, 0, &exn); + if (result == nullptr) { + Value exception = Value(context, exn); + std::string exceptionText = exception.toString().str(); + LOG(ERROR) << "Got JS Exception: " << exceptionText; + auto line = exception.asObject().getProperty("line"); + + std::ostringstream locationInfo; + std::string file = source != nullptr ? String::ref(source).str() : ""; + locationInfo << "(" << (file.length() ? file : ""); + if (line != nullptr && line.isNumber()) { + locationInfo << ":" << line.asInteger(); + } + locationInfo << ")"; + throwJSExecutionException("%s %s", exceptionText.c_str(), locationInfo.str().c_str()); + } + return result; +} + +} } diff --git a/ReactCommon/bridge/JSCHelpers.h b/ReactCommon/bridge/JSCHelpers.h new file mode 100644 index 00000000000..976e4fdc8ba --- /dev/null +++ b/ReactCommon/bridge/JSCHelpers.h @@ -0,0 +1,46 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include + +#include +#include + +namespace facebook { +namespace react { + +struct JsException : std::runtime_error { + using std::runtime_error::runtime_error; +}; + +inline void throwJSExecutionException(const char* msg) { + throw JsException(msg); +} + +template +inline void throwJSExecutionException(const char* fmt, Args... args) { + int msgSize = snprintf(nullptr, 0, fmt, args...); + msgSize = std::min(512, msgSize + 1); + char *msg = (char*) alloca(msgSize); + snprintf(msg, msgSize, fmt, args...); + throw JsException(msg); +} + +void installGlobalFunction( + JSGlobalContextRef ctx, + const char* name, + JSObjectCallAsFunctionCallback callback); + +JSValueRef makeJSCException( + JSContextRef ctx, + const char* exception_text); + +JSValueRef evaluateScript( + JSContextRef ctx, + JSStringRef script, + JSStringRef sourceURL); + +} } diff --git a/ReactCommon/bridge/JSCLegacyProfiler.cpp b/ReactCommon/bridge/JSCLegacyProfiler.cpp new file mode 100644 index 00000000000..dcd1b6d134c --- /dev/null +++ b/ReactCommon/bridge/JSCLegacyProfiler.cpp @@ -0,0 +1,88 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#ifdef WITH_JSC_EXTRA_TRACING + +#include +#include +#include +#include +#include +#include "JSCHelpers.h" +#include "JSCLegacyProfiler.h" +#include "Value.h" + +static JSValueRef nativeProfilerStart( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + if (argumentCount < 1) { + if (exception) { + *exception = facebook::react::makeJSCException( + ctx, + "nativeProfilerStart: requires at least 1 argument"); + } + return JSValueMakeUndefined(ctx); + } + + JSStringRef title = JSValueToStringCopy(ctx, arguments[0], exception); + #if WITH_REACT_INTERNAL_SETTINGS + JSStartProfiling(ctx, title, false); + #else + JSStartProfiling(ctx, title); + #endif + JSStringRelease(title); + return JSValueMakeUndefined(ctx); +} + +static JSValueRef nativeProfilerEnd( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + if (argumentCount < 1) { + if (exception) { + *exception = facebook::react::makeJSCException( + ctx, + "nativeProfilerEnd: requires at least 1 argument"); + } + return JSValueMakeUndefined(ctx); + } + + std::string writeLocation("/sdcard/"); + if (argumentCount > 1) { + JSStringRef fileName = JSValueToStringCopy(ctx, arguments[1], exception); + writeLocation += facebook::react::String::ref(fileName).str(); + JSStringRelease(fileName); + } else { + writeLocation += "profile.json"; + } + JSStringRef title = JSValueToStringCopy(ctx, arguments[0], exception); + JSEndProfilingAndRender(ctx, title, writeLocation.c_str()); + JSStringRelease(title); + return JSValueMakeUndefined(ctx); +} + +namespace facebook { +namespace react { + +void stopAndOutputProfilingFile( + JSContextRef ctx, + JSStringRef title, + const char *filename) { + JSEndProfilingAndRender(ctx, title, filename); +} + +void addNativeProfilingHooks(JSGlobalContextRef ctx) { + JSEnableByteCodeProfiling(); + installGlobalFunction(ctx, "nativeProfilerStart", nativeProfilerStart); + installGlobalFunction(ctx, "nativeProfilerEnd", nativeProfilerEnd); +} + +} } + +#endif diff --git a/ReactCommon/bridge/JSCLegacyProfiler.h b/ReactCommon/bridge/JSCLegacyProfiler.h new file mode 100644 index 00000000000..b5029e6ca59 --- /dev/null +++ b/ReactCommon/bridge/JSCLegacyProfiler.h @@ -0,0 +1,19 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#ifdef WITH_JSC_EXTRA_TRACING + +#include +namespace facebook { +namespace react { + +void addNativeProfilingHooks(JSGlobalContextRef ctx); +void stopAndOutputProfilingFile( + JSContextRef ctx, + JSStringRef title, + const char *filename); + +} } + +#endif diff --git a/ReactCommon/bridge/JSCMemory.cpp b/ReactCommon/bridge/JSCMemory.cpp new file mode 100644 index 00000000000..4ca7d6ea53b --- /dev/null +++ b/ReactCommon/bridge/JSCMemory.cpp @@ -0,0 +1,48 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include +#include +#include +#include +#include "JSCHelpers.h" + +#include "Value.h" + +#ifdef WITH_FB_MEMORY_PROFILING + +static JSValueRef nativeCaptureHeap( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + if (argumentCount < 1) { + if (exception) { + *exception = facebook::react::makeJSCException( + ctx, + "nativeCaptureHeap requires the path to save the capture"); + } + return JSValueMakeUndefined(ctx); + } + + JSStringRef outputFilename = JSValueToStringCopy(ctx, arguments[0], exception); + std::string finalFilename = facebook::react::String::ref(outputFilename).str(); + JSCaptureHeap(ctx, finalFilename.c_str(), exception); + JSStringRelease(outputFilename); + return JSValueMakeUndefined(ctx); +} + +#endif // WITH_FB_MEMORY_PROFILING + +namespace facebook { +namespace react { + +void addNativeMemoryHooks(JSGlobalContextRef ctx) { +#ifdef WITH_FB_MEMORY_PROFILING + installGlobalFunction(ctx, "nativeCaptureHeap", nativeCaptureHeap); +#endif // WITH_FB_MEMORY_PROFILING + +} + +} } diff --git a/ReactCommon/bridge/JSCMemory.h b/ReactCommon/bridge/JSCMemory.h new file mode 100644 index 00000000000..6e05bba709e --- /dev/null +++ b/ReactCommon/bridge/JSCMemory.h @@ -0,0 +1,11 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +namespace facebook { +namespace react { + +void addNativeMemoryHooks(JSGlobalContextRef ctx); + +} } diff --git a/ReactCommon/bridge/JSCPerfStats.cpp b/ReactCommon/bridge/JSCPerfStats.cpp new file mode 100644 index 00000000000..9ef341c9e11 --- /dev/null +++ b/ReactCommon/bridge/JSCPerfStats.cpp @@ -0,0 +1,65 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "JSCPerfStats.h" + +#ifdef JSC_HAS_PERF_STATS_API + +#include +#include + +#include "JSCHelpers.h" +#include "Value.h" + +static JSValueRef nativeGetHeapStats( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + JSHeapStats heapStats = {0}; + JSGetHeapStats(ctx, &heapStats); + + auto result = facebook::react::Object::create(ctx); + result.setProperty("size", {ctx, JSValueMakeNumber(ctx, heapStats.size)}); + result.setProperty("extra_size", {ctx, JSValueMakeNumber(ctx, heapStats.extraSize)}); + result.setProperty("capacity", {ctx, JSValueMakeNumber(ctx, heapStats.capacity)}); + result.setProperty("object_count", {ctx, JSValueMakeNumber(ctx, heapStats.objectCount)}); + + return (JSObjectRef) result; +} + +static JSValueRef nativeGetGCStats( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + JSGCStats gcStats = {0}; + JSGetGCStats(ctx, &gcStats); + + auto result = facebook::react::Object::create(ctx); + result.setProperty( + "last_full_gc_length", + {ctx, JSValueMakeNumber(ctx, gcStats.lastFullGCLength)}); + result.setProperty( + "last_eden_gc_length", + {ctx, JSValueMakeNumber(ctx, gcStats.lastEdenGCLength)}); + + return (JSObjectRef) result; +} + +#endif + +namespace facebook { +namespace react { + +void addJSCPerfStatsHooks(JSGlobalContextRef ctx) { +#ifdef JSC_HAS_PERF_STATS_API + installGlobalFunction(ctx, "nativeGetHeapStats", nativeGetHeapStats); + installGlobalFunction(ctx, "nativeGetGCStats", nativeGetGCStats); +#endif +} + +} } diff --git a/ReactCommon/bridge/JSCPerfStats.h b/ReactCommon/bridge/JSCPerfStats.h new file mode 100644 index 00000000000..73ca29e8b0e --- /dev/null +++ b/ReactCommon/bridge/JSCPerfStats.h @@ -0,0 +1,12 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +namespace facebook { +namespace react { + +void addJSCPerfStatsHooks(JSGlobalContextRef ctx); + +} } diff --git a/ReactCommon/bridge/JSCTracing.cpp b/ReactCommon/bridge/JSCTracing.cpp new file mode 100644 index 00000000000..6bdf03ff94e --- /dev/null +++ b/ReactCommon/bridge/JSCTracing.cpp @@ -0,0 +1,482 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#ifdef WITH_JSC_EXTRA_TRACING + +#include +#include +#include +#include +#include +#include +#include "JSCHelpers.h" + +using std::min; + +static const char *ENABLED_FBSYSTRACE_PROFILE_NAME = "__fbsystrace__"; + +static uint64_t tagFromJSValue( + JSContextRef ctx, + JSValueRef value, + JSValueRef* exception) { + // XXX validate that this is a lossless conversion. + // XXX should we just have separate functions for bridge, infra, and apps, + // then drop this argument to save time? + (void)exception; + uint64_t tag = (uint64_t) JSValueToNumber(ctx, value, NULL); + return tag; +} + +static int64_t int64FromJSValue( + JSContextRef ctx, + JSValueRef value, + JSValueRef* exception) { + (void)exception; + int64_t num = (int64_t) JSValueToNumber(ctx, value, NULL); + return num; +} + +static size_t copyTruncatedAsciiChars( + char* buf, + size_t bufLen, + JSContextRef ctx, + JSValueRef value, + size_t maxLen) { + JSStringRef jsString = JSValueToStringCopy(ctx, value, NULL); + size_t stringLen = JSStringGetLength(jsString); + // Unlike the Java version, we truncate from the end of the string, + // rather than the beginning. + size_t toWrite = min(stringLen, min(bufLen, maxLen)); + + const char *startBuf = buf; + const JSChar* chars = JSStringGetCharactersPtr(jsString); + while (toWrite-- > 0) { + *(buf++) = (char)*(chars++); + } + + JSStringRelease(jsString); + + // Return the number of bytes written + return buf - startBuf; +} + +static size_t copyArgsToBuffer( + char* buf, + size_t bufLen, + size_t pos, + JSContextRef ctx, + size_t argumentCount, + const JSValueRef arguments[]) { + char separator = '|'; + for ( + size_t idx = 0; + idx + 1 < argumentCount; // Make sure key and value are present. + idx += 2) { + JSValueRef key = arguments[idx]; + JSValueRef value = arguments[idx+1]; + + buf[pos++] = separator; + separator = ';'; + if (FBSYSTRACE_UNLIKELY(pos >= bufLen)) { break; } + pos += copyTruncatedAsciiChars( + buf + pos, bufLen - pos, ctx, key, FBSYSTRACE_MAX_MESSAGE_LENGTH); + if (FBSYSTRACE_UNLIKELY(pos >= bufLen)) { break; } + buf[pos++] = '='; + if (FBSYSTRACE_UNLIKELY(pos >= bufLen)) { break; } + pos += copyTruncatedAsciiChars( + buf + pos, bufLen - pos, ctx, value, FBSYSTRACE_MAX_MESSAGE_LENGTH); + if (FBSYSTRACE_UNLIKELY(pos >= bufLen)) { break; } + } + return pos; +} + +static JSValueRef nativeTraceBeginSection( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + if (FBSYSTRACE_UNLIKELY(argumentCount < 2)) { + if (exception) { + *exception = facebook::react::makeJSCException( + ctx, + "nativeTraceBeginSection: requires at least 2 arguments"); + } + return JSValueMakeUndefined(ctx); + } + + uint64_t tag = tagFromJSValue(ctx, arguments[0], exception); + if (!fbsystrace_is_tracing(tag)) { + return JSValueMakeUndefined(ctx); + } + + char buf[FBSYSTRACE_MAX_MESSAGE_LENGTH]; + size_t pos = 0; + + pos += snprintf(buf + pos, sizeof(buf) - pos, "B|%d|", getpid()); + // Skip the overflow check here because the int will be small. + pos += copyTruncatedAsciiChars(buf + pos, sizeof(buf) - pos, ctx, arguments[1], FBSYSTRACE_MAX_SECTION_NAME_LENGTH); + // Skip the overflow check here because the section name will be small-ish. + + pos = copyArgsToBuffer(buf, sizeof(buf), pos, ctx, argumentCount - 2, arguments + 2); + if (FBSYSTRACE_UNLIKELY(pos >= sizeof(buf))) { + goto flush; + } + +flush: + fbsystrace_trace_raw(buf, min(pos, sizeof(buf)-1)); + + return JSValueMakeUndefined(ctx); +} + +static JSValueRef nativeTraceEndSection( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + if (FBSYSTRACE_UNLIKELY(argumentCount < 1)) { + if (exception) { + *exception = facebook::react::makeJSCException( + ctx, + "nativeTraceEndSection: requires at least 1 argument"); + } + return JSValueMakeUndefined(ctx); + } + + uint64_t tag = tagFromJSValue(ctx, arguments[0], exception); + if (!fbsystrace_is_tracing(tag)) { + return JSValueMakeUndefined(ctx); + } + + if (FBSYSTRACE_LIKELY(argumentCount == 1)) { + fbsystrace_end_section(tag); + } else { + char buf[FBSYSTRACE_MAX_MESSAGE_LENGTH]; + size_t pos = 0; + + buf[pos++] = 'E'; + buf[pos++] = '|'; + buf[pos++] = '|'; + pos = copyArgsToBuffer(buf, sizeof(buf), pos, ctx, argumentCount - 1, arguments + 1); + if (FBSYSTRACE_UNLIKELY(pos >= sizeof(buf))) { + goto flush; + } + +flush: + fbsystrace_trace_raw(buf, min(pos, sizeof(buf)-1)); + } + + return JSValueMakeUndefined(ctx); +} + +static JSValueRef beginOrEndAsync( + bool isEnd, + bool isFlow, + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + if (FBSYSTRACE_UNLIKELY(argumentCount < 3)) { + if (exception) { + *exception = facebook::react::makeJSCException( + ctx, + "beginOrEndAsync: requires at least 3 arguments"); + } + return JSValueMakeUndefined(ctx); + } + + uint64_t tag = tagFromJSValue(ctx, arguments[0], exception); + if (!fbsystrace_is_tracing(tag)) { + return JSValueMakeUndefined(ctx); + } + + char buf[FBSYSTRACE_MAX_MESSAGE_LENGTH]; + size_t pos = 0; + + // This uses an if-then-else instruction in ARMv7, which should be cheaper + // than a full branch. + buf[pos++] = ((isFlow) ? (isEnd ? 'f' : 's') : (isEnd ? 'F' : 'S')); + pos += snprintf(buf + pos, sizeof(buf) - pos, "|%d|", getpid()); + // Skip the overflow check here because the int will be small. + pos += copyTruncatedAsciiChars(buf + pos, sizeof(buf) - pos, ctx, arguments[1], FBSYSTRACE_MAX_SECTION_NAME_LENGTH); + // Skip the overflow check here because the section name will be small-ish. + + // I tried some trickery to avoid a branch here, but gcc did not cooperate. + // We could consider changing the implementation to be lest branchy in the + // future. + // This is not required for flow use an or to avoid introducing another branch + if (!(isEnd | isFlow)) { + buf[pos++] = '<'; + buf[pos++] = '0'; + buf[pos++] = '>'; + } + buf[pos++] = '|'; + + // Append the cookie. It should be an integer, but copyTruncatedAsciiChars + // will automatically convert it to a string. We might be able to get more + // performance by just getting the number and doing to string + // conversion ourselves. We truncate to FBSYSTRACE_MAX_SECTION_NAME_LENGTH + // just to make sure we can avoid the overflow check even if the caller + // passes in something bad. + pos += copyTruncatedAsciiChars(buf + pos, sizeof(buf) - pos, ctx, arguments[2], FBSYSTRACE_MAX_SECTION_NAME_LENGTH); + + pos = copyArgsToBuffer(buf, sizeof(buf), pos, ctx, argumentCount - 3, arguments + 3); + if (FBSYSTRACE_UNLIKELY(pos >= sizeof(buf))) { + goto flush; + } + +flush: + fbsystrace_trace_raw(buf, min(pos, sizeof(buf)-1)); + + return JSValueMakeUndefined(ctx); +} + +static JSValueRef stageAsync( + bool isFlow, + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + if (FBSYSTRACE_UNLIKELY(argumentCount < 4)) { + if (exception) { + *exception = facebook::react::makeJSCException( + ctx, + "stageAsync: requires at least 4 arguments"); + } + return JSValueMakeUndefined(ctx); + } + + uint64_t tag = tagFromJSValue(ctx, arguments[0], exception); + if (!fbsystrace_is_tracing(tag)) { + return JSValueMakeUndefined(ctx); + } + + char buf[FBSYSTRACE_MAX_MESSAGE_LENGTH]; + size_t pos = 0; + + buf[pos++] = (isFlow ? 't' : 'T'); + pos += snprintf(buf + pos, sizeof(buf) - pos, "|%d", getpid()); + // Skip the overflow check here because the int will be small. + + // Arguments are section name, cookie, and stage name. + // All added together, they still cannot cause an overflow. + for (int i = 1; i < 4; i++) { + buf[pos++] = '|'; + pos += copyTruncatedAsciiChars(buf + pos, sizeof(buf) - pos, ctx, arguments[i], FBSYSTRACE_MAX_SECTION_NAME_LENGTH); + } + + fbsystrace_trace_raw(buf, min(pos, sizeof(buf)-1)); + + return JSValueMakeUndefined(ctx); +} + +static JSValueRef nativeTraceBeginAsyncSection( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + return beginOrEndAsync( + false /* isEnd */, + false /* isFlow */, + ctx, + function, + thisObject, + argumentCount, + arguments, + exception); +} + +static JSValueRef nativeTraceEndAsyncSection( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + return beginOrEndAsync( + true /* isEnd */, + false /* isFlow */, + ctx, + function, + thisObject, + argumentCount, + arguments, + exception); +} + +static JSValueRef nativeTraceAsyncSectionStage( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + return stageAsync( + false /* isFlow */, + ctx, + function, + thisObject, + argumentCount, + arguments, + exception); +} + +static JSValueRef nativeTraceBeginAsyncFlow( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + return beginOrEndAsync( + false /* isEnd */, + true /* isFlow */, + ctx, + function, + thisObject, + argumentCount, + arguments, + exception); +} + +static JSValueRef nativeTraceEndAsyncFlow( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + return beginOrEndAsync( + true /* isEnd */, + true /* isFlow */, + ctx, + function, + thisObject, + argumentCount, + arguments, + exception); +} + +static JSValueRef nativeTraceAsyncFlowStage( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + return stageAsync( + true /* isFlow */, + ctx, + function, + thisObject, + argumentCount, + arguments, + exception); +} + +static JSValueRef nativeTraceCounter( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + if (FBSYSTRACE_UNLIKELY(argumentCount < 3)) { + if (exception) { + *exception = facebook::react::makeJSCException( + ctx, + "nativeTraceCounter: requires at least 3 arguments"); + } + return JSValueMakeUndefined(ctx); + } + + uint64_t tag = tagFromJSValue(ctx, arguments[0], exception); + if (!fbsystrace_is_tracing(tag)) { + return JSValueMakeUndefined(ctx); + } + + char buf[FBSYSTRACE_MAX_MESSAGE_LENGTH]; + size_t len = copyTruncatedAsciiChars(buf, sizeof(buf), ctx, + arguments[1], FBSYSTRACE_MAX_SECTION_NAME_LENGTH); + buf[min(len,(FBSYSTRACE_MAX_MESSAGE_LENGTH-1))] = 0; + int64_t value = int64FromJSValue(ctx, arguments[2], exception); + + fbsystrace_counter(tag, buf, value); + + return JSValueMakeUndefined(ctx); +} + +static JSValueRef nativeTraceBeginLegacy( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + if (FBSYSTRACE_LIKELY(argumentCount >= 1)) { + uint64_t tag = tagFromJSValue(ctx, arguments[0], exception); + if (!fbsystrace_is_tracing(tag)) { + return JSValueMakeUndefined(ctx); + } + } + + JSStringRef title = JSStringCreateWithUTF8CString(ENABLED_FBSYSTRACE_PROFILE_NAME); + #if WITH_REACT_INTERNAL_SETTINGS + JSStartProfiling(ctx, title, true); + #else + JSStartProfiling(ctx, title); + #endif + JSStringRelease(title); + + return JSValueMakeUndefined(ctx); +} + +static JSValueRef nativeTraceEndLegacy( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef* exception) { + if (FBSYSTRACE_LIKELY(argumentCount >= 1)) { + uint64_t tag = tagFromJSValue(ctx, arguments[0], exception); + if (!fbsystrace_is_tracing(tag)) { + return JSValueMakeUndefined(ctx); + } + } + + JSStringRef title = JSStringCreateWithUTF8CString(ENABLED_FBSYSTRACE_PROFILE_NAME); + JSEndProfiling(ctx, title); + JSStringRelease(title); + + return JSValueMakeUndefined(ctx); +} + +namespace facebook { +namespace react { + +void addNativeTracingHooks(JSGlobalContextRef ctx) { + installGlobalFunction(ctx, "nativeTraceBeginSection", nativeTraceBeginSection); + installGlobalFunction(ctx, "nativeTraceEndSection", nativeTraceEndSection); + installGlobalFunction(ctx, "nativeTraceBeginLegacy", nativeTraceBeginLegacy); + installGlobalFunction(ctx, "nativeTraceEndLegacy", nativeTraceEndLegacy); + installGlobalFunction(ctx, "nativeTraceBeginAsyncSection", nativeTraceBeginAsyncSection); + installGlobalFunction(ctx, "nativeTraceEndAsyncSection", nativeTraceEndAsyncSection); + installGlobalFunction(ctx, "nativeTraceAsyncSectionStage", nativeTraceAsyncSectionStage); + installGlobalFunction(ctx, "nativeTraceBeginAsyncFlow", nativeTraceBeginAsyncFlow); + installGlobalFunction(ctx, "nativeTraceEndAsyncFlow", nativeTraceEndAsyncFlow); + installGlobalFunction(ctx, "nativeTraceAsyncFlowStage", nativeTraceAsyncFlowStage); + installGlobalFunction(ctx, "nativeTraceCounter", nativeTraceCounter); +} + +} } + +#endif diff --git a/ReactCommon/bridge/JSCTracing.h b/ReactCommon/bridge/JSCTracing.h new file mode 100644 index 00000000000..0cca4092d45 --- /dev/null +++ b/ReactCommon/bridge/JSCTracing.h @@ -0,0 +1,15 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#ifdef WITH_JSC_EXTRA_TRACING + +#include +namespace facebook { +namespace react { + +void addNativeTracingHooks(JSGlobalContextRef ctx); + +} } + +#endif diff --git a/ReactCommon/bridge/JSCWebWorker.cpp b/ReactCommon/bridge/JSCWebWorker.cpp new file mode 100644 index 00000000000..d3870536902 --- /dev/null +++ b/ReactCommon/bridge/JSCWebWorker.cpp @@ -0,0 +1,137 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "JSCWebWorker.h" + +#include +#include +#include +#include + +#include +#include + +#include "JSCHelpers.h" +#include "MessageQueueThread.h" +#include "Platform.h" +#include "Value.h" + +#include + +namespace facebook { +namespace react { + +// TODO(9604425): thread safety +static std::unordered_map s_globalContextRefToJSCWebWorker; + +JSCWebWorker::JSCWebWorker(int id, JSCWebWorkerOwner *owner, std::string scriptSrc) : + id_(id), + scriptName_(std::move(scriptSrc)), + owner_(owner) { + ownerMessageQueueThread_ = owner->getMessageQueueThread(); + CHECK(ownerMessageQueueThread_) << "Owner MessageQueue must not be null"; + workerMessageQueueThread_ = WebWorkerUtil::createWebWorkerThread(id, ownerMessageQueueThread_.get()); + CHECK(workerMessageQueueThread_) << "Failed to create worker thread"; + + workerMessageQueueThread_->runOnQueue([this] () { + initJSVMAndLoadScript(); + }); +} + + +JSCWebWorker::~JSCWebWorker() { + CHECK(isTerminated()) << "Didn't terminate the web worker before releasing it!";; +} + +void JSCWebWorker::postMessage(JSValueRef msg) { + std::string msgString = Value(owner_->getContext(), msg).toJSONString(); + + workerMessageQueueThread_->runOnQueue([this, msgString] () { + if (isTerminated()) { + return; + } + + JSValueRef args[] = { createMessageObject(context_, msgString) }; + Value onmessageValue = Object::getGlobalObject(context_).getProperty("onmessage"); + onmessageValue.asObject().callAsFunction(1, args); + }); +} + +void JSCWebWorker::terminate() { + if (isTerminated()) { + return; + } + isTerminated_.store(true, std::memory_order_release); + + workerMessageQueueThread_->runOnQueueSync([this] { + terminateOnWorkerThread(); + }); +} + +void JSCWebWorker::terminateOnWorkerThread() { + s_globalContextRefToJSCWebWorker.erase(context_); + JSGlobalContextRelease(context_); + context_ = nullptr; + workerMessageQueueThread_->quitSynchronous(); +} + +bool JSCWebWorker::isTerminated() { + return isTerminated_.load(std::memory_order_acquire); +} + +void JSCWebWorker::initJSVMAndLoadScript() { + CHECK(!isTerminated()) << "Worker was already finished!"; + CHECK(!context_) << "Worker JS VM was already created!"; + + context_ = JSGlobalContextCreateInGroup( + NULL, // use default JS 'global' object + NULL // create new group (i.e. new VM) + ); + s_globalContextRefToJSCWebWorker[context_] = this; + + // TODO(9604438): Protect against script does not exist + std::string script = WebWorkerUtil::loadScriptFromAssets(scriptName_); + evaluateScript(context_, String(script.c_str()), String(scriptName_.c_str())); + + installGlobalFunction(context_, "postMessage", nativePostMessage); +} + +void JSCWebWorker::postMessageToOwner(JSValueRef msg) { + std::string msgString = Value(context_, msg).toJSONString(); + ownerMessageQueueThread_->runOnQueue([this, msgString] () { + owner_->onMessageReceived(id_, msgString); + }); +} + +JSValueRef JSCWebWorker::nativePostMessage( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception) { + if (argumentCount != 1) { + *exception = makeJSCException(ctx, "postMessage got wrong number of arguments"); + return JSValueMakeUndefined(ctx); + } + JSValueRef msg = arguments[0]; + JSCWebWorker *webWorker = s_globalContextRefToJSCWebWorker.at(JSContextGetGlobalContext(ctx)); + + if (webWorker->isTerminated()) { + return JSValueMakeUndefined(ctx); + } + + webWorker->postMessageToOwner(msg); + + return JSValueMakeUndefined(ctx); +} + +/*static*/ +Object JSCWebWorker::createMessageObject(JSContextRef context, const std::string& msgJson) { + Value rebornJSMsg = Value::fromJSON(context, String(msgJson.c_str())); + Object messageObject = Object::create(context); + messageObject.setProperty("data", rebornJSMsg); + return messageObject; +} + +} +} diff --git a/ReactCommon/bridge/JSCWebWorker.h b/ReactCommon/bridge/JSCWebWorker.h new file mode 100644 index 00000000000..73d983cdfe7 --- /dev/null +++ b/ReactCommon/bridge/JSCWebWorker.h @@ -0,0 +1,94 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include +#include +#include +#include +#include +#include + +#include + +#include "Value.h" + +namespace facebook { +namespace react { + +class MessageQueueThread; + +/** + * A class that can own the lifecycle, receive messages from, and dispatch messages + * to JSCWebWorkers. + */ +class JSCWebWorkerOwner { +public: + /** + * Called when a worker has posted a message with `postMessage`. + */ + virtual void onMessageReceived(int workerId, const std::string& message) = 0; + virtual JSGlobalContextRef getContext() = 0; + + /** + * Should return the owner's MessageQueueThread. Calls to onMessageReceived will be enqueued + * on this thread. + */ + virtual std::shared_ptr getMessageQueueThread() = 0; +}; + +/** + * Implementation of a web worker for JSC. The web worker should be created from the owner's + * (e.g., owning JSCExecutor instance) JS MessageQueueThread. The worker is responsible for + * creating its own MessageQueueThread. + * + * During operation, the JSCExecutor should call postMessage **from its own MessageQueueThread** + * to send messages to the worker. The worker will handle enqueueing those messages on its own + * MessageQueueThread as appropriate. When the worker has a message to post to the owner, it will + * enqueue a call to owner->onMessageReceived on the owner's MessageQueueThread. + */ +class JSCWebWorker { +public: + explicit JSCWebWorker(int id, JSCWebWorkerOwner *owner, std::string script); + ~JSCWebWorker(); + + /** + * Post a message to be received by the worker on its thread. This must be called from + * ownerMessageQueueThread_. + */ + void postMessage(JSValueRef msg); + + /** + * Synchronously quits the current worker and cleans up its VM. + */ + void terminate(); + + /** + * Whether terminate() has been called on this worker. + */ + bool isTerminated(); + + static Object createMessageObject(JSContextRef context, const std::string& msgData); +private: + void initJSVMAndLoadScript(); + void postRunnableToEventLoop(std::function&& runnable); + void postMessageToOwner(JSValueRef result); + void terminateOnWorkerThread(); + + int id_; + std::atomic_bool isTerminated_ = ATOMIC_VAR_INIT(false); + std::string scriptName_; + JSCWebWorkerOwner *owner_ = nullptr; + std::shared_ptr ownerMessageQueueThread_; + std::unique_ptr workerMessageQueueThread_; + JSGlobalContextRef context_ = nullptr; + + static JSValueRef nativePostMessage( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception); +}; + +} +} diff --git a/ReactCommon/bridge/JSModulesUnbundle.h b/ReactCommon/bridge/JSModulesUnbundle.h new file mode 100644 index 00000000000..d1c2f12547f --- /dev/null +++ b/ReactCommon/bridge/JSModulesUnbundle.h @@ -0,0 +1,35 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include + +#include "noncopyable.h" + +namespace facebook { +namespace react { + +class JSModulesUnbundle : noncopyable { + /** + * Represents the set of JavaScript modules that the application consists of. + * The source code of each module can be retrieved by module ID. + * + * The class is non-copyable because copying instances might involve copying + * several megabytes of memory. + */ +public: + class ModuleNotFound : public std::out_of_range { + using std::out_of_range::out_of_range; + }; + struct Module { + std::string name; + std::string code; + }; + virtual ~JSModulesUnbundle() {} + virtual Module getModule(uint32_t moduleId) const = 0; +}; + +} +} diff --git a/ReactCommon/bridge/MessageQueueThread.h b/ReactCommon/bridge/MessageQueueThread.h new file mode 100644 index 00000000000..72d13749f9d --- /dev/null +++ b/ReactCommon/bridge/MessageQueueThread.h @@ -0,0 +1,23 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include + +namespace facebook { +namespace react { + +class MessageQueueThread { + public: + virtual ~MessageQueueThread() {} + virtual void runOnQueue(std::function&&) = 0; + // runOnQueueSync and quitSynchronous are dangerous. They should only be + // used for initialization and cleanup. + virtual void runOnQueueSync(std::function&&) = 0; + // Once quitSynchronous() returns, no further work should run on the queue. + virtual void quitSynchronous() = 0; +}; + +}} diff --git a/ReactCommon/bridge/MethodCall.cpp b/ReactCommon/bridge/MethodCall.cpp new file mode 100644 index 00000000000..2782e05b6f9 --- /dev/null +++ b/ReactCommon/bridge/MethodCall.cpp @@ -0,0 +1,74 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "MethodCall.h" + +#include +#include + +namespace facebook { +namespace react { + +#define REQUEST_MODULE_IDS 0 +#define REQUEST_METHOD_IDS 1 +#define REQUEST_PARAMSS 2 +#define REQUEST_CALLID 3 + +std::vector parseMethodCalls(const std::string& json) { + folly::dynamic jsonData = folly::parseJson(json); + + if (jsonData.isNull()) { + return {}; + } + + if (!jsonData.isArray()) { + throw std::invalid_argument( + folly::to("Did not get valid calls back from JS: ", jsonData.typeName())); + } + + if (jsonData.size() < REQUEST_PARAMSS + 1) { + throw std::invalid_argument( + folly::to("Did not get valid calls back from JS: size == ", jsonData.size())); + } + + auto moduleIds = jsonData[REQUEST_MODULE_IDS]; + auto methodIds = jsonData[REQUEST_METHOD_IDS]; + auto params = jsonData[REQUEST_PARAMSS]; + int callId = -1; + + if (!moduleIds.isArray() || !methodIds.isArray() || !params.isArray()) { + throw std::invalid_argument( + folly::to("Did not get valid calls back from JS: ", json.c_str())); + } + + if (jsonData.size() > REQUEST_CALLID) { + if (!jsonData[REQUEST_CALLID].isInt()) { + throw std::invalid_argument( + folly::to("Did not get valid calls back from JS: %s", json.c_str())); + } else { + callId = jsonData[REQUEST_CALLID].getInt(); + } + } + + std::vector methodCalls; + for (size_t i = 0; i < moduleIds.size(); i++) { + auto paramsValue = params[i]; + if (!paramsValue.isArray()) { + throw std::invalid_argument( + folly::to("Call argument isn't an array")); + } + + methodCalls.emplace_back( + moduleIds[i].getInt(), + methodIds[i].getInt(), + std::move(params[i]), + callId); + + // only incremement callid if contains valid callid as callid is optional + callId += (callId != -1) ? 1 : 0; + } + + return methodCalls; +} + +}} + diff --git a/ReactCommon/bridge/MethodCall.h b/ReactCommon/bridge/MethodCall.h new file mode 100644 index 00000000000..02e86eb9876 --- /dev/null +++ b/ReactCommon/bridge/MethodCall.h @@ -0,0 +1,29 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include + +#include + +namespace facebook { +namespace react { + +struct MethodCall { + int moduleId; + int methodId; + folly::dynamic arguments; + int callId; + + MethodCall(int mod, int meth, folly::dynamic args, int cid) + : moduleId(mod) + , methodId(meth) + , arguments(std::move(args)) + , callId(cid) {} +}; + +std::vector parseMethodCalls(const std::string& json); + +} } diff --git a/ReactCommon/bridge/ModuleRegistry.cpp b/ReactCommon/bridge/ModuleRegistry.cpp new file mode 100644 index 00000000000..64360fef8c3 --- /dev/null +++ b/ReactCommon/bridge/ModuleRegistry.cpp @@ -0,0 +1,114 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "ModuleRegistry.h" + +#include "NativeModule.h" + +#ifdef WITH_FBSYSTRACE +#include +using fbsystrace::FbSystraceSection; +#endif + +namespace facebook { +namespace react { + +ModuleRegistry::ModuleRegistry(std::vector> modules) + : modules_(std::move(modules)) {} + +folly::dynamic ModuleRegistry::moduleDescriptions() { + folly::dynamic modDescs = folly::dynamic::object; + + for (size_t moduleId = 0; moduleId < modules_.size(); ++moduleId) { + const auto& module = modules_[moduleId]; + + folly::dynamic methodDescs = folly::dynamic::object; + std::vector methods; + { +#ifdef WITH_FBSYSTRACE + FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "getMethods", "module", module->getName()); +#endif + methods = module->getMethods(); + } + for (size_t methodId = 0; methodId < methods.size(); ++methodId) { + methodDescs.insert(std::move(methods[methodId].name), + folly::dynamic::object + ("methodID", methodId) + ("type", std::move(methods[methodId].type))); + } + + folly::dynamic constants = folly::dynamic::array(); + { +#ifdef WITH_FBSYSTRACE + FbSystraceSection s(TRACE_TAG_REACT_CXX_BRIDGE, "getConstants", "module", module->getName()); +#endif + constants = module->getConstants(); + } + + modDescs.insert(module->getName(), + folly::dynamic::object + ("supportsWebWorkers", module->supportsWebWorkers()) + ("moduleID", moduleId) + ("methods", std::move(methodDescs)) + ("constants", std::move(constants))); + + } + return modDescs; +} + +void ModuleRegistry::callNativeMethod(ExecutorToken token, unsigned int moduleId, unsigned int methodId, + folly::dynamic&& params, int callId) { + if (moduleId >= modules_.size()) { + throw std::runtime_error( + folly::to("moduleId ", moduleId, + " out of range [0..", modules_.size(), ")")); + } + +#ifdef WITH_FBSYSTRACE + if (callId != -1) { + fbsystrace_end_async_flow(TRACE_TAG_REACT_APPS, "native", callId); + } +#endif + + // TODO mhorowitz: systrace + std::string what; + try { + modules_[moduleId]->invoke(token, methodId, std::move(params)); + return; + } catch (const std::exception& e) { + what = e.what(); + // fall through; + } catch (...) { + // fall through; + } + + std::string moduleName = modules_[moduleId]->getName(); + auto descs = modules_[moduleId]->getMethods(); + std::string methodName; + if (methodId < descs.size()) { + methodName = descs[methodId].name; + } else { + methodName = folly::to("id ", methodId, " (out of range [0..", + descs.size(), "))"); + } + + if (what.empty()) { + throw std::runtime_error( + folly::to("Unknown native exception in module '", moduleName, + "' method '", methodName, "'")); + } else { + throw std::runtime_error( + folly::to("Native exception in module '", moduleName, + "' method '", methodName, "': ", what)); + } +} + +MethodCallResult ModuleRegistry::callSerializableNativeHook(ExecutorToken token, unsigned int moduleId, unsigned int methodId, folly::dynamic&& params) { + if (moduleId >= modules_.size()) { + throw std::runtime_error( + folly::to("moduleId ", moduleId, + " out of range [0..", modules_.size(), ")")); + } + return modules_[moduleId]->callSerializableNativeHook(token, methodId, std::move(params)); +} + +}} diff --git a/ReactCommon/bridge/ModuleRegistry.h b/ReactCommon/bridge/ModuleRegistry.h new file mode 100644 index 00000000000..2bc85b61b40 --- /dev/null +++ b/ReactCommon/bridge/ModuleRegistry.h @@ -0,0 +1,38 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include + +#include + +#include "ExecutorToken.h" +#include "NativeModule.h" + +namespace facebook { +namespace react { + +class NativeModule; + +class ModuleRegistry { + public: + // not implemented: + // onBatchComplete: see https://our.intern.facebook.com/intern/tasks/?t=5279396 + // getModule: only used by views + // getAllModules: only used for cleanup; use RAII instead + // notifyCatalystInstanceInitialized: this is really only used by view-related code + // notifyCatalystInstanceDestroy: use RAII instead + + ModuleRegistry(std::vector> modules); + folly::dynamic moduleDescriptions(); + void callNativeMethod(ExecutorToken token, unsigned int moduleId, unsigned int methodId, + folly::dynamic&& params, int callId); + MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int moduleId, unsigned int methodId, folly::dynamic&& args); + + private: + std::vector> modules_; +}; + +} +} diff --git a/ReactCommon/bridge/NativeModule.h b/ReactCommon/bridge/NativeModule.h new file mode 100644 index 00000000000..6a71ef0d6ec --- /dev/null +++ b/ReactCommon/bridge/NativeModule.h @@ -0,0 +1,44 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include + +#include + +#include "ExecutorToken.h" + +namespace facebook { +namespace react { + +struct MethodCallResult { + folly::dynamic result; + bool isUndefined; +}; + +struct MethodDescriptor { + std::string name; + // type is one of js MessageQueue.MethodTypes + std::string type; + + MethodDescriptor(std::string n, std::string t) + : name(std::move(n)) + , type(std::move(t)) {} +}; + +class NativeModule { + public: + virtual ~NativeModule() {} + virtual std::string getName() = 0; + virtual std::vector getMethods() = 0; + virtual folly::dynamic getConstants() = 0; + virtual bool supportsWebWorkers() = 0; + // TODO mhorowitz: do we need initialize()/onCatalystInstanceDestroy() in C++ + // or only Java? + virtual void invoke(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& params) = 0; + virtual MethodCallResult callSerializableNativeHook(ExecutorToken token, unsigned int reactMethodId, folly::dynamic&& args) = 0; +}; + +} +} diff --git a/ReactCommon/bridge/Platform.cpp b/ReactCommon/bridge/Platform.cpp new file mode 100644 index 00000000000..bd45d1561b1 --- /dev/null +++ b/ReactCommon/bridge/Platform.cpp @@ -0,0 +1,27 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "Platform.h" + +namespace facebook { +namespace react { + +namespace ReactMarker { +LogMarker logMarker; +}; + +namespace WebWorkerUtil { +WebWorkerQueueFactory createWebWorkerThread; +LoadScriptFromAssets loadScriptFromAssets; +LoadScriptFromNetworkSync loadScriptFromNetworkSync; +}; + +namespace PerfLogging { +InstallNativeHooks installNativeHooks; +}; + +namespace JSNativeHooks { +Hook loggingHook = nullptr; +Hook nowHook = nullptr; +} + +} } diff --git a/ReactCommon/bridge/Platform.h b/ReactCommon/bridge/Platform.h new file mode 100644 index 00000000000..e8095aa7115 --- /dev/null +++ b/ReactCommon/bridge/Platform.h @@ -0,0 +1,49 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include + +#include + +#include "MessageQueueThread.h" + +namespace facebook { +namespace react { + +namespace ReactMarker { +using LogMarker = std::function; +extern LogMarker logMarker; +}; + +namespace WebWorkerUtil { +using WebWorkerQueueFactory = std::function(int id, MessageQueueThread* ownerMessageQueue)>; +extern WebWorkerQueueFactory createWebWorkerThread; + +using LoadScriptFromAssets = std::function; +extern LoadScriptFromAssets loadScriptFromAssets; + +using LoadScriptFromNetworkSync = std::function; +extern LoadScriptFromNetworkSync loadScriptFromNetworkSync; +}; + +namespace PerfLogging { +using InstallNativeHooks = std::function; +extern InstallNativeHooks installNativeHooks; +}; + +namespace JSNativeHooks { + using Hook = JSValueRef (*) ( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception); + extern Hook loggingHook; + extern Hook nowHook; +} + +} } diff --git a/ReactCommon/bridge/Value.cpp b/ReactCommon/bridge/Value.cpp new file mode 100644 index 00000000000..84e6e7b8915 --- /dev/null +++ b/ReactCommon/bridge/Value.cpp @@ -0,0 +1,142 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include "Value.h" + +#include "JSCHelpers.h" + +namespace facebook { +namespace react { + +Value::Value(JSContextRef context, JSValueRef value) : + m_context(context), + m_value(value) +{ +} + +Value::Value(Value&& other) : + m_context(other.m_context), + m_value(other.m_value) +{ + other.m_value = nullptr; +} + +JSContextRef Value::context() const { + return m_context; +} + +std::string Value::toJSONString(unsigned indent) const { + JSValueRef exn; + auto stringToAdopt = JSValueCreateJSONString(m_context, m_value, indent, &exn); + if (stringToAdopt == nullptr) { + std::string exceptionText = Value(m_context, exn).toString().str(); + throwJSExecutionException("Exception creating JSON string: %s", exceptionText.c_str()); + } + return String::adopt(stringToAdopt).str(); +} + +/* static */ +Value Value::fromJSON(JSContextRef ctx, const String& json) { + auto result = JSValueMakeFromJSONString(ctx, json); + if (!result) { + throwJSExecutionException("Failed to create String from JSON"); + } + return Value(ctx, result); +} + +Object Value::asObject() { + JSValueRef exn; + JSObjectRef jsObj = JSValueToObject(context(), m_value, &exn); + if (!jsObj) { + std::string exceptionText = Value(m_context, exn).toString().str(); + throwJSExecutionException("Failed to convert to object: %s", exceptionText.c_str()); + } + Object ret = Object(context(), jsObj); + m_value = nullptr; + return ret; +} + +Object::operator Value() const { + return Value(m_context, m_obj); +} + +Value Object::callAsFunction(int nArgs, JSValueRef args[]) { + JSValueRef exn; + JSValueRef result = JSObjectCallAsFunction(m_context, m_obj, NULL, nArgs, args, &exn); + if (!result) { + std::string exceptionText = Value(m_context, exn).toString().str(); + throwJSExecutionException("Exception calling JS function: %s", exceptionText.c_str()); + } + return Value(m_context, result); +} + +Value Object::getProperty(const String& propName) const { + JSValueRef exn; + JSValueRef property = JSObjectGetProperty(m_context, m_obj, propName, &exn); + if (!property) { + std::string exceptionText = Value(m_context, exn).toString().str(); + throwJSExecutionException("Failed to get property: %s", exceptionText.c_str()); + } + return Value(m_context, property); +} + +Value Object::getPropertyAtIndex(unsigned index) const { + JSValueRef exn; + JSValueRef property = JSObjectGetPropertyAtIndex(m_context, m_obj, index, &exn); + if (!property) { + std::string exceptionText = Value(m_context, exn).toString().str(); + throwJSExecutionException("Failed to get property at index %u: %s", index, exceptionText.c_str()); + } + return Value(m_context, property); +} + +Value Object::getProperty(const char *propName) const { + return getProperty(String(propName)); +} + +void Object::setProperty(const String& propName, const Value& value) const { + JSValueRef exn = NULL; + JSObjectSetProperty(m_context, m_obj, propName, value, kJSPropertyAttributeNone, &exn); + if (exn) { + std::string exceptionText = Value(m_context, exn).toString().str(); + throwJSExecutionException("Failed to set property: %s", exceptionText.c_str()); + } +} + +void Object::setProperty(const char *propName, const Value& value) const { + setProperty(String(propName), value); +} + +std::vector Object::getPropertyNames() const { + std::vector names; + auto namesRef = JSObjectCopyPropertyNames(m_context, m_obj); + size_t count = JSPropertyNameArrayGetCount(namesRef); + for (size_t i = 0; i < count; i++) { + auto string = String::ref(JSPropertyNameArrayGetNameAtIndex(namesRef, i)); + names.emplace_back(string.str()); + } + JSPropertyNameArrayRelease(namesRef); + return names; +} + +std::unordered_map Object::toJSONMap() const { + std::unordered_map map; + auto namesRef = JSObjectCopyPropertyNames(m_context, m_obj); + size_t count = JSPropertyNameArrayGetCount(namesRef); + for (size_t i = 0; i < count; i++) { + auto key = String::ref(JSPropertyNameArrayGetNameAtIndex(namesRef, i)); + map.emplace(key.str(), getProperty(key).toJSONString()); + } + JSPropertyNameArrayRelease(namesRef); + return map; +} + +/* static */ +Object Object::create(JSContextRef ctx) { + JSObjectRef newObj = JSObjectMake( + ctx, + NULL, // create instance of default object class + NULL); // no private data + return Object(ctx, newObj); +} + +} } diff --git a/ReactCommon/bridge/Value.h b/ReactCommon/bridge/Value.h new file mode 100644 index 00000000000..08bf8dc04ed --- /dev/null +++ b/ReactCommon/bridge/Value.h @@ -0,0 +1,238 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "noncopyable.h" + +#if WITH_FBJSCEXTENSIONS +#include +#endif + +namespace facebook { +namespace react { + +class Value; +class Context; + +class String : public noncopyable { +public: + explicit String(const char* utf8) : + m_string(JSStringCreateWithUTF8CString(utf8)) + {} + + String(String&& other) : + m_string(other.m_string) + {} + + String(const String& other) : + m_string(other.m_string) + { + if (m_string) { + JSStringRetain(m_string); + } + } + + ~String() { + if (m_string) { + JSStringRelease(m_string); + } + } + + operator JSStringRef() const { + return m_string; + } + + // Length in characters + size_t length() const { + return JSStringGetLength(m_string); + } + + // Length in bytes of a null-terminated utf8 encoded value + size_t utf8Size() const { + return JSStringGetMaximumUTF8CStringSize(m_string); + } + + std::string str() const { + size_t reserved = utf8Size(); + char* bytes = new char[reserved]; + size_t length = JSStringGetUTF8CString(m_string, bytes, reserved) - 1; + std::unique_ptr retainedBytes(bytes); + return std::string(bytes, length); + } + + // Assumes that utf8 is null terminated + bool equals(const char* utf8) { + return JSStringIsEqualToUTF8CString(m_string, utf8); + } + + static String createExpectingAscii(std::string const &utf8) { + #if WITH_FBJSCEXTENSIONS + return String( + JSStringCreateWithUTF8CStringExpectAscii(utf8.c_str(), utf8.size()), true); + #else + return String(JSStringCreateWithUTF8CString(utf8.c_str()), true); + #endif + } + + static String ref(JSStringRef string) { + return String(string, false); + } + + static String adopt(JSStringRef string) { + return String(string, true); + } + +private: + explicit String(JSStringRef string, bool adopt) : + m_string(string) + { + if (!adopt && string) { + JSStringRetain(string); + } + } + + JSStringRef m_string; +}; + +class Object : public noncopyable { +public: + Object(JSContextRef context, JSObjectRef obj) : + m_context(context), + m_obj(obj) + {} + + Object(Object&& other) : + m_context(other.m_context), + m_obj(other.m_obj), + m_isProtected(other.m_isProtected) { + other.m_obj = nullptr; + other.m_isProtected = false; + } + + ~Object() { + if (m_isProtected && m_obj) { + JSValueUnprotect(m_context, m_obj); + } + } + + operator JSObjectRef() const { + return m_obj; + } + + operator Value() const; + + bool isFunction() const { + return JSObjectIsFunction(m_context, m_obj); + } + + Value callAsFunction(int nArgs, JSValueRef args[]); + + Value getProperty(const String& propName) const; + Value getProperty(const char *propName) const; + Value getPropertyAtIndex(unsigned index) const; + void setProperty(const String& propName, const Value& value) const; + void setProperty(const char *propName, const Value& value) const; + std::vector getPropertyNames() const; + std::unordered_map toJSONMap() const; + + void makeProtected() { + if (!m_isProtected && m_obj) { + JSValueProtect(m_context, m_obj); + m_isProtected = true; + } + } + + static Object getGlobalObject(JSContextRef ctx) { + auto globalObj = JSContextGetGlobalObject(ctx); + return Object(ctx, globalObj); + } + + /** + * Creates an instance of the default object class. + */ + static Object create(JSContextRef ctx); + +private: + JSContextRef m_context; + JSObjectRef m_obj; + bool m_isProtected = false; +}; + +class Value : public noncopyable { +public: + Value(JSContextRef context, JSValueRef value); + Value(Value&&); + + operator JSValueRef() const { + return m_value; + } + + bool isBoolean() const { + return JSValueIsBoolean(context(), m_value); + } + + bool asBoolean() const { + return JSValueToBoolean(context(), m_value); + } + + bool isNumber() const { + return JSValueIsNumber(context(), m_value); + } + + bool isNull() const { + return JSValueIsNull(context(), m_value); + } + + bool isUndefined() const { + return JSValueIsUndefined(context(), m_value); + } + + double asNumber() const { + if (isNumber()) { + return JSValueToNumber(context(), m_value, nullptr); + } else { + return 0.0f; + } + } + + int32_t asInteger() const { + return static_cast(asNumber()); + } + + uint32_t asUnsignedInteger() const { + return static_cast(asNumber()); + } + + bool isObject() const { + return JSValueIsObject(context(), m_value); + } + + Object asObject(); + + bool isString() const { + return JSValueIsString(context(), m_value); + } + + String toString() { + return String::adopt(JSValueToStringCopy(context(), m_value, nullptr)); + } + + std::string toJSONString(unsigned indent = 0) const; + static Value fromJSON(JSContextRef ctx, const String& json); +protected: + JSContextRef context() const; + JSContextRef m_context; + JSValueRef m_value; +}; + +} } diff --git a/ReactCommon/bridge/noncopyable.h b/ReactCommon/bridge/noncopyable.h new file mode 100644 index 00000000000..96b32d44b83 --- /dev/null +++ b/ReactCommon/bridge/noncopyable.h @@ -0,0 +1,12 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once +namespace facebook { +namespace react { +struct noncopyable { + noncopyable(const noncopyable&) = delete; + noncopyable& operator=(const noncopyable&) = delete; + protected: + noncopyable() = default; +}; +}}