Spec-Zone.ru › Node.js 24 LTS

Аддоны C++

Аддоны — это динамически подключаемые разделяемые объекты, написанные на C++. Функция require() может загружать аддоны как обычные модули Node.js. Аддоны предоставляют интерфейс между JavaScript и библиотеками C/C++.

Существует три варианта реализации аддонов:

  • Node-API
  • nan (Native Abstractions for Node.js)
  • прямое использование внутренних библиотек V8, libuv и Node.js

Если нет необходимости в прямом доступе к функциональности, которая не
предоставляется Node-API, используйте Node-API. Дополнительные сведения о Node-API см. в разделе Аддоны C/C++ с Node-API.

При реализации аддонов без Node-API процесс становится сложнее и требует
знания нескольких компонентов и API:

  • V8: библиотека C++, которую Node.js использует для реализации JavaScript. Она предоставляет механизмы создания объектов, вызова функций и т. д. API V8 в основном документирован в заголовочном файле v8.h (deps/v8/include/v8.h в исходном коде Node.js), а также доступен в Интернете.

  • libuv: библиотека C, реализующая цикл событий Node.js, его рабочие потоки и все асинхронные возможности платформы. Она также служит кроссплатформенной библиотекой абстракций, предоставляя простой POSIX-подобный доступ во всех основных операционных системах ко многим распространённым системным задачам, таким как работа с файловой системой, сокетами, таймерами и системными событиями. libuv также предоставляет абстракцию потоков, аналогичную потокам POSIX, для более сложных асинхронных аддонов, которым необходимо выйти за пределы стандартного цикла событий. Авторам аддонов следует избегать блокировки цикла событий операциями ввода-вывода и другими длительными задачами, передавая работу через libuv неблокирующим системным операциям, рабочим потокам или пользовательским потокам libuv.

  • Внутренние библиотеки Node.js: сам Node.js экспортирует API C++, которые могут использовать аддоны. Наиболее важным из них является класс node::ObjectWrap.

  • Другие статически скомпонованные библиотеки (включая OpenSSL): эти библиотеки расположены в каталоге deps/ в исходном коде Node.js. Только символы libuv, OpenSSL, V8 и zlib намеренно реэкспортируются Node.js и могут в той или иной степени использоваться аддонами. Дополнительные сведения см. в разделе Компоновка с библиотеками, входящими в состав Node.js.

Все приведённые ниже примеры доступны для загрузки и могут использоваться в качестве отправной точки для создания аддона.

Привет, мир

Этот пример «Привет, мир» — простой аддон на C++, эквивалентный следующему коду JavaScript:

module.exports.hello = () => 'world'; copy

Сначала создайте файл hello.cc:

// hello.cc
#include <node.h>

namespace demo {

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::NewStringType;
using v8::Object;
using v8::String;
using v8::Value;

void Method(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  args.GetReturnValue().Set(String::NewFromUtf8(
      isolate, "world", NewStringType::kNormal).ToLocalChecked());
}

void Initialize(Local<Object> exports) {
  NODE_SET_METHOD(exports, "hello", Method);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

}  // namespace demo copy

Все аддоны Node.js должны экспортировать функцию инициализации следующего вида:

void Initialize(Local<Object> exports);
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize) copy

После NODE_MODULE точка с запятой не ставится, поскольку это не функция (см. node.h).

Значение module_name должно совпадать с именем итогового двоичного файла (без суффикса .node).

Таким образом, в примере hello.cc функция инициализации называется Initialize, а имя модуля аддона — addon.

При сборке аддонов с помощью node-gyp использование макроса NODE_GYP_MODULE_NAME в качестве первого параметра NODE_MODULE() гарантирует, что имя итогового двоичного файла будет передано в NODE_MODULE().

Аддоны, объявленные с помощью NODE_MODULE(), нельзя одновременно загружать в нескольких контекстах или потоках.

Аддоны, учитывающие контекст

В некоторых средах аддоны Node.js может потребоваться загружать несколько раз в нескольких контекстах. Например, среда выполнения Electron запускает несколько экземпляров Node.js в одном процессе. У каждого экземпляра будет собственный кэш require(), поэтому каждому экземпляру потребуется нативный аддон, который корректно работает при загрузке через require(). Это означает, что аддон должен поддерживать многократную инициализацию.

Аддон, учитывающий контекст, можно создать с помощью макроса NODE_MODULE_INITIALIZER, который раскрывается в имя функции, которую Node.js будет искать при загрузке аддона. Таким образом, аддон можно инициализировать, как показано в следующем примере:

