Skip to content

Commit

Permalink
Create HermesRuntimeAgent (#42747)
Browse files Browse the repository at this point in the history
Summary:

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.

Reviewed By: huntie

Differential Revision: D51234333
  • Loading branch information
motiz88 authored and facebook-github-bot committed Feb 1, 2024
1 parent 78f5002 commit 8a19709
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 5 deletions.
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
@@ -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<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

HermesRuntimeAgent::HermesRuntimeAgent(
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 */
[]() {});
}

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<hermes::HermesRuntime> runtime,
RuntimeExecutor runtimeExecutor)
: FallbackRuntimeAgent(
std::move(frontendChannel),
sessionState,
runtime->description()) {}

#endif // HERMES_ENABLE_DEBUGGER

} // namespace facebook::react::jsinspector_modern
Original file line number Diff line number Diff line change
@@ -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 <ReactCommon/RuntimeExecutor.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/API/hermes/hermes.h>
#include <jsinspector-modern/ReactCdp.h>

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<hermes::HermesRuntime> 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<HermesCDPHandler> hermes_;
#endif
};

} // 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 @@ -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>
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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

0 comments on commit 8a19709

Please sign in to comment.