mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-08 09:24:41 +00:00
193 lines
7.4 KiB
C++
Vendored
193 lines
7.4 KiB
C++
Vendored
// The Hermes implementation of core's NodeAddons engine capability
|
|
// (GpuiJsEngine.h): create a Node-API environment for a runtime, load one
|
|
// stock `.node` shared library into it and hand the exports back to JSI.
|
|
//
|
|
// The body is ported from `react-native-node-api`'s
|
|
// `CxxNodeApiHostModule::loadNodeAddon` (MIT, Copyright (c) Callstack):
|
|
// https://github.com/callstack/react-native-node-api
|
|
// It no longer takes a library *name* to be resolved by platform convention; it
|
|
// takes the absolute path core resolved, because a desktop app dlopens whatever
|
|
// file it likes.
|
|
#include "GpuiJsEngine.h"
|
|
#include "HermesNodeApiHost.h"
|
|
|
|
#include <ReactCommon/CallInvoker.h>
|
|
#include <jsi/hermes-interfaces.h>
|
|
#include <napi/hermes_napi.h>
|
|
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <utility>
|
|
|
|
namespace rngpui {
|
|
namespace {
|
|
|
|
/// Renders the exception Hermes left pending on `env` as a message, clearing it
|
|
/// so the env is usable again. Returns an empty string if the exception cannot
|
|
/// be read, in which case the caller reports the status alone.
|
|
std::string takePendingExceptionMessage(napi_env env) {
|
|
napi_value error = nullptr;
|
|
if (napi_get_and_clear_last_exception(env, &error) != napi_ok ||
|
|
error == nullptr) {
|
|
return {};
|
|
}
|
|
napi_value asString = nullptr;
|
|
if (napi_coerce_to_string(env, error, &asString) != napi_ok) {
|
|
return {};
|
|
}
|
|
size_t length = 0;
|
|
if (napi_get_value_string_utf8(env, asString, nullptr, 0, &length) !=
|
|
napi_ok) {
|
|
return {};
|
|
}
|
|
std::string message(length, '\0');
|
|
if (napi_get_value_string_utf8(
|
|
env, asString, message.data(), length + 1, &length) != napi_ok) {
|
|
return {};
|
|
}
|
|
message.resize(length);
|
|
return message;
|
|
}
|
|
|
|
/// Loads one addon into `rt` through `hostContext`, returning its exports.
|
|
/// Throws jsi::JSError when the library cannot be loaded or its init function
|
|
/// fails.
|
|
facebook::jsi::Value loadAddon(
|
|
facebook::jsi::Runtime& rt,
|
|
HostContext& hostContext,
|
|
const std::string& absolutePath) {
|
|
// Create this addon's Node-API environment. Hermes binds an env to its
|
|
// low-level VM runtime, which we reach through the (unstable) IHermes JSI
|
|
// interface, and takes ownership: the env is torn down with the runtime, so
|
|
// there is nothing to free here. Each addon gets its own env, as in Node.
|
|
//
|
|
// Fully qualified: a bare `hermes` would be ambiguous with the top-level
|
|
// `::hermes` (VM) namespace pulled in via <jsi/hermes-interfaces.h>.
|
|
auto* hermes = facebook::jsi::castInterface<facebook::hermes::IHermes>(&rt);
|
|
if (hermes == nullptr) {
|
|
throw facebook::jsi::JSError(
|
|
rt,
|
|
"Cannot load '" + absolutePath +
|
|
"': this runtime is not a Hermes runtime, so no Node-API "
|
|
"environment can be created for it");
|
|
}
|
|
napi_env env =
|
|
hermes_napi_create_env(hermes->getVMRuntimeUnsafe(), hostContext.host());
|
|
if (env == nullptr) {
|
|
throw facebook::jsi::JSError(
|
|
rt,
|
|
"Cannot load '" + absolutePath +
|
|
"': hermes_napi_create_env returned null");
|
|
}
|
|
|
|
// A name to reference the exports object by from JSI. Instead of using
|
|
// random numbers to avoid name clashes, we use the address of the env, which
|
|
// is unique per addon per runtime.
|
|
char generatedName[32];
|
|
std::snprintf(
|
|
generatedName,
|
|
sizeof(generatedName),
|
|
"RN$NodeAddon_%p",
|
|
static_cast<void*>(env));
|
|
|
|
// Every napi_value below is created in this scope, and only reachable from
|
|
// the JavaScript global (or dropped) once it closes.
|
|
napi_handle_scope scope = nullptr;
|
|
napi_status status = napi_open_handle_scope(env, &scope);
|
|
assert(status == napi_ok);
|
|
(void)status;
|
|
|
|
napi_value exports = nullptr;
|
|
status = hermes_napi_load_module(env, absolutePath.c_str(), &exports);
|
|
if (status == napi_ok) {
|
|
napi_value global = nullptr;
|
|
status = napi_get_global(env, &global);
|
|
assert(status == napi_ok);
|
|
status = napi_set_named_property(env, global, generatedName, exports);
|
|
assert(status == napi_ok);
|
|
}
|
|
|
|
const bool failed = status != napi_ok;
|
|
std::string message;
|
|
if (failed) {
|
|
message = takePendingExceptionMessage(env);
|
|
if (message.empty() || message == "undefined") {
|
|
// The exception object did not stringify to anything useful, so report
|
|
// the status instead: it is what distinguishes "the file could not be
|
|
// opened" from "the addon's init function failed".
|
|
message = "Node-API status " + std::to_string(status);
|
|
}
|
|
}
|
|
const napi_status closeStatus = napi_close_handle_scope(env, scope);
|
|
assert(closeStatus == napi_ok);
|
|
(void)closeStatus;
|
|
|
|
if (failed) {
|
|
throw facebook::jsi::JSError(
|
|
rt, "Failed to load '" + absolutePath + "': " + message);
|
|
}
|
|
|
|
// Read the exports back through JSI and drop the temporary global. The
|
|
// scope is closed by now, so the only thing keeping the exports object alive
|
|
// is the global property; the value read here is a new reference on the JSI
|
|
// side.
|
|
facebook::jsi::Value exportsValue =
|
|
rt.global().getProperty(rt, generatedName);
|
|
rt.global().setProperty(rt, generatedName, facebook::jsi::Value::undefined());
|
|
return exportsValue;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::function<facebook::jsi::Value(facebook::jsi::Runtime&, const std::string&)>
|
|
makeHermesNodeAddonLoader(
|
|
std::shared_ptr<facebook::react::CallInvoker> jsInvoker) {
|
|
// The JS-thread dispatcher behind the hermes_napi_host integration:
|
|
// CallInvoker::invokeAsync is callable from any thread, never runs the
|
|
// function inline and delivers in order on the JS thread.
|
|
//
|
|
// The invoker is held strongly, and the context that holds this lambda is
|
|
// retained for the process lifetime — the same lifetime the original host
|
|
// module had, since React Native owned one per runtime. That is what keeps
|
|
// async work and thread-safe functions able to reach the JS thread at all;
|
|
// a weak reference here would expire as soon as the caller's shared_ptr
|
|
// went out of scope, and every dispatch would be dropped.
|
|
//
|
|
// Holding a RuntimeSchedulerCallInvoker longer than its runtime is safe: it
|
|
// stores a std::weak_ptr to the RuntimeScheduler, so invokeAsync no-ops once
|
|
// the scheduler is gone and work cannot outlive the runtime it targets. The
|
|
// scheduler is owned by the ReactInstance together with the runtime.
|
|
//
|
|
// Dropping is safe precisely because a drop implies teardown: every env this
|
|
// host serves is owned by that same runtime and destroyed with it, so a
|
|
// completion or tsfn dispatch being dropped has no live observer. The one
|
|
// caller that could still see the difference — napi_cancel_async_work —
|
|
// receives the verdict through this dispatcher's return value (see
|
|
// HostContext::cancelWork).
|
|
//
|
|
// One context per runtime, retained for the process lifetime: see
|
|
// HostContext::retainForProcessLifetime for why the host struct must
|
|
// outlive every env it was passed to.
|
|
auto hostContext = HostContext::create(
|
|
[invoker = std::move(jsInvoker)](std::function<void()>&& fn) {
|
|
if (!invoker) {
|
|
std::fputs(
|
|
"NodeAddons: dropping a task posted after runtime teardown\n",
|
|
stderr);
|
|
return false;
|
|
}
|
|
invoker->invokeAsync(std::move(fn));
|
|
return true;
|
|
});
|
|
HostContext::retainForProcessLifetime(hostContext);
|
|
|
|
return [hostContext](
|
|
facebook::jsi::Runtime& rt, const std::string& absolutePath) {
|
|
return loadAddon(rt, *hostContext, absolutePath);
|
|
};
|
|
}
|
|
|
|
} // namespace rngpui
|