using namespace v8;

extern "C" NODE_MODULE_EXPORT void
NODE_MODULE_INITIALIZER(Local<Object> exports,
                        Local<Value> module,
                        Local<Context> context) {
  /* Perform addon initialization steps here. */
} copy

Другой вариант — использовать макрос NODE_MODULE_INIT(), который также создаёт аддон, учитывающий контекст. В отличие от NODE_MODULE(), который используется для создания аддона на основе заданной функции инициализации, NODE_MODULE_INIT() служит объявлением такой функции инициализации, за которым следует её тело.

В теле функции после вызова NODE_MODULE_INIT() можно использовать следующие три переменные:

  • Local<Object> exports,
  • Local<Value> module и
  • Local<Context> context

Создание аддона, учитывающего контекст, требует тщательного управления глобальными статическими данными для обеспечения стабильности и корректной работы. Поскольку аддон может загружаться несколько раз, возможно даже из разных потоков, любые глобальные статические данные, хранящиеся в аддоне, необходимо надлежащим образом защищать, и они не должны содержать постоянных ссылок на объекты JavaScript. Это связано с тем, что объекты JavaScript действительны только в одном контексте, и обращение к ним из неправильного контекста или из потока, отличного от потока, в котором они были созданы, скорее всего, приведёт к сбою.

Чтобы избежать глобальных статических данных, аддон, учитывающий контекст, можно структурировать следующим образом:

  • Определите класс, который будет хранить данные для каждого экземпляра аддона и иметь статический член вида
    static void DeleteInstance(void* data) {
      // Cast `data` to an instance of the class and delete it.
    } copy
  • Выделите память в куче для экземпляра этого класса в инициализаторе аддона. Для этого можно использовать ключевое слово new.
  • Вызовите node::AddEnvironmentCleanupHook(), передав ему созданный выше экземпляр и указатель на DeleteInstance(). Это гарантирует удаление экземпляра при завершении работы среды.
  • Сохраните экземпляр класса в v8::External, а затем
  • Передайте v8::External всем методам, доступным из JavaScript: передайте его в v8::FunctionTemplate::New() или v8::Function::New(), которые создают функции JavaScript с нативной реализацией. Третий параметр v8::FunctionTemplate::New() или v8::Function::New() принимает v8::External и предоставляет его в нативном обратном вызове с помощью метода v8::FunctionCallbackInfo::Data().

Это гарантирует, что данные конкретного экземпляра аддона будут доступны каждой привязке, которую можно вызвать из JavaScript. Данные конкретного экземпляра аддона также необходимо передавать во все асинхронные обратные вызовы, которые может создавать аддон.

Следующий пример демонстрирует реализацию аддона, учитывающего контекст:

#include <node.h>

using namespace v8;

class AddonData {
 public:
  explicit AddonData(Isolate* isolate):
      call_count(0) {
    // Ensure this per-addon-instance data is deleted at environment cleanup.
    node::AddEnvironmentCleanupHook(isolate, DeleteInstance, this);
  }

  // Per-addon data.
  int call_count;

  static void DeleteInstance(void* data) {
    delete static_cast<AddonData*>(data);
  }
};

static void Method(const v8::FunctionCallbackInfo<v8::Value>& info) {
  // Retrieve the per-addon-instance data.
  AddonData* data =
      reinterpret_cast<AddonData*>(info.Data().As<External>()->Value());
  data->call_count++;
  info.GetReturnValue().Set((double)data->call_count);
}

// Initialize this addon to be context-aware.
NODE_MODULE_INIT(/* exports, module, context */) {
  Isolate* isolate = context->GetIsolate();

  // Create a new instance of `AddonData` for this instance of the addon and
  // tie its life cycle to that of the Node.js environment.
  AddonData* data = new AddonData(isolate);

  // Wrap the data in a `v8::External` so we can pass it to the method we
  // expose.
  Local<External> external = External::New(isolate, data);

  // Expose the method `Method` to JavaScript, and make sure it receives the
  // per-addon-instance data we created above by passing `external` as the
  // third parameter to the `FunctionTemplate` constructor.
  exports->Set(context,
               String::NewFromUtf8(isolate, "method").ToLocalChecked(),
               FunctionTemplate::New(isolate, Method, external)
                  ->GetFunction(context).ToLocalChecked()).FromJust();
} copy
Поддержка Worker
История
Версия Изменения
v14.8.0, v12.19.0

