Skip to content

Commit

Permalink
Create HermesRuntimeAgent (#42747)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #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
  • Loading branch information
motiz88 authored and facebook-github-bot committed Feb 2, 2024
1 parent 4e6eba7 commit cc34ace
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ 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"
s.dependency "fmt", "9.1.0"
s.dependency "glog"
s.dependency "hermes-engine"
s.dependency "React-jsi"
s.dependency "React-runtimeexecutor"
end
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down Expand Up @@ -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() {
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ target_link_libraries(hermes_inspector_modern
fb
glog
hermes-engine::libhermes
jsi)
jsi
runtimeexecutor)
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions packages/react-native/ReactCommon/jsinspector-modern/Parsing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ target_link_libraries(bridgelesshermes
hermes_executor_common
bridgeless
react_featureflags
jsinspector
)

if(${CMAKE_BUILD_TYPE} MATCHES Debug)
Expand Down
Loading

0 comments on commit cc34ace

Please sign in to comment.