From e2e59c4d0ea78431c46558d3003dd2ee27f4126b Mon Sep 17 00:00:00 2001 From: Lulu Wu Date: Tue, 18 Apr 2023 11:01:12 -0700 Subject: [PATCH] Move C++ code to OSS folders (#36789) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36789 Move > xplat/ReactNative/venice to > xplat/js/react-native-github/packages/react-native/ReactCommon/react/bridgeless Changelog: [General][Changed] - Move Bridgeless C++ code to OSS folders Reviewed By: fkgozali Differential Revision: D44626153 fbshipit-source-id: ec8340db92b805d07d3c5f8e86cb35335637ccd6 --- .../ReactCommon/react/bridgeless/.clang-tidy | 17 + .../bridgeless/BufferedRuntimeExecutor.cpp | 57 ++ .../bridgeless/BufferedRuntimeExecutor.h | 46 + .../react/bridgeless/JSEngineInstance.h | 22 + .../react/bridgeless/PlatformTimerRegistry.h | 29 + .../react/bridgeless/ReactInstance.cpp | 453 +++++++++ .../react/bridgeless/ReactInstance.h | 75 ++ .../react/bridgeless/TimerManager.cpp | 411 +++++++++ .../react/bridgeless/TimerManager.h | 112 +++ .../tests/hermes/ReactInstanceTest.cpp | 865 ++++++++++++++++++ 10 files changed, 2087 insertions(+) create mode 100644 packages/react-native/ReactCommon/react/bridgeless/.clang-tidy create mode 100644 packages/react-native/ReactCommon/react/bridgeless/BufferedRuntimeExecutor.cpp create mode 100644 packages/react-native/ReactCommon/react/bridgeless/BufferedRuntimeExecutor.h create mode 100644 packages/react-native/ReactCommon/react/bridgeless/JSEngineInstance.h create mode 100644 packages/react-native/ReactCommon/react/bridgeless/PlatformTimerRegistry.h create mode 100644 packages/react-native/ReactCommon/react/bridgeless/ReactInstance.cpp create mode 100644 packages/react-native/ReactCommon/react/bridgeless/ReactInstance.h create mode 100644 packages/react-native/ReactCommon/react/bridgeless/TimerManager.cpp create mode 100644 packages/react-native/ReactCommon/react/bridgeless/TimerManager.h create mode 100644 packages/react-native/ReactCommon/react/bridgeless/tests/hermes/ReactInstanceTest.cpp diff --git a/packages/react-native/ReactCommon/react/bridgeless/.clang-tidy b/packages/react-native/ReactCommon/react/bridgeless/.clang-tidy new file mode 100644 index 00000000000..60221afcec1 --- /dev/null +++ b/packages/react-native/ReactCommon/react/bridgeless/.clang-tidy @@ -0,0 +1,17 @@ +--- +InheritParentConfig: true +Checks: ' +-cert-err60-cpp, +-cppcoreguidelines-pro-bounds-pointer-arithmetic, +-cppcoreguidelines-special-member-functions, +-fuchsia-default-arguments-calls, +-google-readability-casting, +-google-runtime-references, +-hicpp-special-member-functions, +-llvm-header-guard, +-misc-non-private-member-variables-in-classes, +-misc-unused-parameters, +-modernize-use-trailing-return-type, +-performance-unnecessary-value-param +' +... diff --git a/packages/react-native/ReactCommon/react/bridgeless/BufferedRuntimeExecutor.cpp b/packages/react-native/ReactCommon/react/bridgeless/BufferedRuntimeExecutor.cpp new file mode 100644 index 00000000000..7fb67a3f9a5 --- /dev/null +++ b/packages/react-native/ReactCommon/react/bridgeless/BufferedRuntimeExecutor.cpp @@ -0,0 +1,57 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include "BufferedRuntimeExecutor.h" +#include +#include + +namespace facebook { +namespace react { + +BufferedRuntimeExecutor::BufferedRuntimeExecutor( + RuntimeExecutor runtimeExecutor) + : runtimeExecutor_(runtimeExecutor), + isBufferingEnabled_(true), + lastIndex_(0) {} + +void BufferedRuntimeExecutor::execute(Work &&callback) { + if (!isBufferingEnabled_) { + // Fast path: Schedule directly to RuntimeExecutor, without locking + runtimeExecutor_(std::move(callback)); + return; + } + + /** + * Note: std::mutex doesn't have a FIFO ordering. + * To preserve the order of the buffered work, use a priority queue and + * track the last known work index. + */ + uint64_t newIndex = lastIndex_++; + std::lock_guard guard(lock_); + if (isBufferingEnabled_) { + queue_.push({.index_ = newIndex, .work_ = std::move(callback)}); + return; + } + + // Force flush the queue to maintain the execution order. + unsafeFlush(); + + runtimeExecutor_(std::move(callback)); +} + +void BufferedRuntimeExecutor::flush() { + std::lock_guard guard(lock_); + unsafeFlush(); + isBufferingEnabled_ = false; +} + +void BufferedRuntimeExecutor::unsafeFlush() { + while (queue_.size() > 0) { + BufferedWork const &bufferedWork = queue_.top(); + Work work = std::move(bufferedWork.work_); + runtimeExecutor_(std::move(work)); + queue_.pop(); + } +} + +} // namespace react +} // namespace facebook diff --git a/packages/react-native/ReactCommon/react/bridgeless/BufferedRuntimeExecutor.h b/packages/react-native/ReactCommon/react/bridgeless/BufferedRuntimeExecutor.h new file mode 100644 index 00000000000..942f1ee9af6 --- /dev/null +++ b/packages/react-native/ReactCommon/react/bridgeless/BufferedRuntimeExecutor.h @@ -0,0 +1,46 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include +#include +#include +#include +#include + +namespace facebook { +namespace react { + +class BufferedRuntimeExecutor { + public: + using Work = std::function; + + // A utility structure to track pending work in the order of when they arrive. + struct BufferedWork { + uint64_t index_; + Work work_; + bool operator<(const BufferedWork &rhs) const { + // Higher index has lower priority, so this inverted comparison puts + // the smaller index on top of the queue. + return index_ > rhs.index_; + } + }; + + BufferedRuntimeExecutor(RuntimeExecutor runtimeExecutor); + + void execute(Work &&callback); + + // Flush buffered JS calls and then diable JS buffering + void flush(); + + private: + // Perform flushing without locking mechanism + void unsafeFlush(); + + RuntimeExecutor runtimeExecutor_; + bool isBufferingEnabled_; + std::mutex lock_; + std::atomic lastIndex_; + std::priority_queue queue_; +}; + +} // namespace react +} // namespace facebook diff --git a/packages/react-native/ReactCommon/react/bridgeless/JSEngineInstance.h b/packages/react-native/ReactCommon/react/bridgeless/JSEngineInstance.h new file mode 100644 index 00000000000..5ca44f6adc5 --- /dev/null +++ b/packages/react-native/ReactCommon/react/bridgeless/JSEngineInstance.h @@ -0,0 +1,22 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#pragma once + +#include +#include + +namespace facebook { +namespace react { + +/** + * Interface for a class that creates and owns an instance of a JS VM + */ +class JSEngineInstance { + public: + virtual std::unique_ptr createJSRuntime() noexcept = 0; + + virtual ~JSEngineInstance() = default; +}; + +} // namespace react +} // namespace facebook diff --git a/packages/react-native/ReactCommon/react/bridgeless/PlatformTimerRegistry.h b/packages/react-native/ReactCommon/react/bridgeless/PlatformTimerRegistry.h new file mode 100644 index 00000000000..af45dac414b --- /dev/null +++ b/packages/react-native/ReactCommon/react/bridgeless/PlatformTimerRegistry.h @@ -0,0 +1,29 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#pragma once + +#include + +namespace facebook { +namespace react { + +/** + * This interface is implemented by each platform. + * Responsibility: Call into some platform API to register/schedule, or delete + * registered/scheduled timers. + */ +class PlatformTimerRegistry { + public: + virtual void createTimer(uint32_t timerID, double delayMS) = 0; + + virtual void deleteTimer(uint32_t timerID) = 0; + + virtual void createRecurringTimer(uint32_t timerID, double delayMS) = 0; + + virtual ~PlatformTimerRegistry() noexcept = default; +}; + +using TimerManagerDelegate = PlatformTimerRegistry; + +} // namespace react +} // namespace facebook diff --git a/packages/react-native/ReactCommon/react/bridgeless/ReactInstance.cpp b/packages/react-native/ReactCommon/react/bridgeless/ReactInstance.cpp new file mode 100644 index 00000000000..b0d5e235b4c --- /dev/null +++ b/packages/react-native/ReactCommon/react/bridgeless/ReactInstance.cpp @@ -0,0 +1,453 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include "ReactInstance.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace facebook { +namespace react { + +// Looping on \c drainMicrotasks until it completes or hits the retries bound. +static void performMicrotaskCheckpoint(jsi::Runtime &runtime) { + uint8_t retries = 0; + // A heuristic number to guard inifinite or absurd numbers of retries. + constexpr unsigned int kRetriesBound = 255; + + while (retries < kRetriesBound) { + try { + // The default behavior of \c drainMicrotasks is unbounded execution. + // We may want to make it bounded in the future. + if (runtime.drainMicrotasks()) { + break; + } + } catch (jsi::JSError &error) { + handleJSError(runtime, error, true); + } + retries++; + } + + if (retries == kRetriesBound) { + throw std::runtime_error("Hits microtasks retries bound."); + } +} + +ReactInstance::ReactInstance( + std::unique_ptr runtime, + std::shared_ptr jsMessageQueueThread, + std::shared_ptr timerManager, + JsErrorHandler::JsErrorHandlingFunc jsErrorHandlingFunc) + : runtime_(std::move(runtime)), + jsMessageQueueThread_(jsMessageQueueThread), + timerManager_(std::move(timerManager)), + jsErrorHandler_(jsErrorHandlingFunc), + hasFatalJsError_(std::make_shared(false)) { + auto runtimeExecutor = [weakRuntime = std::weak_ptr(runtime_), + weakTimerManager = + std::weak_ptr(timerManager_), + weakJsMessageQueueThread = + std::weak_ptr( + jsMessageQueueThread_), + weakHasFatalJsError = + std::weak_ptr(hasFatalJsError_)]( + std::function + &&callback) { + if (std::shared_ptr sharedHasFatalJsError = + weakHasFatalJsError.lock()) { + if (*sharedHasFatalJsError) { + LOG(INFO) + << "Calling into JS using runtimeExecutor but hasFatalJsError_ is true"; + return; + } + } + if (weakRuntime.expired()) { + return; + } + + if (std::shared_ptr sharedJsMessageQueueThread = + weakJsMessageQueueThread.lock()) { + sharedJsMessageQueueThread->runOnQueue( + [weakRuntime, weakTimerManager, callback = std::move(callback)]() { + if (auto strongRuntime = weakRuntime.lock()) { + SystraceSection s("ReactInstance::_runtimeExecutor[Callback]"); + try { + callback(*strongRuntime); + if (auto strongTimerManager = weakTimerManager.lock()) { + strongTimerManager->callReactNativeMicrotasks(*strongRuntime); + } + performMicrotaskCheckpoint(*strongRuntime); + } catch (jsi::JSError &originalError) { + handleJSError(*strongRuntime, originalError, true); + } + } + }); + } + }; + + runtimeScheduler_ = + std::make_shared(std::move(runtimeExecutor)); + + auto pipedRuntimeExecutor = + [runtimeScheduler = runtimeScheduler_.get()]( + std::function &&callback) { + runtimeScheduler->scheduleWork(std::move(callback)); + }; + + bufferedRuntimeExecutor_ = + std::make_shared(pipedRuntimeExecutor); +} + +RuntimeExecutor ReactInstance::getUnbufferedRuntimeExecutor() noexcept { + return [runtimeScheduler = runtimeScheduler_.get()]( + std::function &&callback) { + runtimeScheduler->scheduleWork(std::move(callback)); + }; +} + +// This BufferedRuntimeExecutor ensures that the main JS bundle finished +// execution before any JS queued into it from C++ are executed. Use +// getBufferedRuntimeExecutor() instead if you do not need the main JS bundle to +// have finished. e.g. setting global variables into JS runtime. +RuntimeExecutor ReactInstance::getBufferedRuntimeExecutor() noexcept { + return [weakBufferedRuntimeExecutor_ = + std::weak_ptr(bufferedRuntimeExecutor_)]( + std::function &&callback) { + if (auto strongBufferedRuntimeExecutor_ = + weakBufferedRuntimeExecutor_.lock()) { + strongBufferedRuntimeExecutor_->execute(std::move(callback)); + } + }; +} + +std::shared_ptr +ReactInstance::getRuntimeScheduler() noexcept { + return runtimeScheduler_; +} + +/** + * Defines a property on the global object that is neither enumerable, nor + * configurable, nor writable. This ensures that the private globals exposed by + * ReactInstance cannot overwritten by third-party JavaScript code. It also + * ensures that third-party JavaScript code unaware of these globals isn't able + * to accidentally access them. In JavaScript, equivalent to: + * + * Object.defineProperty(global, propName, { + * value: value + * }) + */ +static void defineReadOnlyGlobal( + jsi::Runtime &runtime, + std::string propName, + jsi::Value &&value) { + jsi::Object jsObject = + runtime.global().getProperty(runtime, "Object").asObject(runtime); + jsi::Function defineProperty = jsObject.getProperty(runtime, "defineProperty") + .asObject(runtime) + .asFunction(runtime); + + jsi::Object descriptor = jsi::Object(runtime); + descriptor.setProperty(runtime, "value", std::move(value)); + defineProperty.callWithThis( + runtime, + jsObject, + runtime.global(), + jsi::String::createFromUtf8(runtime, propName), + descriptor); +} + +namespace { + +// Copied from JSIExecutor.cpp +// basename_r isn't in all iOS SDKs, so use this simple version instead. +std::string simpleBasename(const std::string &path) { + size_t pos = path.rfind("/"); + return (pos != std::string::npos) ? path.substr(pos) : path; +} + +} // namespace + +/** + * Load the JS bundle and flush buffered JS calls, future JS calls won't be + * buffered after calling this. + * Note that this method is asynchronous. However, a completion callback + * isn't needed because all calls into JS should be dispatched to the JSThread, + * preferably via the runtimeExecutor_. + */ +void ReactInstance::loadScript( + std::unique_ptr script, + const std::string &sourceURL) { + auto buffer = std::make_shared(std::move(script)); + std::string scriptName = simpleBasename(sourceURL); + + runtimeScheduler_->scheduleWork( + [this, + scriptName, + sourceURL, + buffer = std::move(buffer), + weakBufferedRuntimeExecuter = std::weak_ptr( + bufferedRuntimeExecutor_)](jsi::Runtime &runtime) { + try { + SystraceSection s("ReactInstance::loadScript"); + bool hasLogger(ReactMarker::logTaggedMarkerBridgelessImpl); + if (hasLogger) { + ReactMarker::logTaggedMarkerBridgeless( + ReactMarker::RUN_JS_BUNDLE_START, scriptName.c_str()); + } + + runtime.evaluateJavaScript(buffer, sourceURL); + if (hasLogger) { + ReactMarker::logTaggedMarkerBridgeless( + ReactMarker::RUN_JS_BUNDLE_STOP, scriptName.c_str()); + } + if (auto strongBufferedRuntimeExecuter = + weakBufferedRuntimeExecuter.lock()) { + strongBufferedRuntimeExecuter->flush(); + } + } catch (jsi::JSError &error) { + // Handle uncaught JS errors during loading JS bundle + *hasFatalJsError_ = true; + this->jsErrorHandler_.handleJsError(error, true); + } + }); +} + +/* + * Calls a method on a JS module that has been registered with + * `registerCallableModule`. Used to invoke a JS function from platform code. + */ +void ReactInstance::callFunctionOnModule( + const std::string &moduleName, + const std::string &methodName, + const folly::dynamic &args) { + bufferedRuntimeExecutor_->execute([=](jsi::Runtime &runtime) { + SystraceSection s( + "ReactInstance::callFunctionOnModule", + "moduleName", + moduleName, + "methodName", + methodName); + if (modules_.find(moduleName) == modules_.end()) { + std::ostringstream knownModules; + int i = 0; + for (auto it = modules_.begin(); it != modules_.end(); it++, i++) { + const char *space = (i > 0 ? ", " : " "); + knownModules << space << it->first; + } + throw jsi::JSError( + runtime, + "Failed to call into JavaScript module method " + moduleName + "." + + methodName + + "(). Module has not been registered as callable. Registered callable JavaScript modules (n = " + + std::to_string(modules_.size()) + "):" + knownModules.str() + + ". Did you forget to call `RN$registerCallableModule`?"); + } + + auto module = modules_[moduleName]->factory.call(runtime).asObject(runtime); + auto method = module.getProperty(runtime, methodName.c_str()); + if (method.isUndefined()) { + throw jsi::JSError( + runtime, + "Failed to call into JavaScript module method " + moduleName + "." + + methodName + ". Module exists, but the method is undefined."); + } + + std::vector jsArgs; + for (auto &arg : args) { + jsArgs.push_back(jsi::valueFromDynamic(runtime, arg)); + } + method.asObject(runtime).asFunction(runtime).callWithThis( + runtime, module, (const jsi::Value *)jsArgs.data(), jsArgs.size()); + }); +} + +void ReactInstance::registerSegment( + uint32_t segmentId, + const std::string &segmentPath) { + LOG(WARNING) << "Starting to run ReactInstance::registerSegment with segment " + << segmentId; + runtimeScheduler_->scheduleWork([=](jsi::Runtime &runtime) { + SystraceSection s("ReactInstance::registerSegment"); + const auto tag = folly::to(segmentId); + auto script = JSBigFileString::fromPath(segmentPath); + if (script->size() == 0) { + throw std::invalid_argument( + "Empty segment registered with ID " + tag + " from " + segmentPath); + } + auto buffer = std::make_shared(std::move(script)); + + bool hasLogger(ReactMarker::logTaggedMarkerBridgelessImpl); + if (hasLogger) { + ReactMarker::logTaggedMarkerBridgeless( + ReactMarker::REGISTER_JS_SEGMENT_START, tag.c_str()); + } + LOG(WARNING) << "Starting to evaluate segment " << segmentId + << " in ReactInstance::registerSegment"; + runtime.evaluateJavaScript( + buffer, JSExecutor::getSyntheticBundlePath(segmentId, segmentPath)); + LOG(WARNING) << "Finished evaluating segment " << segmentId + << " in ReactInstance::registerSegment"; + if (hasLogger) { + ReactMarker::logTaggedMarkerBridgeless( + ReactMarker::REGISTER_JS_SEGMENT_STOP, tag.c_str()); + } + }); +} + +namespace { +void defineReactInstanceFlags( + jsi::Runtime &runtime, + ReactInstance::JSRuntimeFlags options) noexcept { + defineReadOnlyGlobal(runtime, "RN$Bridgeless", jsi::Value(true)); + + jsi::addNativeTracingHooks(runtime); + + if (options.isProfiling) { + defineReadOnlyGlobal(runtime, "__RCTProfileIsProfiling", jsi::Value(true)); + } + + if (options.runtimeDiagnosticFlags.length() > 0) { + defineReadOnlyGlobal( + runtime, + "RN$DiagnosticFlags", + jsi::String::createFromUtf8(runtime, options.runtimeDiagnosticFlags)); + } +} + +} // namespace + +void ReactInstance::initializeRuntime( + JSRuntimeFlags options, + BindingsInstallFunc bindingsInstallFunc) noexcept { + runtimeScheduler_->scheduleWork([this, options, bindingsInstallFunc]( + jsi::Runtime &runtime) { + SystraceSection s("ReactInstance::initializeRuntime"); + RuntimeSchedulerBinding::createAndInstallIfNeeded( + runtime, runtimeScheduler_); + + defineReactInstanceFlags(runtime, options); + + defineReadOnlyGlobal( + runtime, + "RN$registerCallableModule", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "registerCallableModule"), + 2, + [this]( + jsi::Runtime &runtime, + const jsi::Value & /*unused*/, + const jsi::Value *args, + size_t count) { + if (count != 2) { + throw jsi::JSError( + runtime, + "registerCallableModule requires exactly 2 arguments"); + } + if (!args[0].isString()) { + throw jsi::JSError( + runtime, + "The first argument to registerCallableModule must be a string (the name of the JS module)."); + } + auto name = args[0].asString(runtime).utf8(runtime); + if (!args[1].isObject() || + !args[1].asObject(runtime).isFunction(runtime)) { + throw jsi::JSError( + runtime, + "The second argument to registerCallableModule must be a function that returns the JS module."); + } + modules_[name] = std::make_shared( + args[1].getObject(runtime).asFunction(runtime)); + return jsi::Value::undefined(); + })); + + timerManager_->attachGlobals(runtime); + + bindingsInstallFunc(runtime); + }); +} + +void ReactInstance::handleMemoryPressureJs(int pressureLevel) { + // The level is an enum value passed by the Android OS to an onTrimMemory + // event callback. Defined in ComponentCallbacks2. + enum AndroidMemoryPressure { + TRIM_MEMORY_BACKGROUND = 40, + TRIM_MEMORY_COMPLETE = 80, + TRIM_MEMORY_MODERATE = 60, + TRIM_MEMORY_RUNNING_CRITICAL = 15, + TRIM_MEMORY_RUNNING_LOW = 10, + TRIM_MEMORY_RUNNING_MODERATE = 5, + TRIM_MEMORY_UI_HIDDEN = 20, + }; + const char *levelName; + switch (pressureLevel) { + case TRIM_MEMORY_BACKGROUND: + levelName = "TRIM_MEMORY_BACKGROUND"; + break; + case TRIM_MEMORY_COMPLETE: + levelName = "TRIM_MEMORY_COMPLETE"; + break; + case TRIM_MEMORY_MODERATE: + levelName = "TRIM_MEMORY_MODERATE"; + break; + case TRIM_MEMORY_RUNNING_CRITICAL: + levelName = "TRIM_MEMORY_RUNNING_CRITICAL"; + break; + case TRIM_MEMORY_RUNNING_LOW: + levelName = "TRIM_MEMORY_RUNNING_LOW"; + break; + case TRIM_MEMORY_RUNNING_MODERATE: + levelName = "TRIM_MEMORY_RUNNING_MODERATE"; + break; + case TRIM_MEMORY_UI_HIDDEN: + levelName = "TRIM_MEMORY_UI_HIDDEN"; + break; + default: + levelName = "UNKNOWN"; + break; + } + + switch (pressureLevel) { + case TRIM_MEMORY_RUNNING_LOW: + case TRIM_MEMORY_RUNNING_MODERATE: + case TRIM_MEMORY_UI_HIDDEN: + // For non-severe memory trims, do nothing. + LOG(INFO) << "Memory warning (pressure level: " << levelName + << ") received by JS VM, ignoring because it's non-severe"; + break; + case TRIM_MEMORY_BACKGROUND: + case TRIM_MEMORY_COMPLETE: + case TRIM_MEMORY_MODERATE: + case TRIM_MEMORY_RUNNING_CRITICAL: + // For now, pressureLevel is unused by collectGarbage. + // This may change in the future if the JS GC has different styles of + // collections. + LOG(INFO) << "Memory warning (pressure level: " << levelName + << ") received by JS VM, running a GC"; + runtimeScheduler_->scheduleWork([=](jsi::Runtime &runtime) { + SystraceSection s("ReactInstance::handleMemoryPressure"); + runtime.instrumentation().collectGarbage(levelName); + }); + break; + default: + // Use the raw number instead of the name here since the name is + // meaningless. + LOG(WARNING) << "Memory warning (pressure level: " << pressureLevel + << ") received by JS VM, unrecognized pressure level"; + break; + } +} + +} // namespace react +} // namespace facebook diff --git a/packages/react-native/ReactCommon/react/bridgeless/ReactInstance.h b/packages/react-native/ReactCommon/react/bridgeless/ReactInstance.h new file mode 100644 index 00000000000..286e6b81203 --- /dev/null +++ b/packages/react-native/ReactCommon/react/bridgeless/ReactInstance.h @@ -0,0 +1,75 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook { +namespace react { + +struct CallableModule { + explicit CallableModule(jsi::Function factory) + : factory(std::move(factory)) {} + jsi::Function factory; +}; + +class ReactInstance final { + public: + using BindingsInstallFunc = std::function; + + ReactInstance( + std::unique_ptr runtime, + std::shared_ptr jsMessageQueueThread, + std::shared_ptr timerManager, + JsErrorHandler::JsErrorHandlingFunc JsErrorHandlingFunc); + + RuntimeExecutor getUnbufferedRuntimeExecutor() noexcept; + + RuntimeExecutor getBufferedRuntimeExecutor() noexcept; + + std::shared_ptr getRuntimeScheduler() noexcept; + + struct JSRuntimeFlags { + bool isProfiling = false; + const std::string runtimeDiagnosticFlags = ""; + }; + + void initializeRuntime( + JSRuntimeFlags options, + BindingsInstallFunc bindingsInstallFunc) noexcept; + + void loadScript( + std::unique_ptr script, + const std::string &sourceURL); + + void registerSegment(uint32_t segmentId, const std::string &segmentPath); + + void callFunctionOnModule( + const std::string &moduleName, + const std::string &methodName, + const folly::dynamic &args); + + void handleMemoryPressureJs(int pressureLevel); + + private: + std::shared_ptr runtime_; + std::shared_ptr jsMessageQueueThread_; + std::shared_ptr bufferedRuntimeExecutor_; + std::shared_ptr timerManager_; + std::unordered_map> modules_; + std::shared_ptr runtimeScheduler_; + JsErrorHandler jsErrorHandler_; + + // Whether there are errors caught during bundle loading + std::shared_ptr hasFatalJsError_; +}; + +} // namespace react +} // namespace facebook diff --git a/packages/react-native/ReactCommon/react/bridgeless/TimerManager.cpp b/packages/react-native/ReactCommon/react/bridgeless/TimerManager.cpp new file mode 100644 index 00000000000..d3d556cd09c --- /dev/null +++ b/packages/react-native/ReactCommon/react/bridgeless/TimerManager.cpp @@ -0,0 +1,411 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include "TimerManager.h" + +#include +#include + +namespace facebook { +namespace react { + +TimerManager::TimerManager( + std::unique_ptr platformTimerRegistry) noexcept + : platformTimerRegistry_(std::move(platformTimerRegistry)) {} + +void TimerManager::setRuntimeExecutor( + RuntimeExecutor runtimeExecutor) noexcept { + runtimeExecutor_ = runtimeExecutor; +} + +std::shared_ptr TimerManager::createReactNativeMicrotask( + jsi::Function &&callback, + std::vector &&args) { + auto sharedCallback = std::make_shared( + std::move(callback), std::move(args), /* repeat */ false); + + // Get the id for the callback. + uint32_t timerID = timerIndex_++; + timers_[timerID] = std::move(sharedCallback); + + reactNativeMicrotasksQueue_.push_back(timerID); + return std::make_shared(timerID); +} + +void TimerManager::callReactNativeMicrotasks(jsi::Runtime &runtime) { + std::vector reactNativeMicrotasksQueue; + while (!reactNativeMicrotasksQueue_.empty()) { + reactNativeMicrotasksQueue.clear(); + reactNativeMicrotasksQueue.swap(reactNativeMicrotasksQueue_); + + for (auto reactNativeMicrotaskID : reactNativeMicrotasksQueue) { + // ReactNativeMicrotasks can clear other scheduled reactNativeMicrotasks. + if (timers_.count(reactNativeMicrotaskID) > 0) { + timers_[reactNativeMicrotaskID]->invoke(runtime); + timers_.erase(reactNativeMicrotaskID); + } + } + } +} + +std::shared_ptr TimerManager::createTimer( + jsi::Function &&callback, + std::vector &&args, + double delay) { + auto sharedCallback = std::make_shared( + std::move(callback), std::move(args), false); + + // Get the id for the callback. + uint32_t timerID = timerIndex_++; + timers_[timerID] = std::move(sharedCallback); + + platformTimerRegistry_->createTimer(timerID, delay); + + return std::make_shared(timerID); +} + +std::shared_ptr TimerManager::createRecurringTimer( + jsi::Function &&callback, + std::vector &&args, + double delay) { + auto sharedCallback = std::make_shared( + std::move(callback), std::move(args), true); + + // Get the id for the callback. + uint32_t timerID = timerIndex_++; + timers_[timerID] = std::move(sharedCallback); + + platformTimerRegistry_->createRecurringTimer(timerID, delay); + + return std::make_shared(timerID); +} + +void TimerManager::deleteReactNativeMicrotask( + jsi::Runtime &runtime, + std::shared_ptr timerHandle) { + if (timerHandle == nullptr) { + throw jsi::JSError( + runtime, "clearReactNativeMicrotask was called with an invalid handle"); + } + + for (auto it = reactNativeMicrotasksQueue_.begin(); + it != reactNativeMicrotasksQueue_.end(); + it++) { + if ((*it) == timerHandle->index()) { + reactNativeMicrotasksQueue_.erase(it); + break; + } + } + + if (timers_.find(timerHandle->index()) != timers_.end()) { + timers_.erase(timerHandle->index()); + } +} + +void TimerManager::deleteTimer( + jsi::Runtime &runtime, + std::shared_ptr timerHandle) { + if (timerHandle == nullptr) { + throw jsi::JSError(runtime, "clearTimeout called with an invalid handle"); + } + + platformTimerRegistry_->deleteTimer(timerHandle->index()); + if (timers_.find(timerHandle->index()) != timers_.end()) { + timers_.erase(timerHandle->index()); + } +} + +void TimerManager::deleteRecurringTimer( + jsi::Runtime &runtime, + std::shared_ptr timerHandle) { + if (timerHandle == nullptr) { + throw jsi::JSError(runtime, "clearInterval called with an invalid handle"); + } + + platformTimerRegistry_->deleteTimer(timerHandle->index()); + if (timers_.find(timerHandle->index()) != timers_.end()) { + timers_.erase(timerHandle->index()); + } +} + +void TimerManager::callTimer(uint32_t timerID) { + runtimeExecutor_([this, timerID](jsi::Runtime &runtime) { + SystraceSection s("TimerManager::callTimer"); + if (timers_.count(timerID) > 0) { + timers_[timerID]->invoke(runtime); + // Invoking a timer has the potential to delete it. Double check the timer + // still exists before accessing it again. + if (timers_.count(timerID) > 0 && !timers_[timerID]->repeat) { + timers_.erase(timerID); + } + } + }); +} + +void TimerManager::attachGlobals(jsi::Runtime &runtime) { + // Install host functions for timers. + // TODO (T45786383): Add missing timer functions from JSTimers + // TODL (T96212789): Skip immediate APIs when JSVM microtask queue is used. + runtime.global().setProperty( + runtime, + "setImmediate", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "setImmediate"), + 2, // Function, ...args + [this]( + jsi::Runtime &rt, + const jsi::Value &thisVal, + const jsi::Value *args, + size_t count) { + if (count == 0) { + throw jsi::JSError( + rt, + "setImmediate must be called with at least one argument (a function to call)"); + } + + if (!args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) { + throw jsi::JSError( + rt, "The first argument to setImmediate must be a function."); + } + auto callback = args[0].getObject(rt).getFunction(rt); + + // Package up the remaining argument values into one place. + std::vector moreArgs; + for (size_t extraArgNum = 1; extraArgNum < count; extraArgNum++) { + moreArgs.emplace_back(rt, args[extraArgNum]); + } + + auto handle = createReactNativeMicrotask( + std::move(callback), std::move(moreArgs)); + return jsi::Object::createFromHostObject(rt, handle); + })); + + runtime.global().setProperty( + runtime, + "clearImmediate", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "clearImmediate"), + 1, // handle + [this]( + jsi::Runtime &rt, + const jsi::Value &thisVal, + const jsi::Value *args, + size_t count) { + if (count == 0 || !args[0].isObject() || + !args[0].asObject(rt).isHostObject(rt)) { + return jsi::Value::undefined(); + } + std::shared_ptr handle = + args[0].asObject(rt).asHostObject(rt); + deleteReactNativeMicrotask(rt, handle); + return jsi::Value::undefined(); + })); + + runtime.global().setProperty( + runtime, + "setTimeout", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "setTimeout"), + 3, // Function, delay, ...args + [this]( + jsi::Runtime &rt, + const jsi::Value &thisVal, + const jsi::Value *args, + size_t count) { + if (count == 0) { + throw jsi::JSError( + rt, + "setTimeout must be called with at least one argument (the function to call)."); + } + + if (!args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) { + throw jsi::JSError( + rt, "The first argument to setTimeout must be a function."); + } + auto callback = args[0].getObject(rt).getFunction(rt); + + if (count > 1 && !args[1].isNumber() && !args[1].isUndefined()) { + throw jsi::JSError( + rt, + "The second argument to setTimeout must be a number or undefined."); + } + auto delay = + count > 1 && args[1].isNumber() ? args[1].getNumber() : 0; + + // Package up the remaining argument values into one place. + std::vector moreArgs; + for (size_t extraArgNum = 2; extraArgNum < count; extraArgNum++) { + moreArgs.emplace_back(rt, args[extraArgNum]); + } + + auto handle = + createTimer(std::move(callback), std::move(moreArgs), delay); + return jsi::Object::createFromHostObject(rt, handle); + })); + + runtime.global().setProperty( + runtime, + "clearTimeout", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "clearTimeout"), + 1, // timerID + [this]( + jsi::Runtime &rt, + const jsi::Value &thisVal, + const jsi::Value *args, + size_t count) { + if (count == 0 || !args[0].isObject() || + !args[0].asObject(rt).isHostObject(rt)) { + return jsi::Value::undefined(); + } + std::shared_ptr host = + args[0].asObject(rt).asHostObject(rt); + deleteTimer(rt, host); + return jsi::Value::undefined(); + })); + + runtime.global().setProperty( + runtime, + "setInterval", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "setInterval"), + 3, // Function, delay, ...args + [this]( + jsi::Runtime &rt, + const jsi::Value &thisVal, + const jsi::Value *args, + size_t count) { + if (count < 2) { + throw jsi::JSError( + rt, + "setInterval must be called with at least two arguments (the function to call and the delay)."); + } + + if (!args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) { + throw jsi::JSError( + rt, "The first argument to setInterval must be a function."); + } + auto callback = args[0].getObject(rt).getFunction(rt); + + if (!args[1].isNumber()) { + throw jsi::JSError( + rt, "The second argument to setInterval must be a number."); + } + auto delay = args[1].getNumber(); + + // Package up the remaining argument values into one place. + std::vector moreArgs; + for (size_t extraArgNum = 2; extraArgNum < count; extraArgNum++) { + moreArgs.emplace_back(rt, args[extraArgNum]); + } + + auto handle = createRecurringTimer( + std::move(callback), std::move(moreArgs), delay); + return jsi::Object::createFromHostObject(rt, handle); + })); + + runtime.global().setProperty( + runtime, + "clearInterval", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "clearInterval"), + 1, // timerID + [this]( + jsi::Runtime &rt, + const jsi::Value &thisVal, + const jsi::Value *args, + size_t count) { + if (count == 0 || !args[0].isObject() || + !args[0].asObject(rt).isHostObject(rt)) { + return jsi::Value::undefined(); + } + std::shared_ptr host = + args[0].asObject(rt).asHostObject(rt); + deleteRecurringTimer(rt, host); + return jsi::Value::undefined(); + })); + + runtime.global().setProperty( + runtime, + "requestAnimationFrame", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "requestAnimationFrame"), + 1, // callback + [this]( + jsi::Runtime &rt, + const jsi::Value &thisVal, + const jsi::Value *args, + size_t count) { + if (count == 0) { + throw jsi::JSError( + rt, + "requestAnimationFrame must be called with at least one argument (i.e: a callback)"); + } + + if (!args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) { + throw jsi::JSError( + rt, + "The first argument to requestAnimationFrame must be a function."); + } + + using CallbackContainer = std::tuple; + auto callback = jsi::Function::createFromHostFunction( + rt, + jsi::PropNameID::forAscii(rt, "RN$rafFn"), + 0, + [callbackContainer = std::make_shared( + args[0].getObject(rt).getFunction(rt))]( + jsi::Runtime &rt, + const jsi::Value &thisVal, + const jsi::Value *args, + size_t count) { + auto performance = + rt.global().getPropertyAsObject(rt, "performance"); + auto nowFn = performance.getPropertyAsFunction(rt, "now"); + auto now = nowFn.callWithThis(rt, performance, {}); + + return std::get<0>(*callbackContainer) + .call(rt, {std::move(now)}); + }); + + // The current implementation of requestAnimationFrame is the same + // as setTimeout(0). This isn't exactly how requestAnimationFrame + // is supposed to work on web, and may change in the future. + auto handle = createTimer( + std::move(callback), + std::vector(), + /* delay */ 0); + return jsi::Object::createFromHostObject(rt, handle); + })); + + runtime.global().setProperty( + runtime, + "cancelAnimationFrame", + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, "cancelAnimationFrame"), + 1, // timerID + [this]( + jsi::Runtime &rt, + const jsi::Value &thisVal, + const jsi::Value *args, + size_t count) { + if (count == 0 || !args[0].isObject() || + !args[0].asObject(rt).isHostObject(rt)) { + return jsi::Value::undefined(); + } + std::shared_ptr host = + args[0].asObject(rt).asHostObject(rt); + deleteTimer(rt, host); + return jsi::Value::undefined(); + })); +} + +} // namespace react +} // namespace facebook diff --git a/packages/react-native/ReactCommon/react/bridgeless/TimerManager.h b/packages/react-native/ReactCommon/react/bridgeless/TimerManager.h new file mode 100644 index 00000000000..24e0ff9cce2 --- /dev/null +++ b/packages/react-native/ReactCommon/react/bridgeless/TimerManager.h @@ -0,0 +1,112 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#pragma once + +#include +#include +#include +#include + +#include "PlatformTimerRegistry.h" + +namespace facebook { +namespace react { + +/* + * A HostObject subclass representing the result of a setTimeout call. + * Can be used as an argument to clearTimeout. + */ +class TimerHandle : public jsi::HostObject { + public: + explicit TimerHandle(uint32_t index) : index_(index) {} + + uint32_t index() const { + return index_; + } + + ~TimerHandle() override = default; + + private: + // Index in the timeouts_ map of the owning SetTimeoutQueue. + uint32_t index_; +}; + +/* + * Wraps a jsi::Function to make it copyable so we can pass it into a lambda. + */ +struct TimerCallback { + TimerCallback(TimerCallback &&) = default; + + TimerCallback( + jsi::Function callback, + std::vector args, + bool repeat) + : callback_(std::move(callback)), + args_(std::move(args)), + repeat(repeat) {} + + void invoke(jsi::Runtime &runtime) { + callback_.call(runtime, args_.data(), args_.size()); + } + + jsi::Function callback_; + const std::vector args_; + bool repeat; +}; + +class TimerManager { + public: + explicit TimerManager( + std::unique_ptr platformTimerRegistry) noexcept; + + void setRuntimeExecutor(RuntimeExecutor runtimeExecutor) noexcept; + + void callReactNativeMicrotasks(jsi::Runtime &runtime); + + void callTimer(uint32_t); + + void attachGlobals(jsi::Runtime &runtime); + + private: + std::shared_ptr createReactNativeMicrotask( + jsi::Function &&callback, + std::vector &&args); + + void deleteReactNativeMicrotask( + jsi::Runtime &runtime, + std::shared_ptr handle); + + std::shared_ptr createTimer( + jsi::Function &&callback, + std::vector &&args, + double delay); + + void deleteTimer(jsi::Runtime &runtime, std::shared_ptr handle); + + std::shared_ptr createRecurringTimer( + jsi::Function &&callback, + std::vector &&args, + double delay); + + void deleteRecurringTimer( + jsi::Runtime &runtime, + std::shared_ptr handle); + + RuntimeExecutor runtimeExecutor_; + std::unique_ptr platformTimerRegistry_; + + // A map (id => callback func) of the currently active JS timers + std::unordered_map> timers_; + + // Each timeout that is registered on this queue gets a sequential id. This + // is the global count from which those are assigned. + uint64_t timerIndex_{0}; + + // The React Native microtask queue is used to back public APIs including + // `queueMicrotask`, `clearImmediate`, and `setImmediate` (which is used by + // the Promise polyfill) when the JSVM microtask mechanism is not used. + std::vector reactNativeMicrotasksQueue_; +}; + +} // namespace react +} // namespace facebook diff --git a/packages/react-native/ReactCommon/react/bridgeless/tests/hermes/ReactInstanceTest.cpp b/packages/react-native/ReactCommon/react/bridgeless/tests/hermes/ReactInstanceTest.cpp new file mode 100644 index 00000000000..7266b06ba42 --- /dev/null +++ b/packages/react-native/ReactCommon/react/bridgeless/tests/hermes/ReactInstanceTest.cpp @@ -0,0 +1,865 @@ +// (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +using ::testing::_; +using ::testing::SaveArg; + +namespace facebook { +namespace react { + +class MockTimerRegistry : public PlatformTimerRegistry { + public: + MOCK_METHOD2(createTimer, void(uint32_t, double)); + MOCK_METHOD2(createRecurringTimer, void(uint32_t, double)); + MOCK_METHOD1(deleteTimer, void(uint32_t)); +}; + +class MockMessageQueueThread : public MessageQueueThread { + public: + void runOnQueue(std::function &&func) { + callbackQueue_.push(func); + } + + // Unused + void runOnQueueSync(std::function &&) {} + + // Unused + void quitSynchronous() {} + + void tick() { + if (!callbackQueue_.empty()) { + auto callback = callbackQueue_.front(); + callback(); + callbackQueue_.pop(); + } + } + + void guardedTick() { + try { + tick(); + } catch (const std::exception &e) { + // For easier debugging + FAIL() << e.what(); + } + } + + size_t size() { + return callbackQueue_.size(); + } + + private: + std::queue> callbackQueue_; +}; + +class ErrorUtils : public jsi::HostObject { + public: + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override { + auto methodName = name.utf8(rt); + + if (methodName == "reportFatalError") { + return jsi::Function::createFromHostFunction( + rt, + name, + 1, + [this]( + jsi::Runtime &runtime, + const jsi::Value &thisValue, + const jsi::Value *arguments, + size_t count) { + if (count >= 1) { + auto value = jsi::Value(runtime, std::move(arguments[0])); + auto error = jsi::JSError(runtime, std::move(value)); + reportFatalError(std::move(error)); + } + return jsi::Value::undefined(); + }); + } else { + throw std::runtime_error("Unknown method: " + methodName); + } + } + + void reportFatalError(jsi::JSError &&error) { + errors_.push_back(std::move(error)); + } + + size_t size() { + return errors_.size(); + } + + jsi::JSError getLastError() { + auto error = errors_.back(); + errors_.pop_back(); + return error; + } + + private: + std::vector errors_; +}; + +class ReactInstanceTest : public ::testing::Test { + protected: + ReactInstanceTest() {} + + void SetUp() override { + auto runtime = hermes::makeHermesRuntime(); + runtime_ = runtime.get(); + messageQueueThread_ = std::make_shared(); + auto mockRegistry = std::make_unique(); + mockRegistry_ = mockRegistry.get(); + timerManager_ = std::make_shared(std::move(mockRegistry)); + auto jsErrorHandlingFunc = [](MapBuffer errorMap) noexcept { + // Do nothing + }; + instance_ = std::make_unique( + std::move(runtime), + messageQueueThread_, + timerManager_, + std::move(jsErrorHandlingFunc)); + timerManager_->setRuntimeExecutor(instance_->getBufferedRuntimeExecutor()); + + // Install a C++ error handler + errorHandler_ = std::make_shared(); + runtime_->global().setProperty( + *runtime_, + "ErrorUtils", + jsi::Object::createFromHostObject(*runtime_, errorHandler_)); + } + + void initializeRuntimeWithScript( + ReactInstance::JSRuntimeFlags jsRuntimeFlags, + std::string script) { + instance_->initializeRuntime(jsRuntimeFlags, [](jsi::Runtime &runtime) {}); + step(); + + // Run the main bundle, so that native -> JS calls no longer get buffered. + loadScript(script); + } + + void initializeRuntimeWithScript(std::string script) { + instance_->initializeRuntime( + {.isProfiling = false}, [](jsi::Runtime &runtime) {}); + step(); + + // Run the main bundle, so that native -> JS calls no longer get buffered. + loadScript(script); + } + + jsi::Value eval(std::string js) { + RuntimeExecutor runtimeExecutor = instance_->getUnbufferedRuntimeExecutor(); + jsi::Value ret = jsi::Value::undefined(); + runtimeExecutor([js, &ret](jsi::Runtime &runtime) { + ret = runtime.evaluateJavaScript( + std::make_unique(js), ""); + }); + step(); + return ret; + } + + // Call instance_->loadScript() to evaluate JS script and flush buffered JS + // calls + jsi::Value loadScript(std::string js) { + jsi::Value ret = jsi::Value::undefined(); + instance_->loadScript(std::make_unique(std::move(js)), ""); + step(); + return ret; + } + + void expectError() { + EXPECT_NE(errorHandler_->size(), 0) + << "Expected an error to have been thrown, but it wasn't."; + } + + void expectNoError() { + EXPECT_EQ(errorHandler_->size(), 0) + << "Expected no error to have been thrown, but one was."; + } + + std::string getLastErrorMessage() { + auto error = errorHandler_->getLastError(); + return error.getMessage(); + } + + std::string getErrorMessage(std::string js) { + eval(js); + return getLastErrorMessage(); + } + + void step() { + messageQueueThread_->guardedTick(); + } + + jsi::Runtime *runtime_; + std::shared_ptr messageQueueThread_; + std::unique_ptr instance_; + std::shared_ptr timerManager_; + MockTimerRegistry *mockRegistry_; + std::shared_ptr errorHandler_; +}; + +TEST_F(ReactInstanceTest, testBridgelessFlagIsSet) { + eval("RN$Bridgeless === true"); + expectError(); + initializeRuntimeWithScript(""); + auto val = eval("RN$Bridgeless === true"); + EXPECT_EQ(val.getBool(), true); +} + +TEST_F(ReactInstanceTest, testProfilingFlag) { + eval("__RCTProfileIsProfiling === true"); + expectError(); + initializeRuntimeWithScript({.isProfiling = true}, ""); + auto val = eval("__RCTProfileIsProfiling === true"); + EXPECT_EQ(val.getBool(), true); +} + +TEST_F(ReactInstanceTest, testPromiseIntegration) { + initializeRuntimeWithScript(""); + + eval(R"xyz123( +let called = 0; +function getResult() { + return called; +} +Promise.resolve().then(() => { + called++; +}).then(() => { + called++; +}) +)xyz123"); + auto result = runtime_->global() + .getPropertyAsFunction(*runtime_, "getResult") + .call(*runtime_); + EXPECT_EQ(result.getNumber(), 2); +} + +TEST_F(ReactInstanceTest, testSetImmediate) { + initializeRuntimeWithScript(""); + + eval(R"xyz123( +let called = false; +function getResult() { + return called; +} +setImmediate(() => { + called = true; +}); +)xyz123"); + auto result = runtime_->global() + .getPropertyAsFunction(*runtime_, "getResult") + .call(*runtime_); + EXPECT_EQ(result.getBool(), true); +} + +TEST_F(ReactInstanceTest, testNestedSetImmediate) { + initializeRuntimeWithScript(""); + + eval(R"xyz123( +let called = false; +function getResult() { + return called; +} +setImmediate(() => { + setImmediate(() => { + called = true; + }) +}); +)xyz123"); + auto result = runtime_->global() + .getPropertyAsFunction(*runtime_, "getResult") + .call(*runtime_); + EXPECT_EQ(result.getBool(), true); +} + +TEST_F(ReactInstanceTest, testSetImmediateWithInvalidArgs) { + initializeRuntimeWithScript(""); + + EXPECT_EQ( + getErrorMessage("setImmediate();"), + "setImmediate must be called with at least one argument (a function to call)"); + EXPECT_EQ( + getErrorMessage("setImmediate('invalid');"), + "The first argument to setImmediate must be a function."); + EXPECT_EQ( + getErrorMessage("setImmediate({});"), + "The first argument to setImmediate must be a function."); +} + +TEST_F(ReactInstanceTest, testClearImmediate) { + initializeRuntimeWithScript(""); + + eval(R"xyz123( +let called = false; +function getResult() { + return called; +} +const handle = setImmediate(() => { + called = true; +}); +clearImmediate(handle); +)xyz123"); + + auto func = runtime_->global().getPropertyAsFunction(*runtime_, "getResult"); + auto val = func.call(*runtime_); + EXPECT_EQ(val.getBool(), false); +} + +TEST_F(ReactInstanceTest, testClearImmediateWithInvalidHandle) { + initializeRuntimeWithScript(""); + + auto js = R"xyz123( +let called = false; +const handle = setImmediate(() => { + called = true; +}); +function getResult() { + return called; +} +function clearInvalidHandle() { + clearImmediate(handle); +} +)xyz123"; + eval(js); + + auto clear = + runtime_->global().getPropertyAsFunction(*runtime_, "clearInvalidHandle"); + // Clearing an invalid handle should fail silently. + EXPECT_NO_THROW(clear.call((*runtime_))); +} + +TEST_F(ReactInstanceTest, testClearImmediateWithInvalidArgs) { + initializeRuntimeWithScript(""); + + eval("clearImmediate();"); + expectNoError(); + + eval("clearImmediate('invalid');"); + expectNoError(); + + eval("clearImmediate({});"); + expectNoError(); + + eval("clearImmediate(undefined);"); + expectNoError(); +} + +TEST_F(ReactInstanceTest, testSetTimeout) { + initializeRuntimeWithScript(""); + + uint32_t timerID{0}; + EXPECT_CALL(*mockRegistry_, createTimer(_, 100)) + .WillOnce(SaveArg<0>(&timerID)); + eval(R"xyz123( +let called = false; +setTimeout(() => { + called = true; +}, 100); +function getResult() { + return called; +} + )xyz123"); + timerManager_->callTimer(timerID); + step(); + auto called = runtime_->global() + .getPropertyAsFunction(*runtime_, "getResult") + .call(*runtime_); + EXPECT_EQ(called.getBool(), true); +} + +TEST_F(ReactInstanceTest, testSetTimeoutWithoutDelay) { + initializeRuntimeWithScript(""); + + EXPECT_CALL( + *mockRegistry_, + createTimer(_, 0)); // If delay is not provided, it should use 0 + eval("setTimeout(() => {});"); +} + +TEST_F(ReactInstanceTest, testSetTimeoutWithPassThroughArgs) { + initializeRuntimeWithScript(""); + + uint32_t timerID{0}; + EXPECT_CALL(*mockRegistry_, createTimer(_, 0)).WillOnce(SaveArg<0>(&timerID)); + eval(R"xyz123( +let result; +setTimeout(arg => { + result = arg; +}, undefined, 'foo'); +function getResult() { + return result; +} + )xyz123"); + timerManager_->callTimer(timerID); + step(); + auto result = runtime_->global() + .getPropertyAsFunction(*runtime_, "getResult") + .call(*runtime_); + EXPECT_EQ(result.asString(*runtime_).utf8(*runtime_), "foo"); +} + +TEST_F(ReactInstanceTest, testSetTimeoutWithInvalidArgs) { + initializeRuntimeWithScript(""); + + EXPECT_EQ( + getErrorMessage("setTimeout();"), + "setTimeout must be called with at least one argument (the function to call)."); + EXPECT_EQ( + getErrorMessage("setTimeout('invalid');"), + "The first argument to setTimeout must be a function."); + EXPECT_EQ( + getErrorMessage("setTimeout(() => {}, 'invalid');"), + "The second argument to setTimeout must be a number or undefined."); +} + +TEST_F(ReactInstanceTest, testClearTimeout) { + initializeRuntimeWithScript(""); + + uint32_t timerID{0}; + EXPECT_CALL(*mockRegistry_, createTimer(_, 100)) + .WillOnce(SaveArg<0>(&timerID)); + eval(R"xyz123( +const handle = setTimeout(() => {}, 100); +function clear() { + clearTimeout(handle); +} + )xyz123"); + EXPECT_CALL(*mockRegistry_, deleteTimer(timerID)); + runtime_->global().getPropertyAsFunction(*runtime_, "clear").call(*runtime_); +} + +TEST_F(ReactInstanceTest, testClearTimeoutWithInvalidArgs) { + initializeRuntimeWithScript(""); + + eval("clearTimeout();"); + expectNoError(); + + eval("clearTimeout('invalid');"); + expectNoError(); + + eval("clearTimeout({});"); + expectNoError(); + + eval("clearTimeout(undefined);"); + expectNoError(); +} + +TEST_F(ReactInstanceTest, testClearTimeoutForExpiredTimer) { + initializeRuntimeWithScript(""); + + uint32_t timerID{0}; + EXPECT_CALL(*mockRegistry_, createTimer(_, 100)) + .WillOnce(SaveArg<0>(&timerID)); + eval(R"xyz123( +const handle = setTimeout(() => {}, 100); +function clear() { + clearTimeout(handle); +} + )xyz123"); + // Call the timer + timerManager_->callTimer(timerID); + step(); + + // Now clear the called timer + EXPECT_CALL(*mockRegistry_, deleteTimer(timerID)); + auto clear = runtime_->global().getPropertyAsFunction(*runtime_, "clear"); + EXPECT_NO_THROW(clear.call(*runtime_)); +} + +TEST_F(ReactInstanceTest, testSetInterval) { + initializeRuntimeWithScript(""); + + uint32_t timerID{0}; + EXPECT_CALL(*mockRegistry_, createRecurringTimer(_, 100)) + .WillOnce(SaveArg<0>(&timerID)); + eval(R"xyz123( +let result = 0; +setInterval(() => { + result++; +}, 100); +function getResult() { + return result; +} + )xyz123"); + timerManager_->callTimer(timerID); + step(); + auto getResult = + runtime_->global().getPropertyAsFunction(*runtime_, "getResult"); + EXPECT_EQ(getResult.call(*runtime_).asNumber(), 1.0); + + // Should be able to call the same callback again. + timerManager_->callTimer(timerID); + step(); + EXPECT_EQ(getResult.call(*runtime_).asNumber(), 2.0); +} + +TEST_F(ReactInstanceTest, testSetIntervalWithPassThroughArgs) { + initializeRuntimeWithScript(""); + + uint32_t timerID{0}; + EXPECT_CALL(*mockRegistry_, createRecurringTimer(_, 100)) + .WillOnce(SaveArg<0>(&timerID)); + eval(R"xyz123( +let result; +setInterval(arg => { + result = arg; +}, 100, 'foo'); +function getResult() { + return result; +} + )xyz123"); + timerManager_->callTimer(timerID); + step(); + + auto getResult = + runtime_->global().getPropertyAsFunction(*runtime_, "getResult"); + EXPECT_EQ( + getResult.call(*runtime_).asString(*runtime_).utf8(*runtime_), "foo"); +} + +TEST_F(ReactInstanceTest, testSetIntervalWithInvalidArgs) { + initializeRuntimeWithScript(""); + + EXPECT_EQ( + getErrorMessage("setInterval();"), + "setInterval must be called with at least two arguments (the function to call and the delay)."); + EXPECT_EQ( + getErrorMessage("setInterval(() => {});"), + "setInterval must be called with at least two arguments (the function to call and the delay)."); + EXPECT_EQ( + getErrorMessage("setInterval('invalid', 100);"), + "The first argument to setInterval must be a function."); + EXPECT_EQ( + getErrorMessage("setInterval(() => {}, 'invalid');"), + "The second argument to setInterval must be a number."); +} + +TEST_F(ReactInstanceTest, testClearInterval) { + initializeRuntimeWithScript(""); + + uint32_t timerID{0}; + EXPECT_CALL(*mockRegistry_, createRecurringTimer(_, 100)) + .WillOnce(SaveArg<0>(&timerID)); + eval(R"xyz123( +let result = 0; +const handle = setInterval(() => { + result++; +}, 100); +function clear() { + clearInterval(handle); +} +function getResult() { + return result; +} + )xyz123"); + timerManager_->callTimer(timerID); + step(); + auto getResult = + runtime_->global().getPropertyAsFunction(*runtime_, "getResult"); + EXPECT_EQ(getResult.call(*runtime_).asNumber(), 1.0); + + EXPECT_CALL(*mockRegistry_, deleteTimer(timerID)); + runtime_->global().getPropertyAsFunction(*runtime_, "clear").call(*runtime_); + step(); + + timerManager_->callTimer(timerID); + step(); + // Callback should not have been invoked again. + EXPECT_EQ(getResult.call(*runtime_).asNumber(), 1.0); +} + +TEST_F(ReactInstanceTest, testClearIntervalWithInvalidArgs) { + initializeRuntimeWithScript(""); + + eval("clearInterval();"); + expectNoError(); + + eval("clearInterval(false);"); + expectNoError(); + + eval("clearInterval({});"); + expectNoError(); + + eval("clearInterval(undefined);"); + expectNoError(); +} + +TEST_F(ReactInstanceTest, testRequestAnimationFrame) { + initializeRuntimeWithScript(""); + + uint32_t timerID{0}; + EXPECT_CALL(*mockRegistry_, createTimer(_, 0)).WillOnce(SaveArg<0>(&timerID)); + eval(R"xyz123( +let called = false; +performance = { + now: () => 0 +} +requestAnimationFrame(() => { + called = true; +}); +function getResult() { + return called; +} + )xyz123"); + auto getResult = + runtime_->global().getPropertyAsFunction(*runtime_, "getResult"); + EXPECT_EQ(getResult.call(*runtime_).getBool(), false); + + timerManager_->callTimer(timerID); + step(); + EXPECT_EQ(getResult.call(*runtime_).getBool(), true); +} + +TEST_F( + ReactInstanceTest, + testRequestAnimationFrameCallbackArgIsPerformanceNow) { + initializeRuntimeWithScript(""); + + uint32_t timerID{0}; + EXPECT_CALL(*mockRegistry_, createTimer(_, 0)).WillOnce(SaveArg<0>(&timerID)); + eval(R"xyz123( +let now = 0; +performance = { + now: () => 123456 +} +requestAnimationFrame(($now) => { + now = $now; +}); +function getResult() { + return now; +} + )xyz123"); + auto getResult = + runtime_->global().getPropertyAsFunction(*runtime_, "getResult"); + EXPECT_EQ(getResult.call(*runtime_).getNumber(), 0); + + timerManager_->callTimer(timerID); + step(); + EXPECT_EQ(getResult.call(*runtime_).getNumber(), 123456); +} + +TEST_F(ReactInstanceTest, testRequestAnimationFrameWithInvalidArgs) { + initializeRuntimeWithScript(""); + + eval(R"xyz123( +performance = { + now: () => 0 +} + )xyz123"); + + EXPECT_EQ( + getErrorMessage("requestAnimationFrame();"), + "requestAnimationFrame must be called with at least one argument (i.e: a callback)"); + EXPECT_EQ( + getErrorMessage("requestAnimationFrame('invalid');"), + "The first argument to requestAnimationFrame must be a function."); + EXPECT_EQ( + getErrorMessage("requestAnimationFrame({});"), + "The first argument to requestAnimationFrame must be a function."); +} + +TEST_F(ReactInstanceTest, testCancelAnimationFrame) { + initializeRuntimeWithScript(""); + + uint32_t timerID{0}; + EXPECT_CALL(*mockRegistry_, createTimer(_, 0)).WillOnce(SaveArg<0>(&timerID)); + eval(R"xyz123( +let called = false; +performance = { + now: () => 0 +} +const handle = requestAnimationFrame(() => { + called = true; +}); +function clear() { + cancelAnimationFrame(handle); +} +function getResult() { + return called; +} + )xyz123"); + + EXPECT_CALL(*mockRegistry_, deleteTimer(timerID)); + runtime_->global().getPropertyAsFunction(*runtime_, "clear").call(*runtime_); + + // Attempt to call timer; should fail silently. + timerManager_->callTimer(timerID); + step(); + + // Verify the callback was not called. + auto called = runtime_->global() + .getPropertyAsFunction(*runtime_, "getResult") + .call(*runtime_); + EXPECT_EQ(called.getBool(), false); +} + +TEST_F(ReactInstanceTest, testCancelAnimationFrameWithInvalidArgs) { + initializeRuntimeWithScript(""); + + eval("cancelAnimationFrame();"); + expectNoError(); + + eval("cancelAnimationFrame(false);"); + expectNoError(); + + eval("cancelAnimationFrame({});"); + expectNoError(); + + eval("cancelAnimationFrame(undefined);"); + expectNoError(); +} + +TEST_F(ReactInstanceTest, testCancelAnimationFrameWithExpiredTimer) { + initializeRuntimeWithScript(""); + + uint32_t timerID{0}; + EXPECT_CALL(*mockRegistry_, createTimer(_, 0)).WillOnce(SaveArg<0>(&timerID)); + eval(R"xyz123( +let called = false; +performance = { + now: () => 0 +} +const handle = requestAnimationFrame(() => { + called = true; +}); +function clear() { + cancelAnimationFrame(handle); +} +function getResult() { + return called; +} + )xyz123"); + + timerManager_->callTimer(timerID); + step(); + + auto called = runtime_->global() + .getPropertyAsFunction(*runtime_, "getResult") + .call(*runtime_); + EXPECT_EQ(called.getBool(), true); + + EXPECT_CALL(*mockRegistry_, deleteTimer(timerID)); + auto clear = runtime_->global().getPropertyAsFunction(*runtime_, "clear"); + // Canceling an expired timer should fail silently. + EXPECT_NO_THROW(clear.call(*runtime_)); +} + +TEST_F(ReactInstanceTest, testRegisterCallableModule) { + initializeRuntimeWithScript(R"xyz123( +let called = false; +const module = { + bar: () => { + called = true; + }, +}; +function getResult() { + return called; +} +RN$registerCallableModule('foo', () => module); + )xyz123"); + + auto args = folly::dynamic::array(0); + instance_->callFunctionOnModule("foo", "bar", std::move(args)); + step(); + + auto called = runtime_->global() + .getPropertyAsFunction(*runtime_, "getResult") + .call(*runtime_); + EXPECT_EQ(called.getBool(), true); +} + +TEST_F(ReactInstanceTest, testRegisterCallableModule_invalidArgs) { + initializeRuntimeWithScript(""); + + EXPECT_EQ( + getErrorMessage("RN$registerCallableModule();"), + "registerCallableModule requires exactly 2 arguments"); + EXPECT_EQ( + getErrorMessage("RN$registerCallableModule('foo');"), + "registerCallableModule requires exactly 2 arguments"); + EXPECT_EQ( + getErrorMessage("RN$registerCallableModule(1, () => ({}));"), + "The first argument to registerCallableModule must be a string (the name of the JS module)."); + EXPECT_EQ( + getErrorMessage("RN$registerCallableModule('foo', false);"), + "The second argument to registerCallableModule must be a function that returns the JS module."); +} + +TEST_F(ReactInstanceTest, testCallFunctionOnModule_invalidModule) { + initializeRuntimeWithScript(""); + + auto args = folly::dynamic::array(0); + instance_->callFunctionOnModule("invalidModule", "method", std::move(args)); + step(); + expectError(); + EXPECT_EQ( + getLastErrorMessage(), + "Failed to call into JavaScript module method invalidModule.method(). Module has not been registered as callable. Registered callable JavaScript modules (n = 0):. Did you forget to call `RN$registerCallableModule`?"); +} + +TEST_F(ReactInstanceTest, testCallFunctionOnModule_undefinedMethod) { + initializeRuntimeWithScript(R"xyz123( +const module = { + bar: () => {}, +}; +RN$registerCallableModule('foo', () => module); + )xyz123"); + + auto args = folly::dynamic::array(0); + instance_->callFunctionOnModule("foo", "invalidMethod", std::move(args)); + step(); + expectError(); + EXPECT_EQ( + getLastErrorMessage(), + "Failed to call into JavaScript module method foo.invalidMethod. Module exists, but the method is undefined."); +} + +TEST_F(ReactInstanceTest, testCallFunctionOnModule_invalidMethod) { + initializeRuntimeWithScript(R"xyz123( +const module = { + bar: false, +}; +RN$registerCallableModule('foo', () => module); + )xyz123"); + + auto args = folly::dynamic::array(0); + instance_->callFunctionOnModule("foo", "bar", std::move(args)); + step(); + expectError(); +} + +TEST_F(ReactInstanceTest, testRegisterCallableModule_withArgs) { + initializeRuntimeWithScript(R"xyz123( +let result; +const module = { + bar: thing => { + result = thing; + }, +}; +RN$registerCallableModule('foo', () => module); +function getResult() { + return result; +} + )xyz123"); + + auto args = folly::dynamic::array(1); + instance_->callFunctionOnModule("foo", "bar", std::move(args)); + step(); + + auto result = runtime_->global() + .getPropertyAsFunction(*runtime_, "getResult") + .call(*runtime_); + EXPECT_EQ(result.getNumber(), 1); +} + +} // namespace react +} // namespace facebook