diff --git a/packages/react-native/ReactCommon/hermes/executor/HermesExecutorFactory.cpp b/packages/react-native/ReactCommon/hermes/executor/HermesExecutorFactory.cpp index 3a918680c70cd0..409d0431a57627 100644 --- a/packages/react-native/ReactCommon/hermes/executor/HermesExecutorFactory.cpp +++ b/packages/react-native/ReactCommon/hermes/executor/HermesExecutorFactory.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -230,7 +231,12 @@ std::unique_ptr HermesExecutorFactory::createJSExecutor( errorPrototype.setProperty(*decoratedRuntime, "jsEngine", "hermes"); return std::make_unique( - decoratedRuntime, delegate, jsQueue, timeoutInvoker_, runtimeInstaller_); + decoratedRuntime, + delegate, + jsQueue, + timeoutInvoker_, + runtimeInstaller_, + hermesRuntimeRef); } ::hermes::vm::RuntimeConfig HermesExecutorFactory::defaultRuntimeConfig() { @@ -244,7 +250,37 @@ HermesExecutor::HermesExecutor( std::shared_ptr delegate, std::shared_ptr jsQueue, const JSIScopedTimeoutInvoker& timeoutInvoker, - RuntimeInstaller runtimeInstaller) - : JSIExecutor(runtime, delegate, timeoutInvoker, runtimeInstaller) {} + RuntimeInstaller runtimeInstaller, + HermesRuntime& hermesRuntime) + : JSIExecutor(runtime, delegate, timeoutInvoker, runtimeInstaller), + jsQueue_(jsQueue), + runtime_(runtime), + hermesRuntime_(hermesRuntime) {} + +std::unique_ptr +HermesExecutor::createRuntimeAgent( + jsinspector_modern::FrontendChannel frontendChannel, + jsinspector_modern::SessionState& sessionState) { + std::shared_ptr hermesRuntimeShared(runtime_, &hermesRuntime_); + return std::unique_ptr( + new jsinspector_modern::HermesRuntimeAgent( + frontendChannel, + sessionState, + hermesRuntimeShared, + [jsQueueWeak = std::weak_ptr(jsQueue_), + runtimeWeak = std::weak_ptr(runtime_)](auto fn) { + auto jsQueue = jsQueueWeak.lock(); + if (!jsQueue) { + return; + } + jsQueue->runOnQueue([runtimeWeak, fn]() { + auto runtime = runtimeWeak.lock(); + if (!runtime) { + return; + } + fn(*runtime); + }); + })); +} } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/hermes/executor/HermesExecutorFactory.h b/packages/react-native/ReactCommon/hermes/executor/HermesExecutorFactory.h index 9c53e9d73fa5ab..357d90f8e4acf1 100644 --- a/packages/react-native/ReactCommon/hermes/executor/HermesExecutorFactory.h +++ b/packages/react-native/ReactCommon/hermes/executor/HermesExecutorFactory.h @@ -51,10 +51,18 @@ class HermesExecutor : public JSIExecutor { std::shared_ptr delegate, std::shared_ptr jsQueue, const JSIScopedTimeoutInvoker& timeoutInvoker, - RuntimeInstaller runtimeInstaller); + RuntimeInstaller runtimeInstaller, + hermes::HermesRuntime& hermesRuntime); + + virtual std::unique_ptr createRuntimeAgent( + jsinspector_modern::FrontendChannel frontendChannel, + jsinspector_modern::SessionState& sessionState) override; private: JSIScopedTimeoutInvoker timeoutInvoker_; + std::shared_ptr jsQueue_; + std::shared_ptr runtime_; + hermes::HermesRuntime& hermesRuntime_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgent.cpp b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgent.cpp new file mode 100644 index 00000000000000..f49d3bea1f57d3 --- /dev/null +++ b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgent.cpp @@ -0,0 +1,100 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "HermesRuntimeAgent.h" + +using namespace facebook::hermes; + +namespace facebook::react::jsinspector_modern { + +#ifdef HERMES_ENABLE_DEBUGGER + +namespace { + +/** + * An implementation of the Hermes RuntimeAdapter interface (part of + * Hermes's CDPHandler API) for use within a React Native RuntimeAgent. + */ +class HermesRuntimeAgentAdapter : public HermesRuntimeAdapter { + public: + HermesRuntimeAgentAdapter( + std::shared_ptr runtime, + RuntimeExecutor runtimeExecutor) + : runtime_(runtime), runtimeExecutor_(runtimeExecutor) {} + + HermesRuntime& getRuntime() override { + return *runtime_; + } + + void tickleJs() override { + runtimeExecutor_([](jsi::Runtime& runtime) { + jsi::Function func = + runtime.global().getPropertyAsFunction(runtime, "__tickleJs"); + func.call(runtime); + }); + } + + private: + std::shared_ptr runtime_; + RuntimeExecutor runtimeExecutor_; +}; + +} // namespace + +HermesRuntimeAgent::HermesRuntimeAgent( + FrontendChannel frontendChannel, + SessionState& sessionState, + std::shared_ptr runtime, + RuntimeExecutor runtimeExecutor) + : hermes_(HermesCDPHandler::create( + std::make_unique(runtime, runtimeExecutor), + /* waitForDebugger */ false, + /* enableConsoleAPICapturing */ false, + /* state */ nullptr, + {.isRuntimeDomainEnabled = sessionState.isRuntimeDomainEnabled})) { + hermes_->registerCallbacks( + /* msgCallback */ + [frontendChannel = + std::move(frontendChannel)](const std::string& messageFromHermes) { + frontendChannel(messageFromHermes); + ; + }, + /* onUnregister */ + []() {}); +} + +bool HermesRuntimeAgent::handleRequest(const cdp::PreparsedRequest& req) { + // TODO: Change to string::starts_with when we're on C++20. + if (req.method.rfind("Log.", 0) == 0) { + // Since we know Hermes doesn't do anything useful with Log messages, but + // our containing PageAgent will, just bail out early. + // TODO: We need a way to negotiate this more dynamically with Hermes + // through the API. + return false; + } + // Forward everything else to Hermes's CDPHandler. + hermes_->handle(req.toJson()); + // Let the call know that this request is handled (i.e. it is Hermes's + // responsibility to respond with either success or an error). + return true; +} + +#else // !HERMES_ENABLE_DEBUGGER + +HermesRuntimeAgent::HermesRuntimeAgent( + FrontendChannel frontendChannel, + SessionState& sessionState, + std::shared_ptr runtime, + RuntimeExecutor runtimeExecutor) + : FallbackRuntimeAgent( + std::move(frontendChannel), + sessionState, + runtime->description()) {} + +#endif // HERMES_ENABLE_DEBUGGER + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgent.h b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgent.h new file mode 100644 index 00000000000000..89a228038cc665 --- /dev/null +++ b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgent.h @@ -0,0 +1,77 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +// If HERMES_ENABLE_DEBUGGER isn't defined, we can't access any Hermes +// CDPHandler headers or types. + +#ifdef HERMES_ENABLE_DEBUGGER +#include +#include +#else // HERMES_ENABLE_DEBUGGER +#include +#endif // HERMES_ENABLE_DEBUGGER + +#include +#include + +namespace facebook::react::jsinspector_modern { + +#ifdef HERMES_ENABLE_DEBUGGER +using HermesCDPHandler = hermes::inspector_modern::chrome::CDPHandler; +using HermesRuntimeAdapter = hermes::inspector_modern::RuntimeAdapter; +#endif // HERMES_ENABLE_DEBUGGER + +/** + * A RuntimeAgent that handles requests from the Chrome DevTools Protocol for + * an instance of Hermes. + */ +class HermesRuntimeAgent : +#ifdef HERMES_ENABLE_DEBUGGER + public RuntimeAgent +#else // HERMES_ENABLE_DEBUGGER + public FallbackRuntimeAgent +#endif // HERMES_ENABLE_DEBUGGER +{ + public: + /** + * \param frontendChannel A channel used to send responses and events to the + * frontend. + * \param sessionState The state of the current CDP session. This will only + * be accessed on the main thread (during the constructor, in handleRequest, + * etc). + * \param runtime The HermesRuntime that this agent is attached to. + * \param runtimeExecutor A callback for scheduling work on the JS thread. + * \c runtimeExecutor may drop scheduled work if the runtime is destroyed + * first. + */ + HermesRuntimeAgent( + FrontendChannel frontendChannel, + SessionState& sessionState, + std::shared_ptr runtime, + RuntimeExecutor runtimeExecutor); + +#ifdef HERMES_ENABLE_DEBUGGER + /** + * Handle a CDP request. The response will be sent over the provided + * \c FrontendChannel synchronously or asynchronously. + * \param req The parsed request. + * \returns true if this agent has responded, or will respond asynchronously, + * to the request (with either a success or error message). False if the + * agent expects another agent to respond to the request instead. + */ + bool handleRequest(const cdp::PreparsedRequest& req) override; + + private: + std::shared_ptr hermes_; +#endif +}; + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/Parsing.cpp b/packages/react-native/ReactCommon/jsinspector-modern/Parsing.cpp index ca52a3fcfe9943..99b233aa9ca728 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/Parsing.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/Parsing.cpp @@ -19,4 +19,14 @@ PreparsedRequest preparse(std::string_view message) { .params = parsed.count("params") ? parsed["params"] : nullptr}; } +std::string PreparsedRequest::toJson() const { + folly::dynamic obj = folly::dynamic::object; + obj["id"] = id; + obj["method"] = method; + if (params != nullptr) { + obj["params"] = params; + } + return folly::toJson(obj); +} + } // namespace facebook::react::jsinspector_modern::cdp diff --git a/packages/react-native/ReactCommon/jsinspector-modern/Parsing.h b/packages/react-native/ReactCommon/jsinspector-modern/Parsing.h index 4db24fcb2a3767..bd446fb1971448 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/Parsing.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/Parsing.h @@ -43,6 +43,8 @@ struct PreparsedRequest { inline bool operator==(const PreparsedRequest& rhs) const { return id == rhs.id && method == rhs.method && params == rhs.params; } + + std::string toJson() const; }; /** diff --git a/packages/react-native/ReactCommon/react/runtime/hermes/HermesInstance.cpp b/packages/react-native/ReactCommon/react/runtime/hermes/HermesInstance.cpp index 50ec845f41cf61..782ab9ab8c7933 100644 --- a/packages/react-native/ReactCommon/react/runtime/hermes/HermesInstance.cpp +++ b/packages/react-native/ReactCommon/react/runtime/hermes/HermesInstance.cpp @@ -7,6 +7,7 @@ #include "HermesInstance.h" +#include #include #include #include @@ -91,6 +92,47 @@ class DecoratedRuntime : public jsi::RuntimeDecorator { #endif +class HermesJSRuntime : public JSRuntime { + public: + HermesJSRuntime( + std::unique_ptr runtime, + std::shared_ptr msgQueueThread) + : runtime_(std::move(runtime)), + msgQueueThread_(std::move(msgQueueThread)) {} + + jsi::Runtime& getRuntime() noexcept override { + return *runtime_; + } + + std::unique_ptr createInspectorAgent( + jsinspector_modern::FrontendChannel frontendChannel, + jsinspector_modern::SessionState& sessionState) override { + return std::unique_ptr( + new jsinspector_modern::HermesRuntimeAgent( + frontendChannel, + sessionState, + runtime_, + [msgQueueThreadWeak = std::weak_ptr(msgQueueThread_), + runtimeWeak = std::weak_ptr(runtime_)](auto fn) { + auto msgQueueThread = msgQueueThreadWeak.lock(); + if (!msgQueueThread) { + return; + } + msgQueueThread->runOnQueue([runtimeWeak, fn]() { + auto runtime = runtimeWeak.lock(); + if (!runtime) { + return; + } + fn(*runtime); + }); + })); + } + + private: + std::shared_ptr runtime_; + std::shared_ptr msgQueueThread_; +}; + std::unique_ptr HermesInstance::createJSRuntime( std::shared_ptr reactNativeConfig, std::shared_ptr<::hermes::vm::CrashManager> cm, @@ -142,7 +184,8 @@ std::unique_ptr HermesInstance::createJSRuntime( } #endif - return std::make_unique(std::move(hermesRuntime)); + return std::make_unique( + std::move(hermesRuntime), std::move(msgQueueThread)); } } // namespace facebook::react