C++ плагины
Плагины — это динамически подключаемые общие объекты, написанные на C++. Функция require() может загружать плагины как обычные модули Node.js. Плагины обеспечивают интерфейс между JavaScript и библиотеками C/C++.
Существует три варианта реализации плагинов: Node-API, nan или непосредственное использование внутренних библиотек V8, libuv и Node.js. Если нет необходимости в прямом доступе к функциональности, которая не экспортируется Node-API, используйте Node-API. Для получения дополнительной информации о Node-API обратитесь к C/C++ плагины с Node-API.
Когда не используется Node-API, реализация плагинов усложняется, требуя знаний нескольких компонентов и API:
-
V8: библиотека C++, которую Node.js использует для предоставления реализации JavaScript. V8 предоставляет механизмы для создания объектов, вызова функций и т. д. API V8 документирован в основном в заголовочном файле
v8.h(deps/v8/include/v8.hв дереве исходного кода Node.js), который также доступен онлайн. -
libuv: Библиотека C, которая реализует цикл событий Node.js, его потоки обработки и все асинхронные действия платформы. Она также служит межплатформенной библиотекой абстракции, предоставляя лёгкий, похожий на POSIX доступ ко многим общим системным задачам на всех основных операционных системах, таких как взаимодействие с файловой системой, сокетами, таймерами и системными событиями. libuv также предоставляет абстракцию потоков, подобную POSIX-потокам, для более сложных асинхронных плагинов, которые нуждаются в продвижении за пределы стандартного цикла событий. Авторы плагинов должны избегать блокировки цикла событий с помощью операций ввода-вывода или других ресурсоёмких задач, перегружая работу через libuv в неблокирующие системные операции, рабочие потоки или пользовательское использование потоков libuv.
-
Внутренние библиотеки Node.js. Сам Node.js экспортирует API C++, которые могут использовать плагины, наиболее важным из которых является класс
node::ObjectWrap. -
Node.js включает другие статически связанные библиотеки, включая 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::Object;
using v8::String;
using v8::Value;
void Method(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(
isolate, "world").ToLocalChecked());
}
void Initialize(Local<Object> exports) {
NODE_SET_METHOD(exports, "hello", Method);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
} // namespace demo 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()для создания функций с поддержкой нативного кода. Третий параметр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 Поддержка рабочих процессов
Для загрузки из нескольких сред Node.js, таких как основной поток и поток рабочего процесса, плагин должен:
- Быть плагином Node-API, или
- Быть объявлен как плагин, осознающий контекст, используя
NODE_MODULE_INIT()как описано выше
Для поддержки потоков Worker плагины должны очищать любые ресурсы, которые они могут выделять, при существовании такого потока. Этого можно достичь с помощью функции AddEnvironmentCleanupHook():
void AddEnvironmentCleanupHook(v8::Isolate* isolate,
void (*fun)(void* arg),
void* arg); copy Эта функция добавляет обработчик, который будет выполнен перед завершением данного экземпляра Node.js. При необходимости такие обработчики можно удалить до их выполнения с помощью RemoveEnvironmentCleanupHook(), у которой такая же сигнатура. Обработчики вызываются в порядке LIFO.
При необходимости существуют дополнительные перегрузки 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. Смотрите инструкции по установке для получения дополнительной информации, включая платформозависимые требования.
После создания файла 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' 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.
Нативные абстракции для Node.js (или nan) предоставляют набор инструментов, которые рекомендуется использовать разработчикам плагинов, чтобы поддерживать совместимость между прошлыми и будущими версиями V8 и Node.js. См. примеры для иллюстрации того, как это можно использовать.
Node-API
Node-API — это API для создания нативных дополнений. Оно независимо от базового JavaScript-движка (например, V8) и поддерживается как часть самого Node.js. Это API будет стабильным с точки зрения двоичного интерфейса приложений (ABI) в разных версиях Node.js. Оно предназначено для изоляции дополнений от изменений в базовом JavaScript-движке и позволяет модулям, скомпилированным для одной версии, работать в более поздних версиях Node.js без перекомпиляции. Дополнения строятся/упаковываются с использованием того же подхода/инструментов, что и в данном документе (node-gyp и т. д.). Единственное отличие — набор API, используемых нативным кодом. Вместо использования V8 или Native Abstractions for Node.js API используются функции, доступные в Node-API.
Создание и сопровождение дополнения, которое выигрывает от стабильности ABI, предоставляемой Node-API, влечёт за собой определённые соображения реализации.
Чтобы использовать Node-API в приведённом выше примере «Hello world», замените содержимое hello.cc следующим. Все остальные инструкции остаются неизменными.
// hello.cc using Node-API
#include <node_api.h>
namespace demo {
napi_value Method(napi_env env, napi_callback_info args) {
napi_value greeting;
napi_status status;
status = napi_create_string_utf8(env, "world", NAPI_AUTO_LENGTH, &greeting);
if (status != napi_ok) return nullptr;
return greeting;
}
napi_value init(napi_env env, napi_value exports) {
napi_status status;
napi_value fn;
status = napi_create_function(env, nullptr, 0, Method, nullptr, &fn);
if (status != napi_ok) return nullptr;
status = napi_set_named_property(env, exports, "hello", fn);
if (status != napi_ok) return nullptr;
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, init)
} // namespace demo copy Функции, доступные и как их использовать, документированы в C/C++ дополнения с Node-API.
Примеры дополнений
Ниже приведены примеры дополнений, предназначенные для помощи разработчикам в начале работы. Примеры используют API V8. Обратитесь к онлайн справочнику V8 за помощью с различными вызовами V8 и к Руководству Embedder 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, и как возвращать результат:
// 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.Holder());
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 и никогда не должны использоваться за пределами тестирования.
Во время завершения процесса или потоков-работники деструкторы не вызываются движком JS. Поэтому ответственность пользователя состоит в отслеживании этих объектов и обеспечении надлежащего уничтожения, чтобы избежать утечек ресурсов.
Фабрика обернутых объектов
В качестве альтернативы можно использовать шаблон фабрики, чтобы избежать явного создания экземпляров объектов с помощью JavaScript-оператора new:
const obj = addon.createObject(); // instead of: // const obj = new addon.Object(); copy
Сначала метод 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 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.Holder());
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/api/addons.html