每个实例的状态


Node.js 有“Node.js 实例”的概念,通常被称为 node::Environment。 每个 node::Environment 都与:

  • 正好是 v8::Isolate,即 JS 引擎实例,
  • 正好是 uv_loop_t,即事件循环,并且
  • 许多 v8::Context,但只有一个主要的 v8::Context
  • node::IsolateData 实例包含的信息可以由使用相同 v8::Isolate 的多个 node::Environment 共享。 目前,没有针对此场景执行测试。

为了设置 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 事件循环。
  uv_loop_t loop;
  int ret = uv_loop_init(&loop);
  if (ret != 0) {
    fprintf(stderr, "%s: Failed to initialize loop: %s\n",
            args[0].c_str(),
            uv_err_name(ret));
    return 1;
  }

  std::shared_ptr<ArrayBufferAllocator> allocator =
      ArrayBufferAllocator::Create();

  Isolate* isolate = NewIsolate(allocator, &loop, platform);
  if (isolate == nullptr) {
    fprintf(stderr, "%s: Failed to initialize V8 Isolate\n", args[0].c_str());
    return 1;
  }

  {
    Locker locker(isolate);
    Isolate::Scope isolate_scope(isolate);

    // 创建 node::IsolateData 实例,
    // 稍后将使用 node::FreeIsolateData() 释放该实例。
    std::unique_ptr<IsolateData, decltype(&node::FreeIsolateData)> isolate_data(
        node::CreateIsolateData(isolate, &loop, platform, allocator.get()),
        node::FreeIsolateData);

    // 设置新的 v8::Context。
    HandleScope handle_scope(isolate);
    Local<Context> context = node::NewContext(isolate);
    if (context.IsEmpty()) {
      fprintf(stderr, "%s: Failed to initialize V8 Context\n", args[0].c_str());
      return 1;
    }

    // 当调用 node::CreateEnvironment() 和 node::LoadEnvironment() 时,
    // 需要输入 v8::Context。
    Context::Scope context_scope(context);

    // 创建 node::Environment 实例,
    // 稍后将使用 node::FreeEnvironment() 释放该实例。
    std::unique_ptr<Environment, decltype(&node::FreeEnvironment)> env(
        node::CreateEnvironment(isolate_data.get(), context, args, exec_args),
        node::FreeEnvironment);

    // 设置要执行的 Node.js 实例,并在其中运行代码。
    // 还有一个变体接受回调
    // 并为其提供 `require` 和 `process` 对象,
    // 以便它可以根据需要手动编译和运行脚本。
    // 此脚本中的 `require` 函数不访问文件系统,
    // 并且只能加载 Node.js 内置模块。
    // `module.createRequire()` 被用来创建能够从磁盘加载文件的文件,
    // 并使用标准的 CommonJS 文件加载器
    // 而不是内部的 `require` 函数。
    MaybeLocal<Value> loadenv_ret = node::LoadEnvironment(
        env.get(),
        "const publicRequire ="
        "  require('module').createRequire(process.cwd() + '/');"
        "globalThis.require = publicRequire;"
        "require('vm').runInThisContext(process.argv[1]);");

    if (loadenv_ret.IsEmpty())  // 出现了 JS 异常。
      return 1;

    {
      // SealHandleScope 防止来自回调的句柄泄漏。
      SealHandleScope seal(isolate);
      bool more;
      do {
        uv_run(&loop, UV_RUN_DEFAULT);

        // 后台线程上的 V8 任务可能最终会在前台调度新任务,从而使事件循环继续进行。
        // 例如,
        // WebAssembly.compile() 可能会这样做。
        platform->DrainTasks(isolate);

        // 如果有新任务,则继续。
        more = uv_loop_alive(&loop);
        if (more) continue;

        // node::EmitProcessBeforeExit() 用于在 `process` 对象上
        // 触发 'beforeExit' 事件。
        if (node::EmitProcessBeforeExit(env.get()).IsNothing())
          break;

        // 'beforeExit' 还可以调度新的新工作,
        // 以使事件循环保持运行。
        more = uv_loop_alive(&loop);
      } while (more == true);
    }

    // node::EmitProcessExit() 返回当前的退出码。
    exit_code = node::EmitProcessExit(env.get()).FromMaybe(1);

    // node::Stop() 可用于显式停止事件循环并阻止进一步的 JavaScript 运行。
    // 它可以从任何线程调用,
    // 如果从另一个线程调用,则像 worker.terminate() 一样。
    node::Stop(env.get());
  }

  // 取消向平台注册 Isolate 并添加监听器,
  // 当平台完成清理它与 Isolate 关联的任何状态时,
  // 则调用该监听器。
  bool platform_finished = false;
  platform->AddIsolateFinishedCallback(isolate, [](void* data) {
    *static_cast<bool*>(data) = true;
  }, &platform_finished);
  platform->UnregisterIsolate(isolate);
  isolate->Dispose();

  // 等到平台清理完所有相关资源。
  while (!platform_finished)
    uv_run(&loop, UV_RUN_ONCE);
  int err = uv_loop_close(&loop);
  assert(err == 0);

  return exit_code;
}

Node.js has a concept of a “Node.js instance”, that is commonly being referred to as node::Environment. Each node::Environment is associated with:

  • Exactly one v8::Isolate, i.e. one JS Engine instance,
  • Exactly one uv_loop_t, i.e. one event loop, and
  • A number of v8::Contexts, but exactly one main v8::Context.
  • One node::IsolateData instance that contains information that could be shared by multiple node::Environments that use the same v8::Isolate. Currently, no testing if performed for this scenario.

