Spec-Zone.ru › Node.js 16 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';

Сначала создайте файл 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

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

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

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

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

В примере hello.cc функция инициализации — Initialize, а имя модуля плагина — addon.

При сборке плагинов с node-gyp, использование макроса NODE_GYP_MODULE_NAME в качестве первого параметра 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. */
}

Другой вариант — использовать макрос 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.
    }
  • Выделите в куче экземпляр этого класса в функции инициализации плагина. Это можно сделать с помощью ключевого слова 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();
}
Поддержка рабочих процессов
История
Версия Изменения
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);

Эта функция добавляет обработчик, который будет выполняться перед завершением заданного экземпляра 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);
}

Тестирование в JavaScript путём выполнения:

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

Сборка

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

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

Версия утилиты 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'

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

Хотя пакет bindings реализует более сложный поиск модулей плагинов, он по сути использует шаблон try…catch похожий на:

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

Связывание с библиотеками, включёнными в 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 примеры для иллюстрации того, как это можно использовать.

END_OF_DOCUMENT_MARKER

Node-API

Устойчивость: 2 - Стабильно

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

Функции, доступные и как их использовать, описаны в C/C++ дополнениях с помощью Node-API.

Примеры дополнений

Ниже приведены некоторые примеры дополнений, предназначенные для помощи разработчикам в начальном этапе. Примеры используют API V8. Обратитесь к онлайн справке V8 за помощью с различными вызовами V8 и к Руководству по интеграции V8 для объяснения нескольких концепций, таких как обработчики, области видимости, шаблоны функций и т. д.

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

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

В случаях, когда существует более одного .cc файла, просто добавьте дополнительное имя файла в массив sources:

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

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

$ node-gyp configure build

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

Дополнения обычно экспонируют объекты и функции, к которым можно получить доступ из 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

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

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

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

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

В дополнениях обычно передают 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

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

Для проверки запустите следующий JavaScript:

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

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

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

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

Дополнения могут создавать и возвращать новые объекты внутри 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

Для проверки в 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'

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

Ещё один распространённый сценарий — создание 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

Для проверки:

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

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

Оборачивание 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

Затем в 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

В 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<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

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

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

Проверьте с помощью:

// 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

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

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

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

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

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

Сначала метод 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

В 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

Реализация в 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

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

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

Проверьте с помощью:

// 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

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

В дополнение к обёртыванию и возврату 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

В 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

Реализация 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

Проверьте с помощью:

// 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

© 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-v16.x/docs/api/addons.html

Spec-Zone.ru

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