Теперь хуки очистки могут быть асинхронными.

Чтобы загружаться в нескольких средах Node.js, например в основном потоке и потоке Worker, аддон должен:

  • быть аддоном Node-API или
  • быть объявлен как учитывающий контекст с помощью NODE_MODULE_INIT(), как описано выше

Для поддержки потоков Worker аддоны должны освобождать все выделенные ими ресурсы при завершении такого потока. Это можно сделать с помощью функции AddEnvironmentCleanupHook():

void AddEnvironmentCleanupHook(v8::Isolate* isolate,
                               void (*fun)(void* arg),
                               void* arg); copy

Эта функция добавляет хук, который будет выполнен перед завершением работы данного экземпляра Node.js. При необходимости такие хуки можно удалить до их выполнения с помощью RemoveEnvironmentCleanupHook(), имеющего ту же сигнатуру. Обратные вызовы выполняются в порядке «последним пришёл — первым вышел».

При необходимости доступны ещё две перегрузки — AddEnvironmentCleanupHook() и RemoveEnvironmentCleanupHook(), в которых хук очистки принимает функцию обратного вызова. Это можно использовать для завершения работы асинхронных ресурсов, например дескрипторов libuv, зарегистрированных аддоном.

Следующий addon.cc использует AddEnvironmentCleanupHook:

// addon.cc
#include <node.h>
#include <assert.h>
#include <stdlib.h>

using node::AddEnvironmentCleanupHook;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::Object;

// Note: In a real-world application, do not rely on static/global data.
static char cookie[] = "yum yum";
static int cleanup_cb1_called = 0;
static int cleanup_cb2_called = 0;

static void cleanup_cb1(void* arg) {
  Isolate* isolate = static_cast<Isolate*>(arg);
  HandleScope scope(isolate);
  Local<Object> obj = Object::New(isolate);
  assert(!obj.IsEmpty());  // assert VM is still alive
  assert(obj->IsObject());
  cleanup_cb1_called++;
}

static void cleanup_cb2(void* arg) {
  assert(arg == static_cast<void*>(cookie));
  cleanup_cb2_called++;
}

static void sanity_check(void*) {
  assert(cleanup_cb1_called == 1);
  assert(cleanup_cb2_called == 1);
}

// Initialize this addon to be context-aware.
NODE_MODULE_INIT(/* exports, module, context */) {
  Isolate* isolate = context->GetIsolate();

  AddEnvironmentCleanupHook(isolate, sanity_check, nullptr);
  AddEnvironmentCleanupHook(isolate, cleanup_cb2, cookie);
  AddEnvironmentCleanupHook(isolate, cleanup_cb1, isolate);
} copy

Проверьте работу в JavaScript, выполнив:

// test.js
require('./build/Release/addon'); copy

Сборка

После написания исходного кода его необходимо скомпилировать в двоичный файл addon.node. Для этого создайте в корне проекта файл binding.gyp, описывающий конфигурацию сборки модуля в формате, похожем на JSON. Этот файл используется node-gyp — инструментом, специально разработанным для компиляции аддонов Node.js.

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [ "hello.cc" ]
    }
  ]
} copy

Версия утилиты node-gyp поставляется вместе с Node.js в составе npm. Эта версия недоступна разработчикам для непосредственного использования и предназначена только для поддержки команды npm install, позволяющей компилировать и устанавливать аддоны. Разработчики, желающие непосредственно использовать node-gyp, могут установить её с помощью команды npm install -g node-gyp. Дополнительные сведения, включая требования для отдельных платформ, см. в node-gyp инструкциях по установке.

После создания файла binding.gyp используйте node-gyp configure, чтобы сгенерировать файлы проекта для сборки на текущей платформе. В каталоге build/ будет создан файл Makefile (на платформах Unix) или vcxproj (в Windows).

Затем выполните команду node-gyp build, чтобы создать скомпилированный файл addon.node. Он будет помещён в каталог build/Release/.

При установке аддона Node.js с помощью npm install npm использует собственную встроенную версию node-gyp для выполнения тех же действий, создавая по запросу скомпилированную версию аддона для платформы пользователя.

После сборки двоичный аддон можно использовать в Node.js, указав require() путь к собранному модулю addon.node:

// hello.js
const addon = require('./build/Release/addon');

console.log(addon.hello());
// Prints: 'world' copy