In order to set up a v8::Isolate, an v8::ArrayBuffer::Allocator needs to be provided. One possible choice is the default Node.js allocator, which can be created through node::ArrayBufferAllocator::Create(). Using the Node.js allocator allows minor performance optimizations when addons use the Node.js C++ Buffer API, and is required in order to track ArrayBuffer memory in process.memoryUsage().

Additionally, each v8::Isolate that is used for a Node.js instance needs to be registered and unregistered with the MultiIsolatePlatform instance, if one is being used, in order for the platform to know which event loop to use for tasks scheduled by the v8::Isolate.

The node::NewIsolate() helper function creates a v8::Isolate, sets it up with some Node.js-specific hooks (e.g. the Node.js error handler), and registers it with the platform automatically.

int RunNodeInstance(MultiIsolatePlatform* platform,
                    const std::vector<std::string>& args,
                    const std::vector<std::string>& exec_args) {
  int exit_code = 0;
  // Set up a libuv event loop.
  uv_loop_t loop;
  int ret = uv_loop_init(&loop);
  if (ret != 0) {
    fprintf(stderr, "%s: Failed to initialize loop: %s\n",
            args[0].c_str(),
            uv_err_name(ret));
    return 1;
  }

  std::shared_ptr<ArrayBufferAllocator> allocator =
      ArrayBufferAllocator::Create();

  Isolate* isolate = NewIsolate(allocator, &loop, platform);
  if (isolate == nullptr) {
    fprintf(stderr, "%s: Failed to initialize V8 Isolate\n", args[0].c_str());
    return 1;
  }

  {
    Locker locker(isolate);
    Isolate::Scope isolate_scope(isolate);

    // Create a node::IsolateData instance that will later be released using
    // node::FreeIsolateData().
    std::unique_ptr<IsolateData, decltype(&node::FreeIsolateData)> isolate_data(
        node::CreateIsolateData(isolate, &loop, platform, allocator.get()),
        node::FreeIsolateData);

    // Set up a new v8::Context.
    HandleScope handle_scope(isolate);
    Local<Context> context = node::NewContext(isolate);
    if (context.IsEmpty()) {
      fprintf(stderr, "%s: Failed to initialize V8 Context\n", args[0].c_str());
      return 1;
    }

    // The v8::Context needs to be entered when node::CreateEnvironment() and
    // node::LoadEnvironment() are being called.
    Context::Scope context_scope(context);

    // Create a node::Environment instance that will later be released using
    // node::FreeEnvironment().
    std::unique_ptr<Environment, decltype(&node::FreeEnvironment)> env(
        node::CreateEnvironment(isolate_data.get(), context, args, exec_args),
        node::FreeEnvironment);

    // Set up the Node.js instance for execution, and run code inside of it.
    // There is also a variant that takes a callback and provides it with
    // the `require` and `process` objects, so that it can manually compile
    // and run scripts as needed.
    // The `require` function inside this script does *not* access the file
    // system, and can only load built-in Node.js modules.
    // `module.createRequire()` is being used to create one that is able to
    // load files from the disk, and uses the standard CommonJS file loader
    // instead of the internal-only `require` function.
    MaybeLocal<Value> loadenv_ret = node::LoadEnvironment(
        env.get(),
        "const publicRequire ="
        "  require('module').createRequire(process.cwd() + '/');"
        "globalThis.require = publicRequire;"
        "require('vm').runInThisContext(process.argv[1]);");

    if (loadenv_ret.IsEmpty())  // There has been a JS exception.
      return 1;

    {
      // SealHandleScope protects against handle leaks from callbacks.
      SealHandleScope seal(isolate);
      bool more;
      do {
        uv_run(&loop, UV_RUN_DEFAULT);

        // V8 tasks on background threads may end up scheduling new tasks in the
        // foreground, which in turn can keep the event loop going. For example,
        // WebAssembly.compile() may do so.
        platform->DrainTasks(isolate);

        // If there are new tasks, continue.
        more = uv_loop_alive(&loop);
        if (more) continue;

        // node::EmitProcessBeforeExit() is used to emit the 'beforeExit' event
        // on the `process` object.
        if (node::EmitProcessBeforeExit(env.get()).IsNothing())
          break;

        // 'beforeExit' can also schedule new work that keeps the event loop
        // running.
        more = uv_loop_alive(&loop);
      } while (more == true);
    }

    // node::EmitProcessExit() returns the current exit code.
    exit_code = node::EmitProcessExit(env.get()).FromMaybe(1);

    // node::Stop() can be used to explicitly stop the event loop and keep
    // further JavaScript from running. It can be called from any thread,
    // and will act like worker.terminate() if called from another thread.
    node::Stop(env.get());
  }

  // Unregister the Isolate with the platform and add a listener that is called
  // when the Platform is done cleaning up any state it had associated with
  // the Isolate.
  bool platform_finished = false;
  platform->AddIsolateFinishedCallback(isolate, [](void* data) {
    *static_cast<bool*>(data) = true;
  }, &platform_finished);
  platform->UnregisterIsolate(isolate);
  isolate->Dispose();

  // Wait until the platform has cleaned up all relevant resources.
  while (!platform_finished)
    uv_run(&loop, UV_RUN_ONCE);
  int err = uv_loop_close(&loop);
  assert(err == 0);

  return exit_code;
}