Дополнения 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. Node.js намеренно повторно экспортирует только символы libuv, OpenSSL, V8 и zlib, которые дополнения могут использовать в различной степени. Дополнительные сведения см. в разделе Компоновка с библиотеками, входящими в состав 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 Поддержка рабочих потоков
Чтобы загружаться в нескольких средах 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. Дополнительные сведения, в том числе требования для конкретных платформ, см. в инструкциях по установке 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 и загрузит его вместо двоичного файла.
Native Abstractions for 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
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 входные аргументы и возвращаемое значение необходимо преобразовывать между кодом 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.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, и их никогда не следует использовать вне тестирования.
При завершении процесса или рабочих потоков движок JavaScript не вызывает деструкторы. Поэтому пользователь должен отслеживать эти объекты и обеспечивать их надлежащее уничтожение, чтобы избежать утечек ресурсов.
Фабрика обёрнутых объектов
Другой вариант — использовать шаблон «Фабрика», чтобы не создавать экземпляры объектов явно с помощью оператора 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.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-v22.x/docs/api/addons.html