Поскольку точный путь к скомпилированному двоичному файлу аддона может меняться в зависимости от способа компиляции (например, иногда он может находиться в ./build/Debug/), для загрузки скомпилированного модуля аддоны могут использовать пакет bindings.

Реализация пакета bindings сложнее и лучше определяет расположение модулей аддонов, но по сути использует шаблон try…catch, похожий на следующий:

try {
  return require('./build/Release/addon.node');
} catch (err) {
  return require('./build/Debug/addon.node');
} copy

Компоновка с библиотеками, входящими в состав Node.js

Node.js использует статически скомпонованные библиотеки, такие как V8, libuv и OpenSSL. Все аддоны должны компоновываться с V8 и могут также компоновываться с любыми другими зависимостями. Обычно достаточно добавить соответствующие инструкции #include <...> (например, #include <v8.h>), после чего node-gyp автоматически найдёт нужные заголовочные файлы. Однако следует учитывать несколько нюансов:

  • При запуске node-gyp определит конкретную версию выпуска Node.js и загрузит либо полный архив исходного кода, либо только заголовочные файлы. Если загружен полный исходный код, аддоны получат полный доступ ко всем зависимостям Node.js. Если же загружены только заголовочные файлы Node.js, будут доступны лишь символы, экспортируемые Node.js.

  • node-gyp можно запустить с флагом --nodedir, указав локальный каталог с исходным кодом Node.js. При использовании этого параметра аддон получит доступ ко всем зависимостям.

Загрузка аддонов с помощью require()

Расширение имени скомпилированного двоичного файла аддона — .node (в отличие от .dll или .so). Функция require() ищет файлы с расширением .node и инициализирует их как динамически подключаемые библиотеки.

При вызове require() расширение .node обычно можно опустить — Node.js всё равно найдёт и инициализирует аддон. Однако следует учитывать, что сначала Node.js попытается найти и загрузить модули или файлы JavaScript с тем же базовым именем. Например, если в том же каталоге, что и двоичный файл addon.node, находится файл addon.js, то require('addon') отдаст предпочтение файлу addon.js и загрузит его.

Нативные абстракции для Node.js

В каждом из примеров этого документа для реализации аддонов непосредственно используются API Node.js и V8. API V8 может значительно меняться от одного выпуска V8 к следующему (и от одного основного выпуска Node.js к следующему). При каждом таком изменении может потребоваться обновить и перекомпилировать аддоны, чтобы они продолжали работать. График выпусков Node.js призван свести к минимуму частоту и последствия таких изменений, однако Node.js мало что может сделать для обеспечения стабильности API V8.

Native Abstractions for Node.js (или nan) предоставляет набор инструментов, рекомендуемых разработчикам аддонов для обеспечения совместимости с предыдущими и будущими выпусками V8 и Node.js. Примеры использования приведены в nan примерах.

Node-API

Стабильность: 2 — Стабильный

Node-API — это API для создания нативных аддонов. Он не зависит от используемой среды выполнения JavaScript (например, V8) и поддерживается в составе самого Node.js. Этот API обеспечивает стабильность двоичного интерфейса приложений (ABI) в разных версиях Node.js. Он предназначен для защиты аддонов от изменений базового движка JavaScript и позволяет запускать модули, скомпилированные для одной версии, в более поздних версиях Node.js без повторной компиляции. Аддоны собираются и упаковываются теми же способами и инструментами, которые описаны в этом документе (node-gyp и т. д.). Отличается только набор API, используемых нативным кодом. Вместо API V8 или Native Abstractions for Node.js используются функции Node-API.

Создание и поддержка аддона, использующего преимущества стабильности ABI, обеспечиваемой Node-API, связаны с определёнными особенностями реализации.

Чтобы использовать Node-API в приведённом выше примере «Привет, мир», замените содержимое hello.cc следующим кодом. Все остальные инструкции остаются без изменений.

// hello.cc using Node-API
#include <node_api.h>

namespace demo {

napi_value Method(napi_env env, napi_callback_info args) {
  napi_value greeting;
  napi_status status;

  status = napi_create_string_utf8(env, "world", NAPI_AUTO_LENGTH, &greeting);
  if (status != napi_ok) return nullptr;
  return greeting;
}

napi_value init(napi_env env, napi_value exports) {
  napi_status status;
  napi_value fn;

  status = napi_create_function(env, nullptr, 0, Method, nullptr, &fn);
  if (status != napi_ok) return nullptr;

  status = napi_set_named_property(env, exports, "hello", fn);
  if (status != napi_ok) return nullptr;
  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, init)

}  // namespace demo copy

