C++ 嵌入器 API
History
Node.js 提供了许多 C++ API,可用于从其他 C++ 软件在 Node.js 环境中执行 JavaScript。
这些 API 的文档可以在 Node.js 源代码树中的 src/node.h 找到。除了 Node.js 暴露的 API 外,一些必需的概念由 V8 嵌入器 API 提供。
因为将 Node.js 用作嵌入库与编写由 Node.js 执行的代码不同,所以破坏性变更不遵循典型的 Node.js 弃用策略,并且可能在每个语义化主版本发布时发生,而无需事先警告。
以下章节将概述如何使用这些 API 从头创建一个应用程序,该应用程序将执行相当于 node -e <code> 的操作,即获取一段 JavaScript 并在特定于 Node.js 的环境中运行它。
完整代码可以在 Node.js 源代码树中找到。
Node.js 运行需要一些每进程状态管理:
- 解析 Node.js [命令行选项][] 的参数,
- V8 每进程要求,例如
v8::Platform实例。
以下示例展示了如何设置这些。一些类名分别来自 node 和 v8 C++ 命名空间。
int main(int argc, char** argv) { argv = uv_setup_args(argc, argv); std::vector<std::string> args(argv, argv + argc); // 解析 Node.js CLI 选项,并打印任何尝试解析它们时发生的错误。 std::unique_ptr<node::InitializationResult> result = node::InitializeOncePerProcess(args, { node::ProcessInitializationFlags::kNoInitializeV8, node::ProcessInitializationFlags::kNoInitializeNodeV8Platform }); for (const std::string& error : result->errors()) fprintf(stderr, "%s: %s\n", args[0].c_str(), error.c_str()); if (result->early_return() != 0) { return result->exit_code(); } // 创建一个 v8::Platform 实例。`MultiIsolatePlatform::Create()` 是一种 // 创建一个 Node.js 在创建 Worker 线程时可以使用的 v8::Platform 实例的方法。当不存在 `MultiIsolatePlatform` 实例时, // Worker 线程将被禁用。 std::unique_ptr<MultiIsolatePlatform> platform = MultiIsolatePlatform::Create(4); V8::InitializePlatform(platform.get()); V8::Initialize(); // 参见下方了解此函数的内容。 int ret = RunNodeInstance( platform.get(), result->args(), result->exec_args()); V8::Dispose(); V8::DisposePlatform(); node::TearDownOncePerProcess(); return ret; }
限制对环境变量的访问
History
当传递给 node::InitializeOncePerProcess() 的参数启用了[权限模型][],但未指定 --allow-env=* 时,进程环境中不得包含任何未由 --allow-env 授权访问的变量。否则,node::InitializeOncePerProcess() 会失败。与 node 可执行文件不同,嵌入器拥有进程环境,因此 Node.js 不会自行移除这些变量。
node::ScrubProcessEnvironment() 会移除这些变量。由于它会在没有任何锁的情况下修改进程环境,而调用 getenv() 的原生代码不会参与此锁,因此必须在启动任何可能读取环境的线程之前,以及在调用 node::InitializeOncePerProcess() 之前调用它:
int main(int argc, char** argv) { argv = uv_setup_args(argc, argv); std::vector<std::string> args(argv, argv + argc); // 除了 Node.js 读取的变量之外,保留嵌入器自身读取的变量 // (参见 node::GetRuntimeEnvironmentDefaults())。 node::ProcessEnvironmentScrubOptions scrub_options; scrub_options.allow = {"PORT", "APP_*"}; if (node::ScrubProcessEnvironment(scrub_options).IsNothing()) { return 1; } // args 包含例如 --permission --allow-env=PORT std::unique_ptr<node::InitializationResult> result = node::InitializeOncePerProcess(args, { node::ProcessInitializationFlags::kNoInitializeV8, node::ProcessInitializationFlags::kNoInitializeNodeV8Platform }); // ... }
process.permission.drop('env', name) 会从进程环境中移除一个变量,因此从在未使用 node::EnvironmentFlags::kOwnsProcessState 的情况下创建的 node::Environment 中调用时会抛出异常。
Node.js 有一个“Node.js 实例”的概念,通常被称为 node::Environment。每个 node::Environment 关联着:
- 恰好一个
v8::Isolate,即一个 JS 引擎实例, - 恰好一个
uv_loop_t,即一个事件循环, - 若干个
v8::Context,但恰好一个主v8::Context,以及 - 一个
node::IsolateData实例,其中包含可由多个node::Environment共享的信息。嵌入器应确保node::IsolateData仅在共享相同v8::Isolate的node::Environment之间共享,Node.js 不执行此检查。
共享 node::IsolateData 的 node::Environment 也共享其 uv_loop_t。node::FreeEnvironment() 会运行该循环,直到要释放的 node::Environment 的句柄关闭为止;在此期间,整个 v8::Isolate 上都不允许执行 JavaScript,因此,同一循环上属于其他 node::Environment 的待处理计时器、I/O 回调和线程池任务完成回调都可能在该调用期间运行,但无法调用 JavaScript。相互独立地释放的 node::Environment 应各自使用自己的 uv_loop_t 和 node::IsolateData,或者嵌入器应确保释放其中一个时,其他实例没有待处理的工作。
要设置 v8::Isolate,需要提供一个 v8::ArrayBuffer::Allocator。一种可能的选择是 Node.js 默认分配器,它可以通过 node::ArrayBufferAllocator::Create() 创建。当插件使用 Node.js C++ Buffer API 时,使用 Node.js 分配器可以进行一些小幅性能优化;此外,为了在 process.memoryUsage() 中跟踪 ArrayBuffer 内存,也必须使用该分配器。
此外,每个用于 Node.js 实例的 v8::Isolate 都需要向 MultiIsolatePlatform 实例注册和注销(如果正在使用),以便平台知道为该 v8::Isolate 调度的任务使用哪个事件循环。
node::NewIsolate() 辅助函数创建一个 v8::Isolate,使用一些特定于 Node.js 的钩子(例如 Node.js 错误处理程序)对其进行设置,并自动将其注册到平台。
int RunNodeInstance(MultiIsolatePlatform* platform, const std::vector<std::string>& args, const std::vector<std::string>& exec_args) { int exit_code = 0; // 设置 libuv 事件循环、v8::Isolate 和 Node.js Environment。 std::vector<std::string> errors; std::unique_ptr<CommonEnvironmentSetup> setup = CommonEnvironmentSetup::Create(platform, &errors, args, exec_args); if (!setup) { for (const std::string& err : errors) fprintf(stderr, "%s: %s\n", args[0].c_str(), err.c_str()); return 1; } Isolate* isolate = setup->isolate(); Environment* env = setup->env(); { Locker locker(isolate); Isolate::Scope isolate_scope(isolate); HandleScope handle_scope(isolate); // 在调用 node::CreateEnvironment() 和 // node::LoadEnvironment() 时需要进入 v8::Context。 Context::Scope context_scope(setup->context()); // 设置 Node.js 实例以执行,并在其中运行代码。 // 还有一个变体接受一个回调并向其提供 // `require` 和 `process` 对象,以便它可以手动编译 // 并按需运行脚本。 // 此脚本内的 `require` 函数*不*访问文件 // 系统,并且只能加载内置 Node.js 模块。 // 正在使用 `module.createRequire()` 创建一个能够从 // 磁盘加载文件的对象,并使用标准的 CommonJS 文件加载器 // 而不是内部专用的 `require` 函数。 MaybeLocal<Value> loadenv_ret = node::LoadEnvironment( env, "const publicRequire =" " require('node:module').createRequire(process.cwd() + '/');" "globalThis.require = publicRequire;" "require('node:vm').runInThisContext(process.argv[1]);"); if (loadenv_ret.IsEmpty()) // 发生了 JS 异常。 return 1; exit_code = node::SpinEventLoop(env).FromMaybe(1); // node::Stop() 可用于显式停止事件循环并阻止 // 进一步运行 JavaScript。它可以从任何线程调用, // 如果从另一个线程调用,其行为将类似于 worker.terminate()。 node::Stop(env); } return exit_code; }