From 166fdc51250cf5033f59bc018552378e9687af15 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Fri, 8 Mar 2024 07:29:22 -0800 Subject: [PATCH] Create wrapper class for Hermes CDPAgent + CDPDebugAPI (#43352) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/43352 ## Context We are migrating to the new Hermes `CDPAgent` and `CDPDebugAPI` APIs in the modern CDP server (previously `HermesCDPHandler`). ## This diff Adds the `HermesRuntimeAgentDelegateNew` class to provide a swap-in replacement for the existing `HermesRuntimeAgentDelegate` when we enable this via an incoming feature flag. Changelog: [Internal] Reviewed By: motiz88 Differential Revision: D53810356 fbshipit-source-id: c63684252230a747ecf0bd8cbb6f4e22052ed9bf --- .github/workflows/ios-tests.yml | 2 +- .../chrome/HermesRuntimeAgentDelegateNew.cpp | 132 ++++++++++++++++++ .../chrome/HermesRuntimeAgentDelegateNew.h | 75 ++++++++++ .../chrome/HermesRuntimeTargetDelegate.cpp | 26 ++++ .../chrome/HermesRuntimeTargetDelegate.h | 16 +++ .../sdks/hermes-engine/hermes-engine.podspec | 6 + .../utils/build-apple-framework.sh | 6 + 7 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgentDelegateNew.cpp create mode 100644 packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgentDelegateNew.h diff --git a/.github/workflows/ios-tests.yml b/.github/workflows/ios-tests.yml index 7677425ab0b..cdfe1189593 100644 --- a/.github/workflows/ios-tests.yml +++ b/.github/workflows/ios-tests.yml @@ -36,7 +36,7 @@ jobs: uses: actions/cache@v3 with: path: packages/rn-tester/Pods - key: v1-${{ runner.os }}-RNTesterPods-${{ hashFiles('packages/rn-tester/Podfile.lock') }}-${{ hashFiles('packages/rn-tester/Podfile') }}-${{ hashFiles('tmp/hermes/hermesversion') }} + key: v2-${{ runner.os }}-RNTesterPods-${{ hashFiles('packages/rn-tester/Podfile.lock') }}-${{ hashFiles('packages/rn-tester/Podfile') }}-${{ hashFiles('tmp/hermes/hermesversion') }} - name: Pod Install run: | cd packages/rn-tester diff --git a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgentDelegateNew.cpp b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgentDelegateNew.cpp new file mode 100644 index 00000000000..429b7e69132 --- /dev/null +++ b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgentDelegateNew.cpp @@ -0,0 +1,132 @@ +/* + * 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 "HermesRuntimeAgentDelegateNew.h" + +// If HERMES_ENABLE_DEBUGGER isn't defined, we can't access any Hermes +// CDP headers or types. + +#ifdef HERMES_ENABLE_DEBUGGER +#include +#include +#include +#else // HERMES_ENABLE_DEBUGGER +#include +#endif // HERMES_ENABLE_DEBUGGER + +#include +#include + +using namespace facebook::hermes; + +namespace facebook::react::jsinspector_modern { + +#ifdef HERMES_ENABLE_DEBUGGER + +class HermesRuntimeAgentDelegateNew::Impl final : public RuntimeAgentDelegate { + public: + Impl( + FrontendChannel frontendChannel, + SessionState& /*unused*/, + std::unique_ptr + previouslyExportedState, + const ExecutionContextDescription& executionContextDescription, + HermesRuntime& runtime, + HermesRuntimeTargetDelegate& runtimeTargetDelegate, + const RuntimeExecutor& runtimeExecutor) + : hermes_(hermes::cdp::CDPAgent::create( + executionContextDescription.id, + runtimeTargetDelegate.getCDPDebugAPI(), + // RuntimeTask takes a HermesRuntime whereas our RuntimeExecutor + // takes a jsi::Runtime. + [runtimeExecutor, + &runtime](facebook::hermes::debugger::RuntimeTask fn) { + runtimeExecutor( + [&runtime, fn = std::move(fn)](auto&) { fn(runtime); }); + }, + std::move(frontendChannel))) { + // TODO(T178858701): Pass previouslyExportedState to CDPAgent + (void)previouslyExportedState; + } + + /** + * 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 { + // 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 CDPAgent. + hermes_->handleCommand(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; + } + + private: + std::unique_ptr hermes_; +}; + +#else // !HERMES_ENABLE_DEBUGGER + +/** + * A stub for HermesRuntimeAgentDelegateNew when Hermes is compiled without + * debugging support. + */ +class HermesRuntimeAgentDelegateNew::Impl final + : public FallbackRuntimeAgentDelegate { + public: + Impl( + FrontendChannel frontendChannel, + SessionState& sessionState, + std::unique_ptr, + const ExecutionContextDescription&, + HermesRuntime& runtime, + HermesRuntimeTargetDelegate& runtimeTargetDelegate, + RuntimeExecutor) + : FallbackRuntimeAgentDelegate( + std::move(frontendChannel), + sessionState, + runtime.description()) {} +}; + +#endif // HERMES_ENABLE_DEBUGGER + +HermesRuntimeAgentDelegateNew::HermesRuntimeAgentDelegateNew( + FrontendChannel frontendChannel, + SessionState& sessionState, + std::unique_ptr + previouslyExportedState, + const ExecutionContextDescription& executionContextDescription, + HermesRuntime& runtime, + HermesRuntimeTargetDelegate& runtimeTargetDelegate, + RuntimeExecutor runtimeExecutor) + : impl_(std::make_unique( + std::move(frontendChannel), + sessionState, + std::move(previouslyExportedState), + executionContextDescription, + runtime, + runtimeTargetDelegate, + std::move(runtimeExecutor))) {} + +bool HermesRuntimeAgentDelegateNew::handleRequest( + const cdp::PreparsedRequest& req) { + return impl_->handleRequest(req); +} + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgentDelegateNew.h b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgentDelegateNew.h new file mode 100644 index 00000000000..2505451eb27 --- /dev/null +++ b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeAgentDelegateNew.h @@ -0,0 +1,75 @@ +/* + * 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 "HermesRuntimeTargetDelegate.h" + +#include +#include +#include + +namespace facebook::react::jsinspector_modern { + +/** + * A RuntimeAgentDelegate that handles requests from the Chrome DevTools + * Protocol for an instance of Hermes, using the new CDPAgent API. + */ +class HermesRuntimeAgentDelegateNew : public RuntimeAgentDelegate { + 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 previouslyExportedState The exported state from a previous instance + * of RuntimeAgentDelegate (NOT necessarily HermesRuntimeAgentDelegateNew). + * This may be nullptr, and if not nullptr it may be of any concrete type that + * implements RuntimeAgentDelegate::ExportedState. + * \param executionContextDescription A description of the execution context + * represented by this runtime. This is used for disambiguating the + * source/destination of CDP messages when there are multiple runtimes + * (concurrently or over the life of a Page). + * \param runtime The HermesRuntime that this agent is attached to. The caller + * is responsible for keeping this object alive for the duration of the + * \c HermesRuntimeAgentDelegateNew lifetime. + * \param runtimeTargetDelegate The \c HermesRuntimeTargetDelegate object + * object for the passed runtime. + * \param runtimeExecutor A callback for scheduling work on the JS thread. + * \c runtimeExecutor may drop scheduled work if the runtime is destroyed + * first. + */ + HermesRuntimeAgentDelegateNew( + FrontendChannel frontendChannel, + SessionState& sessionState, + std::unique_ptr + previouslyExportedState, + const ExecutionContextDescription& executionContextDescription, + hermes::HermesRuntime& runtime, + HermesRuntimeTargetDelegate& runtimeTargetDelegate, + RuntimeExecutor runtimeExecutor); + + /** + * 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: + // We use the private implementation idiom to keep HERMES_ENABLE_DEBUGGER + // checks out of the header. + class Impl; + + const std::unique_ptr impl_; +}; + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.cpp b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.cpp index 810d9f35af3..c9f651d75c5 100644 --- a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.cpp +++ b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.cpp @@ -8,6 +8,12 @@ #include "HermesRuntimeTargetDelegate.h" #include "HermesRuntimeAgentDelegate.h" +#ifdef HERMES_ENABLE_DEBUGGER +#include + +using namespace facebook::hermes::cdp; +#endif // HERMES_ENABLE_DEBUGGER + #include using namespace facebook::hermes; @@ -16,8 +22,18 @@ namespace facebook::react::jsinspector_modern { class HermesRuntimeTargetDelegate::Impl : public RuntimeTargetDelegate { public: +#ifdef HERMES_ENABLE_DEBUGGER + explicit Impl(std::shared_ptr hermesRuntime) + : runtime_(std::move(hermesRuntime)), + cdpDebugAPI_(CDPDebugAPI::create(*runtime_)) {} + + CDPDebugAPI& getCDPDebugAPI() { + return *cdpDebugAPI_; + } +#else explicit Impl(std::shared_ptr hermesRuntime) : runtime_(std::move(hermesRuntime)) {} +#endif // RuntimeTargetDelegate methods @@ -39,6 +55,10 @@ class HermesRuntimeTargetDelegate::Impl : public RuntimeTargetDelegate { private: std::shared_ptr runtime_; + +#ifdef HERMES_ENABLE_DEBUGGER + const std::unique_ptr cdpDebugAPI_; +#endif }; HermesRuntimeTargetDelegate::HermesRuntimeTargetDelegate( @@ -63,4 +83,10 @@ HermesRuntimeTargetDelegate::createAgentDelegate( std::move(runtimeExecutor)); } +#ifdef HERMES_ENABLE_DEBUGGER +CDPDebugAPI& HermesRuntimeTargetDelegate::getCDPDebugAPI() { + return impl_->getCDPDebugAPI(); +} +#endif + } // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.h b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.h index 08a4da81084..2196568c099 100644 --- a/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.h +++ b/packages/react-native/ReactCommon/hermes/inspector-modern/chrome/HermesRuntimeTargetDelegate.h @@ -13,6 +13,10 @@ #include #include +#ifdef HERMES_ENABLE_DEBUGGER +#include +#endif + #include namespace facebook::react::jsinspector_modern { @@ -42,8 +46,20 @@ class HermesRuntimeTargetDelegate : public RuntimeTargetDelegate { RuntimeExecutor runtimeExecutor) override; private: + // We use the private implementation idiom to ensure this class has the same + // layout regardless of whether HERMES_ENABLE_DEBUGGER is defined. The net + // effect is that callers can include HermesRuntimeTargetDelegate.h without + // setting HERMES_ENABLE_DEBUGGER one way or the other. class Impl; +// Callers within this library may set HERMES_ENABLE_DEBUGGER to see this extra +// API. +#ifdef HERMES_ENABLE_DEBUGGER + friend class HermesRuntimeAgentDelegateNew; + + hermes::cdp::CDPDebugAPI& getCDPDebugAPI(); +#endif + std::unique_ptr impl_; }; diff --git a/packages/react-native/sdks/hermes-engine/hermes-engine.podspec b/packages/react-native/sdks/hermes-engine/hermes-engine.podspec index 335004f1e87..32d71f78fd7 100644 --- a/packages/react-native/sdks/hermes-engine/hermes-engine.podspec +++ b/packages/react-native/sdks/hermes-engine/hermes-engine.podspec @@ -79,6 +79,12 @@ Pod::Spec.new do |spec| ss.header_dir = 'hermes' end + spec.subspec 'cdp' do |ss| + ss.source_files = '' + ss.public_header_files = 'API/hermes/cdp/*.h' + ss.header_dir = 'hermes/cdp' + end + spec.subspec 'inspector' do |ss| ss.source_files = '' ss.public_header_files = 'API/hermes/inspector/*.h' diff --git a/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh b/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh index a4fe7d79024..d3af54bb9ce 100755 --- a/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh +++ b/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh @@ -135,6 +135,9 @@ function build_apple_framework { mkdir -p destroot/include/hermes cp API/hermes/*.h destroot/include/hermes + mkdir -p destroot/include/hermes/cdp + cp API/hermes/cdp/*.h destroot/include/hermes/cdp + mkdir -p destroot/include/hermes/inspector cp API/hermes/inspector/*.h destroot/include/hermes/inspector @@ -162,6 +165,9 @@ function prepare_dest_root_for_ci { mkdir -p destroot/include/hermes cp API/hermes/*.h destroot/include/hermes + mkdir -p destroot/include/hermes/cdp + cp API/hermes/cdp/*.h destroot/include/hermes/cdp + mkdir -p destroot/include/hermes/inspector cp API/hermes/inspector/*.h destroot/include/hermes/inspector