C++ плагины
Плагины — это динамически подключаемые общие объекты, написанные на C++. Функция require() может загружать плагины как обычные модули Node.js. Плагины обеспечивают интерфейс между JavaScript и библиотеками C/C++.
Существует три варианта реализации плагинов: N-API, nan или непосредственное использование внутренних библиотек V8, libuv и Node.js. Если нет необходимости в прямом доступе к функциональности, которая не экспортируется через N-API, используйте N-API. Для получения дополнительной информации об N-API обратитесь к Плагины C/C++ с N-API.
При отсутствии использования N-API реализация плагинов усложняется, требуя знаний о нескольких компонентах и API:
-
V8: библиотека C++, используемая Node.js для обеспечения реализации JavaScript. V8 предоставляет механизмы для создания объектов, вызова функций и т. д. API V8 в основном документирован в файле заголовков
v8.h(deps/v8/include/v8.hв дереве исходных кодов Node.js), который также доступен онлайн. -
libuv: Библиотека C, реализующая цикл событий Node.js, его рабочие потоки и все асинхронные поведения платформы. Она также служит кроссплатформенной абстрагирующей библиотекой, обеспечивая лёгкий доступ в стиле POSIX ко многим общим системным задачам на всех основных операционных системах, таким как взаимодействие с файловой системой, сокетами, таймерами и системными событиями. libuv также предоставляет абстракцию потоков, похожую на 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 действительны только в одном контексте и, скорее всего, приведут к ошибке при доступе из неправильного контекста или из другого потока, отличного от того, в котором они были созданы.
Плагин, учитывающий контекст, можно структурировать, избегая глобальных статических данных, выполнив следующие шаги:
- Определите класс, который будет содержать данные для каждого экземпляра плагина, и который имеет статическое член в форме
static void DeleteInstance(void* data) { // Cast `data` to an instance of the class and delete it. } - Выделите в куче экземпляр этого класса в функции инициализации плагина. Это можно сделать с использованием ключевого слова
new - Вызовите
node::AddEnvironmentCleanupHook(), передав созданный экземпляр и указатель наDeleteInstance(). - Храните экземпляр класса в
v8::External, и - Передавайте
v8::Externalво все методы, экспортируемые в JavaScript, передавая его вv8::FunctionTemplate::New()илиv8::Function::New(), которые создают нативные JavaScript-функции. Третий параметрv8::FunctionTemplate::New()илиv8::Function::New()принимаетv8::Externalи делает его доступным в нативной функции обратного вызова с использованием методаv8::FunctionCallbackInfo::Data().
Это обеспечит, что данные для каждого экземпляра плагина поступят в каждую точку привязки, которую можно вызвать из JavaScript. Данные для каждого экземпляра плагина также должны передаваться в любые асинхронные функции обратного вызова, которые может создавать плагин.
Следующий пример иллюстрирует реализацию плагина, учитывающего контекст:
#include <node.h>
using namespace v8;
class AddonData {
public:
explicit AddonData(Isolate* isolate):
call_count(0) {
// Ensure this per-addon-instance data is deleted at environment cleanup.
node::AddEnvironmentCleanupHook(isolate, DeleteInstance, this);
}
// Per-addon data.
int call_count;
static void DeleteInstance(void* data) {
delete static_cast<AddonData*>(data);
}
};
static void Method(const v8::FunctionCallbackInfo<v8::Value>& info) {
// Retrieve the per-addon-instance data.
AddonData* data =
reinterpret_cast<AddonData*>(info.Data().As<External>()->Value());
data->call_count++;
info.GetReturnValue().Set((double)data->call_count);
}
// Initialize this addon to be context-aware.
NODE_MODULE_INIT(/* exports, module, context */) {
Isolate* isolate = context->GetIsolate();
// Create a new instance of `AddonData` for this instance of the addon and
// tie its life cycle to that of the Node.js environment.
AddonData* data = new AddonData(isolate);
// Wrap the data in a `v8::External` so we can pass it to the method we
// expose.
Local<External> external = External::New(isolate, data);
// Expose the method `Method` to JavaScript, and make sure it receives the
// per-addon-instance data we created above by passing `external` as the
// third parameter to the `FunctionTemplate` constructor.
exports->Set(context,
String::NewFromUtf8(isolate, "method", NewStringType::kNormal)
.ToLocalChecked(),
FunctionTemplate::New(isolate, Method, external)
->GetFunction(context).ToLocalChecked()).FromJust();
} Поддержка рабочих потоков
Для загрузки из нескольких сред Node.js, таких как основной поток и поток рабочего потока, плагин должен:
- Быть плагином N-API, или
- Быть объявлен как плагин, учитывающий контекст, с использованием
NODE_MODULE_INIT()как описано выше
Для поддержки потоков Worker плагины должны очищать любые ресурсы, которые они могут выделить, когда существует такой поток. Этого можно достичь с помощью функции AddEnvironmentCleanupHook():
void AddEnvironmentCleanupHook(v8::Isolate* isolate,
void (*fun)(void* arg),
void* arg); Эта функция добавляет обработчик, который будет выполнен перед завершением работы данного экземпляра Node.js. Если необходимо, такие обработчики можно удалить перед их выполнением с помощью RemoveEnvironmentCleanupHook(), у которой такая же сигнатура. Обработчики вызываются в порядке LIFO.
При необходимости существуют дополнительные перегрузки пар AddEnvironmentCleanupHook() и RemoveEnvironmentCleanupHook(), где обработчик очистки принимает функцию обратного вызова. Это можно использовать для завершения работы асинхронных ресурсов, таких как любые обработчики libuv, зарегистрированные плагином.
Следующий addon.cc использует AddEnvironmentCleanupHook:
// addon.cc
#include <assert.h>
#include <stdlib.h>
#include <node.h>
using node::AddEnvironmentCleanupHook;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::Object;
// Note: In a real-world application, do not rely on static/global data.
static char cookie[] = "yum yum";
static int cleanup_cb1_called = 0;
static int cleanup_cb2_called = 0;
static void cleanup_cb1(void* arg) {
Isolate* isolate = static_cast<Isolate*>(arg);
HandleScope scope(isolate);
Local<Object> obj = Object::New(isolate);
assert(!obj.IsEmpty()); // assert VM is still alive
assert(obj->IsObject());
cleanup_cb1_called++;
}
static void cleanup_cb2(void* arg) {
assert(arg == static_cast<void*>(cookie));
cleanup_cb2_called++;
}
static void sanity_check(void*) {
assert(cleanup_cb1_called == 1);
assert(cleanup_cb2_called == 1);
}
// Initialize this addon to be context-aware.
NODE_MODULE_INIT(/* exports, module, context */) {
Isolate* isolate = context->GetIsolate();
AddEnvironmentCleanupHook(isolate, sanity_check, nullptr);
AddEnvironmentCleanupHook(isolate, cleanup_cb2, cookie);
AddEnvironmentCleanupHook(isolate, cleanup_cb1, isolate);
} Тестирование в JavaScript путём выполнения:
// test.js
require('./build/Release/addon'); Компиляция
После написания исходного кода его необходимо скомпилировать в двоичный файл addon.node. Для этого создайте файл binding.gyp в корне проекта, описывающий конфигурацию компиляции модуля с использованием формата, подобного JSON. Этот файл используется инструментом node-gyp, созданным специально для компиляции плагинов Node.js.
{
"targets": [
{
"target_name": "addon",
"sources": [ "hello.cc" ]
}
]
} Версия утилиты node-gyp включена в состав Node.js и распространяется как часть npm. Эта версия не предоставляется разработчикам напрямую и предназначена только для поддержки использования команды npm install для компиляции и установки плагинов. Разработчики, которые хотят использовать node-gyp напрямую, могут установить его с помощью команды npm install -g node-gyp. См. node-gyp инструкции по установке для получения дополнительной информации, включая платформенно-специфические требования.
После создания файла binding.gyp используйте node-gyp configure для генерации соответствующих файлов проекта для текущей платформы. Это сгенерирует файл Makefile (на платформах Unix) или файл vcxproj (на Windows) в каталоге build/.
Далее, выполните команду node-gyp build для генерации скомпилированного файла addon.node. Он будет помещён в каталог build/Release/.
При использовании npm install для установки плагина Node.js, npm использует свою встроенную версию node-gyp для выполнения того же набора действий, генерируя скомпилированную версию плагина для платформы пользователя по требованию.
После компиляции двоичный плагин может быть использован в Node.js, указав require() на скомпилированный модуль addon.node:
// hello.js
const addon = require('./build/Release/addon');
console.log(addon.hello());
// Prints: 'world' Так как точный путь к двоичному файлу скомпилированного плагина может изменяться в зависимости от способа его компиляции (иногда он может находиться в ./build/Debug/), плагины могут использовать пакет bindings для загрузки скомпилированного модуля.
Хотя реализация пакета bindings более совершенна в том, как она находит модули плагинов, она по существу использует шаблон try…catch , подобный:
try {
return require('./build/Release/addon.node');
} catch (err) {
return require('./build/Debug/addon.node');
} Связывание с библиотеками, включёнными в Node.js
Node.js использует статически скомпилированные библиотеки, такие как V8, libuv и OpenSSL. Все плагины должны быть связаны с V8 и могут быть связаны с другими зависимостями. Обычно это простая задача, заключающаяся в включении соответствующих #include <...> инструкций (например, #include <v8.h>) и node-gyp, которые автоматически найдут необходимые заголовочные файлы. Однако следует учитывать несколько нюансов:
-
При выполнении
node-gyp, он обнаружит конкретную версию Node.js и загрузит либо полный исходный архив tar, либо только заголовки. Если загружается весь исходный код, плагины получат полный доступ к полному набору зависимостей 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 примеры для иллюстрации того, как это можно использовать.
N-API
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 world", замените содержимое 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 для получения объяснений ряда концепций, таких как обработчики, области видимости, шаблоны функций и т. д.
Каждый из этих примеров использует следующий файл 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);
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::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", NewStringType::kNormal).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", NewStringType::kNormal).ToLocalChecked(),
constructor).FromJust();
}
void MyObject::New(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
if (args.IsConstructCall()) {
// Invoked as constructor: `new MyObject(...)`
double value = args[0]->IsUndefined() ?
0 : args[0]->NumberValue(context).FromMaybe(0);
MyObject* obj = new MyObject(value);
obj->Wrap(args.This());
args.GetReturnValue().Set(args.This());
} else {
// Invoked as plain function `MyObject(...)`, turn into construct call.
const int argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons =
args.Data().As<Object>()->GetInternalField(0).As<Function>();
Local<Object> result =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(result);
}
}
void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.Holder());
obj->value_ += 1;
args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}
} // namespace demo Для построения этого примера, файл myobject.cc должен быть добавлен в binding.gyp:
{
"targets": [
{
"target_name": "addon",
"sources": [
"addon.cc",
"myobject.cc"
]
}
]
} Протестируйте с:
// test.js
const addon = require('./build/Release/addon');
const obj = new addon.MyObject(10);
console.log(obj.plusOne());
// Prints: 11
console.log(obj.plusOne());
// Prints: 12
console.log(obj.plusOne());
// Prints: 13 Деструктор объекта-обёртки будет выполняться при сборе мусора. Для тестирования деструктора доступны флаги командной строки, которые можно использовать для принудительного сбора мусора. Эти флаги предоставляются базовым движком JavaScript V8. Они могут быть изменены или удалены в любое время. Они не документированы Node.js или V8 и никогда не должны использоваться вне тестирования.
Фабрика обернутых объектов
В качестве альтернативы, можно использовать паттерн фабрики, чтобы избежать явного создания экземпляров объектов с использованием JavaScript-оператора new:
const obj = addon.createObject(); // instead of: // const obj = new addon.Object();
Во-первых, метод createObject() реализуется в addon.cc:
// addon.cc
#include <node.h>
#include "myobject.h"
namespace demo {
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
void CreateObject(const FunctionCallbackInfo<Value>& args) {
MyObject::NewInstance(args);
}
void InitAll(Local<Object> exports, Local<Object> module) {
MyObject::Init(exports->GetIsolate());
NODE_SET_METHOD(module, "exports", CreateObject);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)
} // namespace demo В myobject.h, статический метод NewInstance() добавляется для обработки создания объекта. Этот метод заменяет использование new в JavaScript:
// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H
#include <node.h>
#include <node_object_wrap.h>
namespace demo {
class MyObject : public node::ObjectWrap {
public:
static void Init(v8::Isolate* isolate);
static void NewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);
private:
explicit MyObject(double value = 0);
~MyObject();
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void PlusOne(const v8::FunctionCallbackInfo<v8::Value>& args);
static v8::Global<v8::Function> constructor;
double value_;
};
} // namespace demo
#endif Реализация в myobject.cc аналогична предыдущему примеру:
// myobject.cc
#include <node.h>
#include "myobject.h"
namespace demo {
using node::AddEnvironmentCleanupHook;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::Isolate;
using v8::Local;
using v8::NewStringType;
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", 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());
AddEnvironmentCleanupHook(isolate, [](void*) {
constructor.Reset();
}, nullptr);
}
void MyObject::New(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
if (args.IsConstructCall()) {
// Invoked as constructor: `new MyObject(...)`
double value = args[0]->IsUndefined() ?
0 : args[0]->NumberValue(context).FromMaybe(0);
MyObject* obj = new MyObject(value);
obj->Wrap(args.This());
args.GetReturnValue().Set(args.This());
} else {
// Invoked as plain function `MyObject(...)`, turn into construct call.
const int argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Object> instance =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(instance);
}
}
void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
const unsigned argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Context> context = isolate->GetCurrentContext();
Local<Object> instance =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(instance);
}
void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.Holder());
obj->value_ += 1;
args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}
} // namespace demo Ещё раз, для построения этого примера, файл myobject.cc должен быть добавлен в binding.gyp:
{
"targets": [
{
"target_name": "addon",
"sources": [
"addon.cc",
"myobject.cc"
]
}
]
} Протестируйте с:
// test.js
const createObject = require('./build/Release/addon');
const obj = createObject(10);
console.log(obj.plusOne());
// Prints: 11
console.log(obj.plusOne());
// Prints: 12
console.log(obj.plusOne());
// Prints: 13
const obj2 = createObject(20);
console.log(obj2.plusOne());
// Prints: 21
console.log(obj2.plusOne());
// Prints: 22
console.log(obj2.plusOne());
// Prints: 23 Передача обернутых объектов
Помимо обертывания и возврата C++ объектов, можно передавать обернутые объекты, распаковывая их с помощью вспомогательной функции Node.js node::ObjectWrap::Unwrap. Следующий пример показывает функцию add(), которая может принимать два объекта MyObject в качестве входных аргументов:
// addon.cc
#include <node.h>
#include <node_object_wrap.h>
#include "myobject.h"
namespace demo {
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;
void CreateObject(const FunctionCallbackInfo<Value>& args) {
MyObject::NewInstance(args);
}
void Add(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
MyObject* obj1 = node::ObjectWrap::Unwrap<MyObject>(
args[0]->ToObject(context).ToLocalChecked());
MyObject* obj2 = node::ObjectWrap::Unwrap<MyObject>(
args[1]->ToObject(context).ToLocalChecked());
double sum = obj1->value() + obj2->value();
args.GetReturnValue().Set(Number::New(isolate, sum));
}
void InitAll(Local<Object> exports) {
MyObject::Init(exports->GetIsolate());
NODE_SET_METHOD(exports, "createObject", CreateObject);
NODE_SET_METHOD(exports, "add", Add);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)
} // namespace demo В myobject.h, добавляется новый публичный метод для доступа к закрытым значениям после распаковки объекта.
// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H
#include <node.h>
#include <node_object_wrap.h>
namespace demo {
class MyObject : public node::ObjectWrap {
public:
static void Init(v8::Isolate* isolate);
static void NewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);
inline double value() const { return value_; }
private:
explicit MyObject(double value = 0);
~MyObject();
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static v8::Global<v8::Function> constructor;
double value_;
};
} // namespace demo
#endif Реализация myobject.cc аналогична предыдущей:
// myobject.cc
#include <node.h>
#include "myobject.h"
namespace demo {
using node::AddEnvironmentCleanupHook;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::Isolate;
using v8::Local;
using v8::NewStringType;
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", NewStringType::kNormal).ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
Local<Context> context = isolate->GetCurrentContext();
constructor.Reset(isolate, tpl->GetFunction(context).ToLocalChecked());
AddEnvironmentCleanupHook(isolate, [](void*) {
constructor.Reset();
}, nullptr);
}
void MyObject::New(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
if (args.IsConstructCall()) {
// Invoked as constructor: `new MyObject(...)`
double value = args[0]->IsUndefined() ?
0 : args[0]->NumberValue(context).FromMaybe(0);
MyObject* obj = new MyObject(value);
obj->Wrap(args.This());
args.GetReturnValue().Set(args.This());
} else {
// Invoked as plain function `MyObject(...)`, turn into construct call.
const int argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Object> instance =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(instance);
}
}
void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
const unsigned argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Context> context = isolate->GetCurrentContext();
Local<Object> instance =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(instance);
}
} // namespace demo Протестируйте с:
// test.js
const addon = require('./build/Release/addon');
const obj1 = addon.createObject(10);
const obj2 = addon.createObject(20);
const result = addon.add(obj1, obj2);
console.log(result);
// Prints: 30
© Joyent, Inc. and other Node contributors
Licensed under the MIT License.
Node.js is a trademark of Joyent, Inc. and is used with its permission.
We are not endorsed by or affiliated with Joyent.
https://nodejs.org/dist/latest-v12.x/docs/api/addons.html