Описание доступных функций и способов их использования приведено в разделе Аддоны C/C++ с Node-API.

Примеры аддонов

Ниже приведены примеры аддонов, призванные помочь разработчикам начать работу. В примерах используются API V8. Сведения о различных вызовах V8 см. в онлайн-справочнике по V8, а объяснения таких понятий, как дескрипторы, области видимости, шаблоны функций и т. д., — в руководстве для встраивающих V8.

В каждом из этих примеров используется следующий файл binding.gyp:

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [ "addon.cc" ]
    }
  ]
} copy

Если файлов .cc несколько, просто добавьте дополнительные имена файлов в массив sources:

"sources": ["addon.cc", "myexample.cc"] copy

После подготовки файла binding.gyp примеры аддонов можно настроить и собрать с помощью node-gyp:

node-gyp configure build copy

Аргументы функции

Аддоны обычно предоставляют объекты и функции, к которым можно обращаться из JavaScript, выполняемого в Node.js. При вызове функций из JavaScript входные аргументы и возвращаемое значение необходимо преобразовывать между кодом JavaScript и кодом C/C++.

Следующий пример показывает, как прочитать аргументы функции, переданные из JavaScript, и вернуть результат:

// addon.cc
#include <node.h>

namespace demo {

using v8::Exception;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;

// This is the implementation of the "add" method
// Input arguments are passed using the
// const FunctionCallbackInfo<Value>& args struct
void Add(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  // Check the number of arguments passed.
  if (args.Length() < 2) {
    // Throw an Error that is passed back to JavaScript
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate,
                            "Wrong number of arguments").ToLocalChecked()));
    return;
  }

  // Check the argument types
  if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate,
                            "Wrong arguments").ToLocalChecked()));
    return;
  }

  // Perform the operation
  double value =
      args[0].As<Number>()->Value() + args[1].As<Number>()->Value();
  Local<Number> num = Number::New(isolate, value);

  // Set the return value (using the passed in
  // FunctionCallbackInfo<Value>&)
  args.GetReturnValue().Set(num);
}

