Skip to content

C++ API для встраивания

Node.js предоставляет ряд C++ API, которые можно использовать для выполнения JavaScript в среде Node.js из другого программного обеспечения на C++.

Документацию по этим API можно найти в src/node.h в исходном дереве Node.js. В дополнение к API, предоставляемым Node.js, некоторые необходимые концепции предоставляются API для встраивания V8.

Поскольку использование Node.js в качестве встроенной библиотеки отличается от написания кода, который выполняется Node.js, критические изменения не следуют типичной политике устаревания Node.js и могут происходить при каждом основном выпуске semver без предварительного уведомления.

Пример приложения для встраивания

В следующих разделах будет представлен обзор того, как использовать эти API для создания приложения с нуля, которое будет выполнять эквивалент node -e \<code\>, т.е. принимать фрагмент JavaScript и запускать его в среде, специфичной для Node.js.

Полный код можно найти в исходном дереве Node.js.

Настройка состояния для каждого процесса

Для запуска Node.js требуется некоторое управление состоянием для каждого процесса:

  • Разбор аргументов для параметров CLI Node.js,
  • Требования V8 для каждого процесса, такие как экземпляр v8::Platform.

В следующем примере показано, как их можно настроить. Некоторые имена классов взяты из пространств имен node и v8 C++ соответственно.

C++
int main(int argc, char** argv) {
  argv = uv_setup_args(argc, argv);
  std::vector<std::string> args(argv, argv + argc);
  // Разбор параметров CLI Node.js и вывод любых ошибок, возникших при
  // попытке их разбора.
  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()` - это способ
  // создать экземпляр v8::Platform, который Node.js может использовать при создании
  // рабочих потоков. Когда нет экземпляра `MultiIsolatePlatform`,
  // рабочие потоки отключаются.
  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;
}

Настройка состояния для каждого экземпляра

[История]

ВерсияИзменения
v15.0.0Добавлены утилиты CommonEnvironmentSetup и SpinEventLoop.

Node.js имеет концепцию "экземпляра Node.js", который обычно называют node::Environment. Каждый node::Environment связан со следующим:

  • Ровно один v8::Isolate, то есть один экземпляр движка JS,
  • Ровно один uv_loop_t, то есть один цикл событий,
  • Несколько v8::Contextов, но ровно один основной v8::Context, и
  • Один экземпляр node::IsolateData, который содержит информацию, которая может совместно использоваться несколькими node::Environmentами. Встраиватель должен убедиться, что node::IsolateData используется совместно только среди node::Environmentов, которые используют один и тот же v8::Isolate, Node.js не выполняет эту проверку.

Чтобы настроить v8::Isolate, необходимо предоставить v8::ArrayBuffer::Allocator. Одним из возможных вариантов является стандартный распределитель Node.js, который можно создать с помощью node::ArrayBufferAllocator::Create(). Использование распределителя Node.js позволяет добиться небольших оптимизаций производительности, когда дополнения используют C++ API Buffer Node.js, и требуется для отслеживания памяти ArrayBuffer в process.memoryUsage().

Кроме того, каждый v8::Isolate, который используется для экземпляра Node.js, должен быть зарегистрирован и отменен регистрацию в экземпляре MultiIsolatePlatform, если он используется, чтобы платформа знала, какой цикл событий использовать для задач, запланированных v8::Isolate.

Вспомогательная функция node::NewIsolate() создает v8::Isolate, настраивает его с помощью некоторых специфичных для Node.js перехватчиков (например, обработчика ошибок Node.js) и автоматически регистрирует его на платформе.

C++
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);
    // v8::Context должен быть введен, когда вызываются node::CreateEnvironment() и
    // node::LoadEnvironment().
    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;
}