Skip to content

C++ аддоны

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

Есть три варианта реализации аддонов:

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

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

  • V8: библиотека C++, которую Node.js использует для предоставления реализации JavaScript. Она предоставляет механизмы для создания объектов, вызова функций и т. д. API V8 документирован в основном в заголовочном файле v8.h (deps/v8/include/v8.h в дереве исходного кода Node.js), а также доступен онлайн.
  • libuv: Библиотека C, которая реализует цикл событий Node.js, его рабочие потоки и все асинхронные поведения платформы. Она также служит кроссплатформенной библиотекой абстракций, предоставляя легкий, POSIX-подобный доступ ко всем основным операционным системам ко многим общим системным задачам, таким как взаимодействие с файловой системой, сокетами, таймерами и системными событиями. libuv также предоставляет абстракцию потоков, аналогичную потокам POSIX, для более сложных асинхронных аддонов, которым необходимо выйти за рамки стандартного цикла событий. Авторам аддонов следует избегать блокировки цикла событий операциями ввода-вывода или другими трудоемкими задачами, перенося работу через libuv на неблокирующие системные операции, рабочие потоки или настраиваемое использование потоков libuv.
  • Внутренние библиотеки Node.js: сам Node.js экспортирует API C++, которые могут использовать аддоны, наиболее важным из которых является класс node::ObjectWrap.
  • Другие статически связанные библиотеки (включая OpenSSL): Эти другие библиотеки расположены в каталоге deps/ в дереве исходного кода Node.js. Только символы libuv, OpenSSL, V8 и zlib целенаправленно реэкспортируются Node.js и могут в разной степени использоваться аддонами. См. Связывание с библиотеками, включенными в Node.js для получения дополнительной информации.

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

Привет, мир

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

js
module.exports.hello = () => 'world'

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

C++
// 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 должны экспортировать функцию инициализации, следующую шаблону:

C++
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_MODULE(), нельзя загружать в нескольких контекстах или нескольких потоках одновременно.

Аддоны с учетом контекста

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

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

C++
using namespace v8;

extern "C" NODE_MODULE_EXPORT void
NODE_MODULE_INITIALIZER(Local<Object> exports,
                        Local<Value> module,
                        Local<Context> context) {
  /* Выполните здесь шаги инициализации аддона. */
}

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

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

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

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

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

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

В следующем примере показана реализация аддона с учетом контекста:

C++
#include <node.h>

using namespace v8;

class AddonData {
 public:
  explicit AddonData(Isolate* isolate):
      call_count(0) {
    // Убедитесь, что эти данные для каждого экземпляра аддона удаляются при очистке среды.
    node::AddEnvironmentCleanupHook(isolate, DeleteInstance, this);
  }

  // Данные для каждого аддона.
  int call_count;

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

static void Method(const v8::FunctionCallbackInfo<v8::Value>& info) {
  // Извлеките данные для каждого экземпляра аддона.
  AddonData* data =
      reinterpret_cast<AddonData*>(info.Data().As<External>()->Value());
  data->call_count++;
  info.GetReturnValue().Set((double)data->call_count);
}

// Инициализируйте этот аддон как аддон с учетом контекста.
NODE_MODULE_INIT(/* exports, module, context */) {
  Isolate* isolate = context->GetIsolate();

  // Создайте новый экземпляр `AddonData` для этого экземпляра аддона и
  // привяжите его жизненный цикл к жизненному циклу среды Node.js.
  AddonData* data = new AddonData(isolate);

  // Оберните данные в `v8::External`, чтобы мы могли передать их методу, который мы
  // предоставляем.
  Local<External> external = External::New(isolate, data);

  // Предоставьте метод `Method` для JavaScript и убедитесь, что он получает
  // данные для каждого экземпляра аддона, который мы создали выше, передав `external` в качестве
  // третьего параметра конструктору `FunctionTemplate`.
  exports->Set(context,
               String::NewFromUtf8(isolate, "method").ToLocalChecked(),
               FunctionTemplate::New(isolate, Method, external)
                  ->GetFunction(context).ToLocalChecked()).FromJust();
}

Поддержка Worker

[История]

ВерсияИзменения
v14.8.0, v12.19.0Хуки очистки теперь могут быть асинхронными.

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