void Init(Local<Object> exports) {
  NODE_SET_METHOD(exports, "add", Add);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo copy

После компиляции пример аддона можно подключить и использовать в Node.js:

// test.js
const addon = require('./build/Release/addon');

console.log('This should be eight:', addon.add(3, 5)); copy

Обратные вызовы

В аддонах часто передают функции JavaScript в функцию C++ и вызывают их оттуда. В следующем примере показано, как вызывать такие обратные вызовы:

// addon.cc
#include <node.h>

namespace demo {

using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Null;
using v8::Object;
using v8::String;
using v8::Value;

void RunCallback(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();
  Local<Function> cb = Local<Function>::Cast(args[0]);
  const unsigned argc = 1;
  Local<Value> argv[argc] = {
      String::NewFromUtf8(isolate,
                          "hello world").ToLocalChecked() };
  cb->Call(context, Null(isolate), argc, argv).ToLocalChecked();
}

void Init(Local<Object> exports, Local<Object> module) {
  NODE_SET_METHOD(module, "exports", RunCallback);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo copy

В этом примере используется двухаргументная форма Init(), получающая в качестве второго аргумента полный объект module. Это позволяет аддону полностью переопределить exports одной функцией, а не добавлять функцию в качестве свойства exports.

Чтобы проверить пример, выполните следующий код JavaScript:

// test.js
const addon = require('./build/Release/addon');

addon((msg) => {
  console.log(msg);
// Prints: 'hello world'
}); copy

В этом примере функция обратного вызова вызывается синхронно.

Фабрика объектов

Аддоны могут создавать и возвращать новые объекты из функции C++, как показано в следующем примере. Создаётся объект со свойством msg, которое возвращает строку, переданную в createObject():

// addon.cc
#include <node.h>

namespace demo {

using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

void CreateObject(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  Local<Object> obj = Object::New(isolate);
  obj->Set(context,
           String::NewFromUtf8(isolate,
                               "msg").ToLocalChecked(),
                               args[0]->ToString(context).ToLocalChecked())
           .FromJust();

  args.GetReturnValue().Set(obj);
}

void Init(Local<Object> exports, Local<Object> module) {
  NODE_SET_METHOD(module, "exports", CreateObject);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo copy

Проверка в JavaScript:

// test.js
const addon = require('./build/Release/addon');

const obj1 = addon('hello');
const obj2 = addon('world');
console.log(obj1.msg, obj2.msg);
// Prints: 'hello world' copy

Фабрика функций

Другой распространённый сценарий — создание функций JavaScript, обёртывающих функции C++, и их возврат в JavaScript:

// addon.cc
#include <node.h>

namespace demo {

using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

void MyFunction(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  args.GetReturnValue().Set(String::NewFromUtf8(
      isolate, "hello world").ToLocalChecked());
}

void CreateFunction(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  Local<Context> context = isolate->GetCurrentContext();
  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, MyFunction);
  Local<Function> fn = tpl->GetFunction(context).ToLocalChecked();

  // omit this to make it anonymous
  fn->SetName(String::NewFromUtf8(
      isolate, "theFunction").ToLocalChecked());

  args.GetReturnValue().Set(fn);
}

void Init(Local<Object> exports, Local<Object> module) {
  NODE_SET_METHOD(module, "exports", CreateFunction);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo copy

Проверка:

// test.js
const addon = require('./build/Release/addon');

const fn = addon();
console.log(fn());
// Prints: 'hello world' copy

Обёртывание объектов C++

Также можно обернуть объекты/классы C++ таким образом, чтобы создавать их экземпляры с помощью оператора JavaScript new:

// addon.cc
#include <node.h>
#include "myobject.h"

namespace demo {

using v8::Local;
using v8::Object;

void InitAll(Local<Object> exports) {
  MyObject::Init(exports);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)

}  // namespace demo copy

Затем в myobject.h класс-обёртка наследуется от node::ObjectWrap:

// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <node.h>
#include <node_object_wrap.h>

namespace demo {

class MyObject : public node::ObjectWrap {
 public:
  static void Init(v8::Local<v8::Object> exports);

 private:
  explicit MyObject(double value = 0);
  ~MyObject();

  static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void PlusOne(const v8::FunctionCallbackInfo<v8::Value>& args);

  double value_;
};

}  // namespace demo

#endif copy

В myobject.cc реализуйте методы, которые должны быть доступны извне. В следующем коде метод plusOne() становится доступен благодаря добавлению в прототип конструктора:

// myobject.cc
#include "myobject.h"

namespace demo {

using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::ObjectTemplate;
using v8::String;
using v8::Value;

MyObject::MyObject(double value) : value_(value) {
}

MyObject::~MyObject() {
}

void MyObject::Init(Local<Object> exports) {
  Isolate* isolate = exports->GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  Local<ObjectTemplate> addon_data_tpl = ObjectTemplate::New(isolate);
  addon_data_tpl->SetInternalFieldCount(1);  // 1 field for the MyObject::New()
  Local<Object> addon_data =
      addon_data_tpl->NewInstance(context).ToLocalChecked();

  // Prepare constructor template
  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New, addon_data);
  tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

  // Prototype
  NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);

  Local<Function> constructor = tpl->GetFunction(context).ToLocalChecked();
  addon_data->SetInternalField(0, constructor);
  exports->Set(context, String::NewFromUtf8(
      isolate, "MyObject").ToLocalChecked(),
      constructor).FromJust();
}

void MyObject::New(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  if (args.IsConstructCall()) {
    // Invoked as constructor: `new MyObject(...)`
    double value = args[0]->IsUndefined() ?
        0 : args[0]->NumberValue(context).FromMaybe(0);
    MyObject* obj = new MyObject(value);
    obj->Wrap(args.This());
    args.GetReturnValue().Set(args.This());
  } else {
    // Invoked as plain function `MyObject(...)`, turn into construct call.
    const int argc = 1;
    Local<Value> argv[argc] = { args[0] };
    Local<Function> cons =
        args.Data().As<Object>()->GetInternalField(0)
            .As<Value>().As<Function>();
    Local<Object> result =
        cons->NewInstance(context, argc, argv).ToLocalChecked();
    args.GetReturnValue().Set(result);
  }
}

void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.This());
  obj->value_ += 1;

  args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}

}  // namespace demo copy

Чтобы собрать этот пример, файл myobject.cc необходимо добавить в binding.gyp:

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [
        "addon.cc",
        "myobject.cc"
      ]
    }
  ]
} copy

Проверка:

// test.js
const addon = require('./build/Release/addon');

