Spec-Zone.ru › Node.js 20 LTS

Дополнения C++

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

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

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

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

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

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

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

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

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

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

Привет, мир

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

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

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

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

namespace demo {

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

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

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

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

}  // namespace demo copy

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

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

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

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

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

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

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

Дополнения с поддержкой контекстов

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

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

using namespace v8;

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

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

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

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

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

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

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

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

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

#include <node.h>

using namespace v8;

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

  // Per-addon data.
  int call_count;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Сборка

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

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

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

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

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

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

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

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

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

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

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

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

Подключение библиотек, включённых в Node.js

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

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

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

Загрузка дополнений с помощью require()

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

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

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

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

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

Node-API

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

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

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

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

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

namespace demo {

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

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

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

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

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

NAPI_MODULE(NODE_GYP_MODULE_NAME, init)

}  // namespace demo copy

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

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

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

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

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

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

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

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

node-gyp configure build copy

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

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

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

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

namespace demo {

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

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

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

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

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

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

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

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo copy

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

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

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

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

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

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

namespace demo {

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

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

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

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo copy

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

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

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

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

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

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

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

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

namespace demo {

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

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

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

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

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

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo copy

Проверка в JavaScript:

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

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

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

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

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

namespace demo {

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

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

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

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

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

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

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

NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo copy

Проверка:

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

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

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

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

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

namespace demo {

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

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

NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)

}  // namespace demo copy

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

// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

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

namespace demo {

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

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

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

  double value_;
};

}  // namespace demo

#endif copy

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

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

namespace demo {

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

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

MyObject::~MyObject() {
}

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

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

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

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

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

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

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

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

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

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

}  // namespace demo copy

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

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

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

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

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

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

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

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

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

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

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

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

namespace demo {

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

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

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

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

NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)

}  // namespace demo copy

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

// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

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

namespace demo {

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

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

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

}  // namespace demo

#endif copy

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

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

namespace demo {

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

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

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

MyObject::~MyObject() {
}

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

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

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

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

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

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

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

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

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

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

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

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

}  // namespace demo copy

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

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

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

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

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

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

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

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

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

namespace demo {

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

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

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

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

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

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

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

NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)

}  // namespace demo copy

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

// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

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

namespace demo {

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

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

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

}  // namespace demo

#endif copy

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

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

namespace demo {

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

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

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

MyObject::~MyObject() {
}

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

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

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

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

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

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

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

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

}  // namespace demo copy

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

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

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

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

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

Spec-Zone.ru

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