Spec-Zone.ru › Node.js 20 LTS

C++ плагины

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

Существует три варианта реализации плагинов: Node-API, nan или прямое использование внутренних библиотек V8, libuv и Node.js. Если нет необходимости в прямом доступе к функциональности, которая не экспонируется Node-API, используйте Node-API. Для получения дополнительной информации о Node-API обратитесь к C/C++ плагины с Node-API.

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

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

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

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

  • Node.js включает другие статически связанные библиотеки, включая 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::Object;
using v8::String;
using v8::Value;

void Method(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  args.GetReturnValue().Set(String::NewFromUtf8(
      isolate, "world").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(), которые создают функции, поддерживаемые нативным кодом. Третий параметр 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
Поддержка рабочих процессов
История
Версия Изменения
v14.8.0, v12.19.0

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

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

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

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

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

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

При необходимости существуют дополнительные перегрузки 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 для генерации соответствующих файлов проекта для текущей платформы. Это сгенерирует файл Makefile (на платформах Unix) или vcxproj (на Windows) в каталоге build/.

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

При использовании npm install для установки плагина Node.js, 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.

Родные абстракции для 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 world», замените содержимое 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

Функции, доступные в Node-API и как их использовать, описаны в 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 входные аргументы и возвращаемое значение должны быть отображены в 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.Holder());
  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 и никогда не должны использоваться за пределами тестирования.

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

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

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

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

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

// 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.Holder());
  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-v20.x/docs/api/addons.html

Spec-Zone.ru

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