const obj = new addon.MyObject(10);
console.log(obj.plusOne());
// Prints: 11
console.log(obj.plusOne());
// Prints: 12
console.log(obj.plusOne());
// Prints: 13 copy

Деструктор объекта-обёртки будет вызван при сборке мусора для этого объекта. Для проверки деструктора можно использовать флаги командной строки, позволяющие принудительно запускать сборку мусора. Эти флаги предоставляет базовый движок JavaScript V8. Они могут быть изменены или удалены в любой момент. Они не документированы ни в Node.js, ни в V8, и их никогда не следует использовать вне тестирования.

При завершении процесса или потоков Worker движок JS не вызывает деструкторы. Поэтому пользователь несёт ответственность за отслеживание этих объектов и обеспечение их корректного уничтожения во избежание утечек ресурсов.

Фабрика обёрнутых объектов

Кроме того, можно использовать шаблон «фабрика», чтобы не создавать экземпляры объектов явно с помощью оператора JavaScript new:

const obj = addon.createObject();
// instead of:
// const obj = new addon.Object(); copy

Сначала в addon.cc реализуется метод createObject():

// addon.cc
#include <node.h>
#include "myobject.h"

namespace demo {

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

void CreateObject(const FunctionCallbackInfo<Value>& args) {
  MyObject::NewInstance(args);
}

void InitAll(Local<Object> exports, Local<Object> module) {
  MyObject::Init(exports->GetIsolate());

  NODE_SET_METHOD(module, "exports", CreateObject);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)

}  // namespace demo copy

В myobject.h добавляется статический метод NewInstance() для создания экземпляра объекта. Этот метод заменяет использование new в JavaScript:

// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <node.h>
#include <node_object_wrap.h>

namespace demo {

class MyObject : public node::ObjectWrap {
 public:
  static void Init(v8::Isolate* isolate);
  static void NewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);

 private:
  explicit MyObject(double value = 0);
  ~MyObject();

  static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
  static void PlusOne(const v8::FunctionCallbackInfo<v8::Value>& args);
  static v8::Global<v8::Function> constructor;
  double value_;
};

}  // namespace demo

#endif copy

Реализация в myobject.cc аналогична предыдущему примеру:

// myobject.cc
#include <node.h>
#include "myobject.h"

namespace demo {

using node::AddEnvironmentCleanupHook;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;

// Warning! This is not thread-safe, this addon cannot be used for worker
// threads.
Global<Function> MyObject::constructor;

MyObject::MyObject(double value) : value_(value) {
}

MyObject::~MyObject() {
}

void MyObject::Init(Isolate* isolate) {
  // Prepare constructor template
  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
  tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

  // Prototype
  NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);

  Local<Context> context = isolate->GetCurrentContext();
  constructor.Reset(isolate, tpl->GetFunction(context).ToLocalChecked());

  AddEnvironmentCleanupHook(isolate, [](void*) {
    constructor.Reset();
  }, nullptr);
}

void MyObject::New(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  if (args.IsConstructCall()) {
    // Invoked as constructor: `new MyObject(...)`
    double value = args[0]->IsUndefined() ?
        0 : args[0]->NumberValue(context).FromMaybe(0);
    MyObject* obj = new MyObject(value);
    obj->Wrap(args.This());
    args.GetReturnValue().Set(args.This());
  } else {
    // Invoked as plain function `MyObject(...)`, turn into construct call.
    const int argc = 1;
    Local<Value> argv[argc] = { args[0] };
    Local<Function> cons = Local<Function>::New(isolate, constructor);
    Local<Object> instance =
        cons->NewInstance(context, argc, argv).ToLocalChecked();
    args.GetReturnValue().Set(instance);
  }
}

void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  const unsigned argc = 1;
  Local<Value> argv[argc] = { args[0] };
  Local<Function> cons = Local<Function>::New(isolate, constructor);
  Local<Context> context = isolate->GetCurrentContext();
  Local<Object> instance =
      cons->NewInstance(context, argc, argv).ToLocalChecked();

  args.GetReturnValue().Set(instance);
}

void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.This());
  obj->value_ += 1;

  args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}

}  // namespace demo copy

И снова для сборки этого примера файл myobject.cc необходимо добавить в binding.gyp:

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [
        "addon.cc",
        "myobject.cc"
      ]
    }
  ]
} copy

Проверка:

// test.js
const createObject = require('./build/Release/addon');

const obj = createObject(10);
console.log(obj.plusOne());
// Prints: 11
console.log(obj.plusOne());
// Prints: 12
console.log(obj.plusOne());
// Prints: 13