  • Быть дополнением Node-API, либо
  • Быть объявленным как контекстно-зависимое с использованием NODE_MODULE_INIT(), как описано выше.

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

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

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

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

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

C++
// 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;

// Примечание: в реальном приложении не следует полагаться на статические/глобальные данные.
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());  // Утверждение, что VM все еще жив
  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);
}

// Инициализировать это дополнение как контекстно-зависимое.
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, запустив:

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

Сборка

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

json
{
  "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:

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

console.log(addon.hello())
// Выводит: 'world'

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

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

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

Node-API

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

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

Node-API — это API для создания нативных аддонов. Он не зависит от базовой среды выполнения JavaScript (например, V8) и поддерживается как часть самого Node.js. Этот API будет стабильным по отношению к Application Binary Interface (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 следующим. Все остальные инструкции остаются прежними.

C++
// 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:

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

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

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

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

bash
node-gyp configure build

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

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

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

C++
// 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;

// Это реализация метода "add"
// Входные аргументы передаются с помощью
// структуры const FunctionCallbackInfo<Value>& args
void Add(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  // Проверка количества переданных аргументов.
  if (args.Length() < 2) {
    // Возвращаем ошибку, которая передается обратно в JavaScript
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate,
                            "Неверное количество аргументов").ToLocalChecked()));
    return;
  }

  // Проверка типов аргументов
  if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate,
                            "Неверные аргументы").ToLocalChecked()));
    return;
  }

  // Выполнение операции
  double value =
      args[0].As<Number>()->Value() + args[1].As<Number>()->Value();
  Local<Number> num = Number::New(isolate, value);

  // Устанавливаем возвращаемое значение (с помощью переданного
  // 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:

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

console.log('Должно получиться восемь:', addon.add(3, 5))

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

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

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:

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

addon(msg => {
  console.log(msg)
  // Выводит: 'hello world'
})

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

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

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

C++
// 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:

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

const obj1 = addon('hello')
const obj2 = addon('world')
console.log(obj1.msg, obj2.msg)
// Выводит: 'hello world'

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

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

C++
// 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();

  // опустите это, чтобы сделать ее анонимной
  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

Чтобы протестировать:

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

const fn = addon()
console.log(fn())
// Выводит: 'hello world'

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

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

C++
// 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:

C++
// 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() предоставляется путем добавления его в прототип конструктора:

C++
// 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 поле для MyObject::New()
  Local<Object> addon_data =
      addon_data_tpl->NewInstance(context).ToLocalChecked();

  // Подготовка шаблона конструктора
  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New, addon_data);
  tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

  // Прототип
  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()) {
    // Вызвано как конструктор: `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 {
    // Вызвано как обычная функция `MyObject(...)`, преобразование в конструктор.
    const int argc = 1;
    Local<Value> argv[argc] = { args[0] };
    Local<Function> cons =
        args.Data().As<Object>()->GetInternalField(0)
            .As<Value>().As<Function>();
    Local<Object> result =
        cons->NewInstance(context, argc, argv).ToLocalChecked();
    args.GetReturnValue().Set(result);
  }
}

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

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

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

}  // namespace demo

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

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

Протестируйте это с помощью:

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

const obj = new addon.MyObject(10)
console.log(obj.plusOne())
// Выводит: 11
console.log(obj.plusOne())
// Выводит: 12
console.log(obj.plusOne())
// Выводит: 13

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

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

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

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

js
const obj = addon.createObject()
// вместо:
// const obj = new addon.Object();

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

C++
// 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:

C++
// 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 аналогична предыдущему примеру:

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

namespace demo {

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

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

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

MyObject::~MyObject() {
}

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

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

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

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

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

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

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

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

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

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

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

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

}  // namespace demo

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

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

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

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

const obj = createObject(10)
console.log(obj.plusOne())
// Выводит: 11
console.log(obj.plusOne())
// Выводит: 12
console.log(obj.plusOne())
// Выводит: 13

const obj2 = createObject(20)
console.log(obj2.plusOne())
// Выводит: 21
console.log(obj2.plusOne())
// Выводит: 22
console.log(obj2.plusOne())
// Выводит: 23

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

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

C++
// 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 добавлен новый открытый метод для доступа к закрытым значениям после извлечения объекта.

C++
// 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 остается аналогичной предыдущей версии:

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

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

js
// 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)
// Выводит: 30