Spec-Zone.ru › Node.js 10 LTS

C++ Плагины

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

В настоящее время способ реализации плагинов довольно сложный, требующий знаний нескольких компонентов и API:

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

  • libuv: Библиотека C, реализующая цикл событий Node.js, его потоки-рабочие, и все асинхронные функции платформы. Она также служит кроссплатформенной абстракционной библиотекой, предоставляя легкий, похожий на POSIX доступ на всех основных операционных системах к многим распространенным системным задачам, таким как взаимодействие с файловой системой, сокетами, таймерами и системными событиями. libuv также предоставляет абстракцию потоков, похожую на pthreads, которая может быть использована для создания более сложных асинхронных плагинов, которым нужно выйти за рамки стандартного цикла событий. Разработчикам плагинов рекомендуется подумать о том, как избежать блокировки цикла событий с помощью операций ввода-вывода или других задач с интенсивной обработкой, переложив работу с помощью 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::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

Обратите внимание, что все плагины 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 действительны только в одном контексте и, скорее всего, приведут к сбою при обращении к ним из неправильного контекста или из другого потока, чем тот, в котором они были созданы.

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

  • определить класс, который будет содержать данные для каждого экземпляра плагина. Такой класс должен содержать v8::Persistent<v8::Object>, который будет содержать слабую ссылку на объект exports плагина. Обработчик, связанный со слабой ссылкой, затем уничтожит экземпляр класса.
  • создать экземпляр этого класса в инициализаторе плагина таким образом, чтобы v8::Persistent<v8::Object> был установлен на объект exports.
  • сохранить экземпляр класса в v8::External, и
  • передать v8::External во все методы, экспортируемые в JavaScript, передав его конструктору v8::FunctionTemplate для создания нативных функций JavaScript. Третий параметр конструктора v8::FunctionTemplate принимает v8::External.

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

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

#include <node.h>

using namespace v8;

class AddonData {
 public:
  AddonData(Isolate* isolate, Local<Object> exports):
      call_count(0) {
    // Link the existence of this object instance to the existence of exports.
    exports_.Reset(isolate, exports);
    exports_.SetWeak(this, DeleteMe, WeakCallbackType::kParameter);
  }

  ~AddonData() {
    if (!exports_.IsEmpty()) {
      // Reset the reference to avoid leaking data.
      exports_.ClearWeak();
      exports_.Reset();
    }
  }

  // Per-addon data.
  int call_count;

 private:
  // Method to call when "exports" is about to be garbage-collected.
  static void DeleteMe(const WeakCallbackInfo<AddonData>& info) {
    delete info.GetParameter();
  }

  // Weak handle to the "exports" object. An instance of this class will be
  // destroyed along with the exports object to which it is weakly bound.
  v8::Persistent<v8::Object> exports_;
};

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.
  AddonData* data = new AddonData(isolate, exports);
  // 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", NewStringType::kNormal)
                  .ToLocalChecked(),
               FunctionTemplate::New(isolate, Method, external)
                  ->GetFunction(context).ToLocalChecked()).FromJust();
}

Компиляция

После написания исходного кода его необходимо скомпилировать в двоичный файл 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. См. инструкции по установке, включая платформенно-специфические требования.

После создания файла 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'

Дополнительную информацию см. в примерах ниже или на https://github.com/arturadib/node-qt (пример в производстве).

Так как точный путь к скомпилированному двоичному файлу плагина может варьироваться в зависимости от способа его компиляции (например, иногда он может быть в ./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» (Native Abstractions for Node.js) (или nan) предоставляют набор инструментов, которые разработчики плагинов рекомендуют использовать для поддержания совместимости между прошлыми и будущими выпусками V8 и Node.js. См. примеры nan примеров для иллюстрации того, как это можно использовать.

N-API

Стабильность: 2 - Стабильно

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

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

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

// hello.cc using N-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++ плагины — N-API.

Примеры плагинов

Ниже приведены некоторые примеры плагинов, предназначенные для помощи разработчикам в начале работы. Примеры используют API V8. Обратитесь к онлайн-справочнику V8 за помощью с различными вызовами V8 и к «Руководству разработчика» V8 (Embedder's Guide) для объяснения нескольких используемых концепций, таких как обработчики, области видимости, шаблоны функций и т. д.

Каждый из этих примеров использует следующий 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::NewStringType;
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",
                            NewStringType::kNormal).ToLocalChecked()));
    return;
  }

  // Check the argument types
  if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate,
                            "Wrong arguments",
                            NewStringType::kNormal).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::NewStringType;
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",
                          NewStringType::kNormal).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::NewStringType;
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",
                               NewStringType::kNormal).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::NewStringType;
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", NewStringType::kNormal).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", NewStringType::kNormal).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);
  static v8::Persistent<v8::Function> constructor;
  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::NewStringType;
using v8::Number;
using v8::Object;
using v8::Persistent;
using v8::String;
using v8::Value;

Persistent<Function> MyObject::constructor;

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

MyObject::~MyObject() {
}

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

  // Prepare constructor template
  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
  tpl->SetClassName(String::NewFromUtf8(
      isolate, "MyObject", NewStringType::kNormal).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());
  exports->Set(context, String::NewFromUtf8(
      isolate, "MyObject", NewStringType::kNormal).ToLocalChecked(),
               tpl->GetFunction(context).ToLocalChecked()).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 = Local<Function>::New(isolate, constructor);
    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 и никогда не должны использоваться за пределами тестирования.

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

В качестве альтернативы можно использовать паттерн фабрики, чтобы избежать явного создания экземпляров объектов с помощью оператора 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::Persistent<v8::Function> constructor;
  double value_;
};

}  // namespace demo

#endif

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

// myobject.cc
#include <node.h>
#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::NewStringType;
using v8::Number;
using v8::Object;
using v8::Persistent;
using v8::String;
using v8::Value;

Persistent<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", NewStringType::kNormal).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());
}

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::Persistent<v8::Function> constructor;
  double value_;
};

}  // namespace demo

#endif

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

// myobject.cc
#include <node.h>
#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::NewStringType;
using v8::Object;
using v8::Persistent;
using v8::String;
using v8::Value;

Persistent<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", NewStringType::kNormal).ToLocalChecked());
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

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

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

Обработчики AtExit

Обработчик AtExit — это функция, которая вызывается после завершения цикла событий Node.js, но перед завершением JavaScript-виртуальной машины и выключением Node.js. Обработчики AtExit регистрируются с помощью API node::AtExit.

void AtExit(callback, args)

  • callback <void (*)(void*)> Указатель на функцию, которую нужно вызвать при выходе.
  • args <void*> Указатель, передаваемый в обратный вызов при выходе.

Регистрирует обработчики выхода, которые выполняются после завершения цикла событий, но до убийства виртуальной машины.

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

Обратные вызовы выполняются в порядке LIFO.

Следующий addon.cc реализует AtExit:

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

namespace demo {

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

static char cookie[] = "yum yum";
static int at_exit_cb1_called = 0;
static int at_exit_cb2_called = 0;

static void at_exit_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());
  at_exit_cb1_called++;
}

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

static void sanity_check(void*) {
  assert(at_exit_cb1_called == 1);
  assert(at_exit_cb2_called == 2);
}

void init(Local<Object> exports) {
  AtExit(at_exit_cb2, cookie);
  AtExit(at_exit_cb2, cookie);
  AtExit(at_exit_cb1, exports->GetIsolate());
  AtExit(sanity_check);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, init)

}  // namespace demo

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

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

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

Spec-Zone.ru

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