const obj2 = createObject(20);
console.log(obj2.plusOne());
// Prints: 21
console.log(obj2.plusOne());
// Prints: 22
console.log(obj2.plusOne());
// Prints: 23 copy

Передача обёрнутых объектов

Помимо обёртывания и возврата объектов C++, можно передавать обёрнутые объекты, извлекая их с помощью вспомогательной функции Node.js node::ObjectWrap::Unwrap. В следующем примере показана функция add(), принимающая два объекта MyObject в качестве входных аргументов:

// addon.cc
#include <node.h>
#include <node_object_wrap.h>
#include "myobject.h"

namespace demo {

using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;

void CreateObject(const FunctionCallbackInfo<Value>& args) {
  MyObject::NewInstance(args);
}

void Add(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  MyObject* obj1 = node::ObjectWrap::Unwrap<MyObject>(
      args[0]->ToObject(context).ToLocalChecked());
  MyObject* obj2 = node::ObjectWrap::Unwrap<MyObject>(
      args[1]->ToObject(context).ToLocalChecked());

  double sum = obj1->value() + obj2->value();
  args.GetReturnValue().Set(Number::New(isolate, sum));
}

void InitAll(Local<Object> exports) {
  MyObject::Init(exports->GetIsolate());

  NODE_SET_METHOD(exports, "createObject", CreateObject);
  NODE_SET_METHOD(exports, "add", Add);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)

}  // namespace demo copy

В myobject.h добавляется новый открытый метод, позволяющий получить доступ к закрытым значениям после извлечения объекта.

// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <node.h>
#include <node_object_wrap.h>

namespace demo {

class MyObject : public node::ObjectWrap {
 public:
  static void Init(v8::Isolate* isolate);
  static void NewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);
  inline double value() const { return value_; }

 private:
  explicit MyObject(double value = 0);
  ~MyObject();

  static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
  static v8::Global<v8::Function> constructor;
  double value_;
};

}  // namespace demo

#endif copy

Реализация myobject.cc остаётся аналогичной предыдущей версии:

// myobject.cc
#include <node.h>
#include "myobject.h"

namespace demo {

using node::AddEnvironmentCleanupHook;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

// Warning! This is not thread-safe, this addon cannot be used for worker
// threads.
Global<Function> MyObject::constructor;

MyObject::MyObject(double value) : value_(value) {
}

MyObject::~MyObject() {
}

void MyObject::Init(Isolate* isolate) {
  // Prepare constructor template
  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
  tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

  Local<Context> context = isolate->GetCurrentContext();
  constructor.Reset(isolate, tpl->GetFunction(context).ToLocalChecked());

  AddEnvironmentCleanupHook(isolate, [](void*) {
    constructor.Reset();
  }, nullptr);
}

void MyObject::New(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  if (args.IsConstructCall()) {
    // Invoked as constructor: `new MyObject(...)`
    double value = args[0]->IsUndefined() ?
        0 : args[0]->NumberValue(context).FromMaybe(0);
    MyObject* obj = new MyObject(value);
    obj->Wrap(args.This());
    args.GetReturnValue().Set(args.This());
  } else {
    // Invoked as plain function `MyObject(...)`, turn into construct call.
    const int argc = 1;
    Local<Value> argv[argc] = { args[0] };
    Local<Function> cons = Local<Function>::New(isolate, constructor);
    Local<Object> instance =
        cons->NewInstance(context, argc, argv).ToLocalChecked();
    args.GetReturnValue().Set(instance);
  }
}

void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  const unsigned argc = 1;
  Local<Value> argv[argc] = { args[0] };
  Local<Function> cons = Local<Function>::New(isolate, constructor);
  Local<Context> context = isolate->GetCurrentContext();
  Local<Object> instance =
      cons->NewInstance(context, argc, argv).ToLocalChecked();

  args.GetReturnValue().Set(instance);
}

}  // namespace demo copy

Проверка:

// test.js
const addon = require('./build/Release/addon');

const obj1 = addon.createObject(10);
const obj2 = addon.createObject(20);
const result = addon.add(obj1, obj2);

console.log(result);
// Prints: 30 copy

© Joyent, Inc. and other Node contributors
Licensed under the MIT License.
Node.js is a trademark of Joyent, Inc. and is used with its permission.
We are not endorsed by or affiliated with Joyent.
https://nodejs.org/dist/latest-v24.x/docs/api/addons.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API