Create HermesRuntimeAgent (#42747)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/42747

Changelog: [Internal]

Implements a `RuntimeAgent` (D51231326) for Hermes for the modern CDP backend, based on the `CDPHandler` API that Hermes exposes currently.

## A note on `console`

We unfortunately have to disable `console` interception (D51234334 / equivalently D52971652) because `CDPHandler`'s current implementation is not aligned with the Agent concept:

* Agents are only created once a session has started, but the `console` interceptor needs to be injected at VM startup.
* Agents should not clobber each other's shared state (nor consume excessive resources per Agent), but each `CDPHandler` would install its own independent `console` interceptor if enabled.

We will enable CDP `console` support in the modern backend in future work. This will require either some additional plumbing in RN (e.g. to safely access JSI from an Agent/Target) or some additional work in Hermes.

## Conditional compilation based on `HERMES_ENABLE_DEBUGGER`

`HermesRuntimeAgent.cpp` compiles both with and without `-DHERMES_ENABLE_DEBUGGER`, which is the flag Hermes uses to control the availability of `CDPHandler` (and its containing Buck library).

If the debugger is not enabled, `HermesRuntimeAgent` reduces to a `FallbackRuntimeAgent`. In either case, no Hermes debugger headers leak into `HermesRuntimeAgent.h`, so callers don't need to check `#ifdef HERMES_ENABLE_DEBUGGER`, and the overall CDP backend infra is not gated on whether the Hermes debugger is compiled in.

Reviewed By: huntie

Differential Revision: D51234333

fbshipit-source-id: ccbca443560308c5edba4b9689501d01059fdd94
This commit is contained in:
Moti Zilberman
2024-02-02 10:24:10 -08:00
committed by Facebook GitHub Bot
parent 4e6eba7a2d
commit cc34acefab
11 changed files with 338 additions and 7 deletions
@@ -42,7 +42,7 @@ Pod::Spec.new do |s|
s.header_dir = "reacthermes"
s.dependency "React-cxxreact", version
s.dependency "React-jsiexecutor", version
s.dependency "React-jsinspector", version
add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern')
s.dependency "React-perflogger", version
s.dependency "RCT-Folly", folly_version
s.dependency "DoubleConversion"
@@ -50,4 +50,5 @@ Pod::Spec.new do |s|
s.dependency "glog"
s.dependency "hermes-engine"
s.dependency "React-jsi"
s.dependency "React-runtimeexecutor"
end
@@ -15,6 +15,7 @@
#include <jsi/decorator.h>
#include <jsinspector-modern/InspectorFlags.h>
#include <hermes/inspector-modern/chrome/HermesRuntimeAgent.h>
#include <hermes/inspector-modern/chrome/Registration.h>
#include <hermes/inspector/RuntimeAdapter.h>
@@ -230,7 +231,12 @@ std::unique_ptr<JSExecutor> HermesExecutorFactory::createJSExecutor(
errorPrototype.setProperty(*decoratedRuntime, "jsEngine", "hermes");
return std::make_unique<HermesExecutor>(
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<ExecutorDelegate> delegate,
std::shared_ptr<MessageQueueThread> 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<jsinspector_modern::RuntimeAgent>
HermesExecutor::createRuntimeAgent(
jsinspector_modern::FrontendChannel frontendChannel,
jsinspector_modern::SessionState& sessionState) {
std::shared_ptr<HermesRuntime> hermesRuntimeShared(runtime_, &hermesRuntime_);
return std::unique_ptr<jsinspector_modern::RuntimeAgent>(
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
@@ -51,10 +51,18 @@ class HermesExecutor : public JSIExecutor {
std::shared_ptr<ExecutorDelegate> delegate,
std::shared_ptr<MessageQueueThread> jsQueue,
const JSIScopedTimeoutInvoker& timeoutInvoker,
RuntimeInstaller runtimeInstaller);
RuntimeInstaller runtimeInstaller,
hermes::HermesRuntime& hermesRuntime);
virtual std::unique_ptr<jsinspector_modern::RuntimeAgent> createRuntimeAgent(
jsinspector_modern::FrontendChannel frontendChannel,
jsinspector_modern::SessionState& sessionState) override;
private:
JSIScopedTimeoutInvoker timeoutInvoker_;
std::shared_ptr<MessageQueueThread> jsQueue_;
std::shared_ptr<jsi::Runtime> runtime_;
hermes::HermesRuntime& hermesRuntime_;
};
} // namespace facebook::react
@@ -33,4 +33,5 @@ target_link_libraries(hermes_inspector_modern
fb
glog
hermes-engine::libhermes
jsi)
jsi
runtimeexecutor)
@@ -0,0 +1,169 @@
/*
* 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"
// If HERMES_ENABLE_DEBUGGER isn't defined, we can't access any Hermes
// CDPHandler headers or types.
#ifdef HERMES_ENABLE_DEBUGGER
#include <hermes/inspector/RuntimeAdapter.h>
#include <hermes/inspector/chrome/CDPHandler.h>
#else // HERMES_ENABLE_DEBUGGER
#include <jsinspector-modern/FallbackRuntimeAgent.h>
#endif // HERMES_ENABLE_DEBUGGER
#include <hermes/hermes.h>
#include <jsinspector-modern/ReactCdp.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 hermes::inspector_modern::RuntimeAdapter {
public:
HermesRuntimeAgentAdapter(
std::shared_ptr<hermes::HermesRuntime> 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<hermes::HermesRuntime> runtime_;
RuntimeExecutor runtimeExecutor_;
};
} // namespace
/**
* A RuntimeAgent that handles requests from the Chrome DevTools Protocol for
* an instance of Hermes.
*/
class HermesRuntimeAgent::Impl final : public RuntimeAgent {
using HermesCDPHandler = hermes::inspector_modern::chrome::CDPHandler;
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.
*/
Impl(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::shared_ptr<hermes::HermesRuntime> runtime,
RuntimeExecutor runtimeExecutor)
: hermes_(HermesCDPHandler::create(
std::make_unique<HermesRuntimeAgentAdapter>(
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 */
[]() {});
}
/**
* 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 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;
}
private:
std::shared_ptr<HermesCDPHandler> hermes_;
};
#else // !HERMES_ENABLE_DEBUGGER
/**
* A stub for HermesRuntimeAgent when Hermes is compiled without debugging
* support.
*/
class HermesRuntimeAgent::Impl final : public FallbackRuntimeAgent {
public:
Impl(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::shared_ptr<hermes::HermesRuntime> runtime,
RuntimeExecutor)
: FallbackRuntimeAgent(
std::move(frontendChannel),
sessionState,
runtime->description()) {}
};
#endif // HERMES_ENABLE_DEBUGGER
HermesRuntimeAgent::HermesRuntimeAgent(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::shared_ptr<hermes::HermesRuntime> runtime,
RuntimeExecutor runtimeExecutor)
: impl_(std::make_unique<Impl>(
std::move(frontendChannel),
sessionState,
std::move(runtime),
std::move(runtimeExecutor))) {}
bool HermesRuntimeAgent::handleRequest(const cdp::PreparsedRequest& req) {
return impl_->handleRequest(req);
}
} // namespace facebook::react::jsinspector_modern
@@ -0,0 +1,58 @@
/*
* 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 <ReactCommon/RuntimeExecutor.h>
#include <hermes/hermes.h>
#include <jsinspector-modern/ReactCdp.h>
namespace facebook::react::jsinspector_modern {
/**
* A RuntimeAgent that handles requests from the Chrome DevTools Protocol for
* an instance of Hermes.
*/
class HermesRuntimeAgent : public RuntimeAgent {
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<hermes::HermesRuntime> runtime,
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> impl_;
};
} // namespace facebook::react::jsinspector_modern
@@ -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
@@ -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;
};
/**
@@ -51,8 +51,10 @@ Pod::Spec.new do |s|
s.dependency "React-jsi"
s.dependency "React-RuntimeCore"
s.dependency "React-featureflags"
add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern')
if ENV["USE_HERMES"] == nil || ENV["USE_HERMES"] == "1"
s.dependency "React-hermes"
s.dependency "hermes-engine"
else
s.dependency "React-jsc"
@@ -24,6 +24,7 @@ target_link_libraries(bridgelesshermes
hermes_executor_common
bridgeless
react_featureflags
jsinspector
)
if(${CMAKE_BUILD_TYPE} MATCHES Debug)
@@ -7,6 +7,7 @@
#include "HermesInstance.h"
#include <hermes/inspector-modern/chrome/HermesRuntimeAgent.h>
#include <jsi/jsilib.h>
#include <jsinspector-modern/InspectorFlags.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
@@ -91,6 +92,47 @@ class DecoratedRuntime : public jsi::RuntimeDecorator<jsi::Runtime> {
#endif
class HermesJSRuntime : public JSRuntime {
public:
HermesJSRuntime(
std::unique_ptr<HermesRuntime> runtime,
std::shared_ptr<MessageQueueThread> msgQueueThread)
: runtime_(std::move(runtime)),
msgQueueThread_(std::move(msgQueueThread)) {}
jsi::Runtime& getRuntime() noexcept override {
return *runtime_;
}
std::unique_ptr<jsinspector_modern::RuntimeAgent> createInspectorAgent(
jsinspector_modern::FrontendChannel frontendChannel,
jsinspector_modern::SessionState& sessionState) override {
return std::unique_ptr<jsinspector_modern::RuntimeAgent>(
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<HermesRuntime> runtime_;
std::shared_ptr<MessageQueueThread> msgQueueThread_;
};
std::unique_ptr<JSRuntime> HermesInstance::createJSRuntime(
std::shared_ptr<const ReactNativeConfig> reactNativeConfig,
std::shared_ptr<::hermes::vm::CrashManager> cm,
@@ -142,7 +184,8 @@ std::unique_ptr<JSRuntime> HermesInstance::createJSRuntime(
}
#endif
return std::make_unique<JSIRuntimeHolder>(std::move(hermesRuntime));
return std::make_unique<HermesJSRuntime>(
std::move(hermesRuntime), std::move(msgQueueThread));
}
} // namespace facebook::react