Аддоны C++
Аддоны — это динамически подключаемые общие объекты, которые можно загружать с помощью функции require() как обычные модули Node.js. Аддоны предоставляют интерфейс взаимодействия между JavaScript и нативным кодом.
Существует три способа реализации аддонов:
- Node-API (рекомендуется)
-
nan(Нативные абстракции для Node.js) - прямое использование внутренних библиотек V8, libuv и Node.js
В остальной части этого документа рассматривается последний способ, для которого требуются знания нескольких компонентов и 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'; jscopy
Сначала создайте файл 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) // N.B.: no semi-colon, this is not a function
} // namespace demo
cppcopy
На большинстве платформ для начала работы можно использовать следующий Makefile:
NODEJS_DEV_ROOT ?= $(shell dirname "$(command -v node)")/.. CXXFLAGS = -std=c++23 -I$(NODEJS_DEV_ROOT)/include/node -fPIC -shared -Wl,-undefined,dynamic_lookup hello.node: hello.cc $(CXX) $(CXXFLAGS) -o $@ $< bashcopy
Затем следующие команды скомпилируют и запустят код:
$ make
$ node -p 'require("./hello.node").hello()'
world
consolecopy
Интеграцию с экосистемой npm см. в разделе Сборка.
Аддоны с поддержкой нескольких контекстов
Аддоны, определённые с помощью 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. */
}
cppcopy
Другой вариант — использовать макрос 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. } cppcopy - Выделите память в куче для экземпляра этого класса в функции инициализации аддона. Для этого можно использовать ключевое слово
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 = Isolate::GetCurrent();
// 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();
}
cppcopyПоддержка рабочих потоков
Чтобы аддон можно было загружать в нескольких средах Node.js, например в основном потоке и рабочем потоке, он должен:
- быть аддоном Node-API или
- быть объявлен как поддерживающий несколько контекстов с помощью
NODE_MODULE_INIT(), как описано выше.
Для поддержки потоков Worker аддоны должны очищать все выделенные ресурсы при завершении такого потока. Это можно сделать с помощью функции AddEnvironmentCleanupHook():
void AddEnvironmentCleanupHook(v8::Isolate* isolate,
void (*fun)(void* arg),
void* arg);
cppcopy
Эта функция добавляет хук, который будет выполнен перед завершением работы данного экземпляра Node.js. При необходимости такие хуки можно удалить до их выполнения с помощью RemoveEnvironmentCleanupHook(), имеющей ту же сигнатуру. Обратные вызовы выполняются в порядке «последним пришёл — первым вышел».
При необходимости доступны дополнительные перегрузки AddEnvironmentCleanupHook() и RemoveEnvironmentCleanupHook(), в которых хук очистки принимает функцию обратного вызова. Это можно использовать для завершения работы асинхронных ресурсов, например любых дескрипторов libuv, зарегистрированных аддоном.
В следующем addon.cc используется AddEnvironmentCleanupHook:
// addon.cc
#include <node.h>
#include <assert.h>
#include <stdlib.h>
using node::AddEnvironmentCleanupHook;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::Object;
// Note: In a real-world application, do not rely on static/global data.
static char cookie[] = "yum yum";
static int cleanup_cb1_called = 0;
static int cleanup_cb2_called = 0;
static void cleanup_cb1(void* arg) {
Isolate* isolate = static_cast<Isolate*>(arg);
HandleScope scope(isolate);
Local<Object> obj = Object::New(isolate);
assert(!obj.IsEmpty()); // assert VM is still alive
assert(obj->IsObject());
cleanup_cb1_called++;
}
static void cleanup_cb2(void* arg) {
assert(arg == static_cast<void*>(cookie));
cleanup_cb2_called++;
}
static void sanity_check(void*) {
assert(cleanup_cb1_called == 1);
assert(cleanup_cb2_called == 1);
}
// Initialize this addon to be context-aware.
NODE_MODULE_INIT(/* exports, module, context */) {
Isolate* isolate = Isolate::GetCurrent();
AddEnvironmentCleanupHook(isolate, sanity_check, nullptr);
AddEnvironmentCleanupHook(isolate, cleanup_cb2, cookie);
AddEnvironmentCleanupHook(isolate, cleanup_cb1, isolate);
}
cppcopy
Проверьте работу на JavaScript, выполнив:
// test.js
require('./build/Release/addon');
jscopyСборка
После написания исходного кода его необходимо скомпилировать в двоичный файл addon.node. Для этого создайте в корневом каталоге проекта файл binding.gyp с описанием конфигурации сборки модуля в формате, подобном JSON. Этот файл использует node-gyp — инструмент, специально разработанный для компиляции аддонов Node.js.
{
"targets": [
{
"target_name": "addon",
"sources": [ "hello.cc" ]
}
]
}
jsoncopy
Версия утилиты 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'
jscopy
Поскольку точный путь к скомпилированному двоичному файлу аддона может зависеть от способа сборки (например, иногда он может находиться в ./build/Debug/), для загрузки скомпилированного модуля аддоны могут использовать пакет bindings.
Хотя пакет bindings реализует более сложный поиск модулей-аддонов, по сути он использует шаблон try…catch, подобный следующему:
try {
return require('./build/Release/addon.node');
} catch (err) {
return require('./build/Debug/addon.node');
}
jscopyПодключение библиотек, включённых в 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 и загрузит его.
Загрузка аддонов с помощью import
Стабильность: 1.0 — ранняя разработка
Чтобы включить поддержку статических import и динамических import() для загрузки двоичных аддонов, можно использовать флаг --experimental-addon-modules.
Если использовать приведённый выше пример «Привет, мир», можно написать следующее:
// hello.mjs
import myAddon from './hello.node';
// N.B.: import {hello} from './hello.node' would not work
console.log(myAddon.hello());
mjscopy
$ node --experimental-addon-modules hello.mjs world consolecopy
Нативные абстракции для Node.js
В каждом примере этого документа для реализации аддонов напрямую используются API Node.js и V8. API V8 может существенно меняться от одного выпуска V8 к следующему (и от одного основного выпуска Node.js к следующему). При каждом таком изменении аддоны может потребоваться обновить и перекомпилировать, чтобы они продолжали работать. График выпусков Node.js разработан так, чтобы свести к минимуму частоту и влияние таких изменений, однако Node.js мало что может сделать для обеспечения стабильности API V8.
Нативные абстракции для Node.js (или nan) предоставляют набор инструментов, рекомендованных разработчикам аддонов для обеспечения совместимости между предыдущими и будущими версиями V8 и Node.js. Примеры nan в разделе приведены здесь и показывают, как их использовать.
Node-API
Стабильность: 2 — стабильный
См. раздел Аддоны C/C++ с Node-API.
Примеры аддонов
Ниже приведены примеры аддонов, призванные помочь разработчикам начать работу. В примерах используются API V8. Описание различных вызовов V8 см. в онлайн-справочнике V8, а объяснение таких понятий, как дескрипторы, области видимости и шаблоны функций, — в руководстве для встраивающих V8.
В каждом из этих примеров используется следующий файл binding.gyp:
{
"targets": [
{
"target_name": "addon",
"sources": [ "addon.cc" ]
}
]
}
jsoncopy
Если файлов .cc несколько, просто добавьте имя дополнительного файла в массив sources:
"sources": ["addon.cc", "myexample.cc"] jsoncopy
После подготовки файла binding.gyp примеры аддонов можно настроить и собрать с помощью node-gyp:
node-gyp configure build bashcopy
Аргументы функции
Аддоны обычно предоставляют объекты и функции, доступные из 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
cppcopy
После компиляции пример аддона можно подключить и использовать в Node.js:
// test.js
const addon = require('./build/Release/addon');
console.log('This should be eight:', addon.add(3, 5));
jscopyОбратные вызовы
В аддонах часто передают функции 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
cppcopy
В этом примере используется форма Init() с двумя аргументами, которая получает полный объект module в качестве второго аргумента. Это позволяет аддону полностью заменить exports одной функцией, а не добавлять эту функцию в качестве свойства exports.
Чтобы проверить работу, выполните следующий код JavaScript:
// test.js
const addon = require('./build/Release/addon');
addon((msg) => {
console.log(msg);
// Prints: 'hello world'
});
jscopy
В этом примере функция обратного вызова вызывается синхронно.
Фабрика объектов
Аддоны могут создавать и возвращать новые объекты из функции 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
cppcopy
Проверка на 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'
jscopyФабрика функций
Ещё один распространённый сценарий — создание функций 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
cppcopy
Проверка:
// test.js
const addon = require('./build/Release/addon');
const fn = addon();
console.log(fn());
// Prints: 'hello world'
jscopyОбёртывание объектов 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
cppcopy
Затем в 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
cppcopy
В 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 = Isolate::GetCurrent();
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
cppcopy
Для сборки этого примера файл myobject.cc необходимо добавить в binding.gyp:
{
"targets": [
{
"target_name": "addon",
"sources": [
"addon.cc",
"myobject.cc"
]
}
]
}
jsoncopy
Проверьте работу с помощью:
// 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
jscopy
Деструктор объекта-обёртки будет вызван при сборке мусора для этого объекта. Для проверки деструктора можно использовать флаги командной строки, позволяющие принудительно запустить сборку мусора. Эти флаги предоставляет базовый движок JavaScript V8. Они могут быть изменены или удалены в любое время. Они не документированы Node.js или V8 и не должны использоваться вне тестирования.
При завершении работы процесса или рабочих потоков движок JS не вызывает деструкторы. Поэтому пользователь должен отслеживать эти объекты и обеспечивать их корректное уничтожение, чтобы избежать утечек ресурсов.
Фабрика обёрнутых объектов
Кроме того, можно использовать шаблон «фабрика», чтобы не создавать экземпляры объектов явно с помощью оператора JavaScript new:
const obj = addon.createObject(); // instead of: // const obj = new addon.Object(); jscopy
Сначала метод 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();
NODE_SET_METHOD(module, "exports", CreateObject);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)
} // namespace demo
cppcopy
В 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();
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
cppcopy
Реализация в 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 = Isolate::GetCurrent();
// 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
cppcopy
И снова: чтобы собрать этот пример, файл myobject.cc необходимо добавить в binding.gyp:
{
"targets": [
{
"target_name": "addon",
"sources": [
"addon.cc",
"myobject.cc"
]
}
]
}
jsoncopy
Проверьте работу с помощью:
// 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
jscopyПередача обёрнутых объектов
Помимо обёртывания и возврата объектов 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();
NODE_SET_METHOD(exports, "createObject", CreateObject);
NODE_SET_METHOD(exports, "add", Add);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)
} // namespace demo
cppcopy
В 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();
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
cppcopy
Реализация 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 = Isolate::GetCurrent();
// 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
cppcopy
Проверьте работу с помощью:
// 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
jscopy
© 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