C++-Addons
Addons sind dynamisch verknüpfte, gemeinsam genutzte Objekte, die in C++ geschrieben sind. Die Funktion require()
kann Addons wie gewöhnliche Node.js-Module laden. Addons bieten eine Schnittstelle zwischen JavaScript und C/C++-Bibliotheken.
Es gibt drei Möglichkeiten zur Implementierung von Addons:
- Node-API
nan
(Native Abstractions for Node.js)- direkte Verwendung interner V8-, libuv- und Node.js-Bibliotheken
Sofern kein direkter Zugriff auf Funktionen erforderlich ist, die nicht von Node-API bereitgestellt werden, verwenden Sie Node-API. Weitere Informationen zu Node-API finden Sie unter C/C++-Addons mit Node-API.
Wenn Node-API nicht verwendet wird, wird die Implementierung von Addons komplexer und erfordert Kenntnisse mehrerer Komponenten und APIs:
- V8: die C++-Bibliothek, die Node.js verwendet, um die JavaScript-Implementierung bereitzustellen. Sie bietet die Mechanismen zum Erstellen von Objekten, Aufrufen von Funktionen usw. Die API von V8 ist hauptsächlich in der Header-Datei
v8.h
(deps/v8/include/v8.h
im Node.js-Quellbaum) dokumentiert und auch online verfügbar. - libuv: Die C-Bibliothek, die die Node.js-Ereignisschleife, ihre Worker-Threads und alle asynchronen Verhaltensweisen der Plattform implementiert. Sie dient auch als plattformübergreifende Abstraktionsbibliothek und bietet auf allen wichtigen Betriebssystemen einfachen, POSIX-artigen Zugriff auf viele gängige Systemtasks, wie z. B. die Interaktion mit dem Dateisystem, Sockets, Timern und Systemabläufen. libuv bietet auch eine Thread-Abstraktion ähnlich wie POSIX-Threads für anspruchsvollere asynchrone Addons, die über die Standard-Ereignisschleife hinausgehen müssen. Addon-Autoren sollten vermeiden, die Ereignisschleife mit E/A- oder anderen zeitintensiven Aufgaben zu blockieren, indem sie die Arbeit über libuv an nicht blockierende Systemoperationen, Worker-Threads oder eine benutzerdefinierte Verwendung von libuv-Threads auslagern.
- Interne Node.js-Bibliotheken: Node.js exportiert selbst C++-APIs, die Addons verwenden können, von denen die wichtigste die Klasse
node::ObjectWrap
ist. - Andere statisch verknüpfte Bibliotheken (einschließlich OpenSSL): Diese anderen Bibliotheken befinden sich im Verzeichnis
deps/
im Node.js-Quellbaum. Nur die libuv-, OpenSSL-, V8- und zlib-Symbole werden von Node.js absichtlich erneut exportiert und können von Addons in unterschiedlichem Umfang verwendet werden. Weitere Informationen finden Sie unter Verknüpfen mit Bibliotheken, die mit Node.js enthalten sind.
Alle folgenden Beispiele stehen zum Download zur Verfügung und können als Ausgangspunkt für ein Addon verwendet werden.
Hallo Welt
Dieses "Hallo Welt"-Beispiel ist ein einfaches Addon, geschrieben in C++, das dem folgenden JavaScript-Code entspricht:
module.exports.hello = () => 'world'
Erstellen Sie zunächst die Datei 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
Alle Node.js-Addons müssen eine Initialisierungsfunktion exportieren, die dem Muster folgt:
void Initialize(Local<Object> exports);
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
Es gibt kein Semikolon nach NODE_MODULE
, da es keine Funktion ist (siehe node.h
).
Der Modulname
muss mit dem Dateinamen der endgültigen Binärdatei (ohne die .node
-Endung) übereinstimmen.
Im Beispiel hello.cc
ist die Initialisierungsfunktion dann Initialize
und der Addon-Modulname ist addon
.
Beim Erstellen von Addons mit node-gyp
stellt die Verwendung des Makros NODE_GYP_MODULE_NAME
als ersten Parameter von NODE_MODULE()
sicher, dass der Name der endgültigen Binärdatei an NODE_MODULE()
übergeben wird.
Addons, die mit NODE_MODULE()
definiert sind, können nicht gleichzeitig in mehreren Kontexten oder mehreren Threads geladen werden.
Kontext-bewusste Addons
Es gibt Umgebungen, in denen Node.js-Addons möglicherweise mehrmals in mehreren Kontexten geladen werden müssen. Beispielsweise führt die Electron-Laufzeit mehrere Instanzen von Node.js in einem einzigen Prozess aus. Jede Instanz hat ihren eigenen require()
-Cache, und daher benötigt jede Instanz ein natives Addon, um sich korrekt zu verhalten, wenn es über require()
geladen wird. Das bedeutet, dass das Addon mehrere Initialisierungen unterstützen muss.
Ein kontext-bewusstes Addon kann mit dem Makro NODE_MODULE_INITIALIZER
erstellt werden, das zu dem Namen einer Funktion expandiert, die Node.js beim Laden eines Addons erwarten wird. Ein Addon kann daher wie im folgenden Beispiel initialisiert werden:
using namespace v8;
extern "C" NODE_MODULE_EXPORT void
NODE_MODULE_INITIALIZER(Local<Object> exports,
Local<Value> module,
Local<Context> context) {
/* Führen Sie hier die Schritte zur Addon-Initialisierung durch. */
}
Eine andere Möglichkeit ist die Verwendung des Makros NODE_MODULE_INIT()
, das auch ein kontext-bewusstes Addon erstellt. Im Gegensatz zu NODE_MODULE()
, das verwendet wird, um ein Addon um eine gegebene Addon-Initialisierungsfunktion herum zu erstellen, dient NODE_MODULE_INIT()
als Deklaration eines solchen Initialisierers, dem ein Funktionskörper folgt.
Die folgenden drei Variablen können innerhalb des Funktionskörpers nach einem Aufruf von NODE_MODULE_INIT()
verwendet werden:
Local\<Object\> exports
,Local\<Value\> module
, undLocal\<Context\> context
Der Aufbau eines kontext-bewussten Addons erfordert eine sorgfältige Verwaltung globaler statischer Daten, um Stabilität und Korrektheit zu gewährleisten. Da das Addon möglicherweise mehrmals geladen wird, möglicherweise sogar von verschiedenen Threads, müssen alle im Addon gespeicherten globalen statischen Daten ordnungsgemäß geschützt werden und dürfen keine dauerhaften Verweise auf JavaScript-Objekte enthalten. Der Grund dafür ist, dass JavaScript-Objekte nur in einem Kontext gültig sind und wahrscheinlich zu einem Absturz führen, wenn sie aus dem falschen Kontext oder von einem anderen Thread als dem, auf dem sie erstellt wurden, zugegriffen werden.
Das kontext-bewusste Addon kann so strukturiert werden, dass globale statische Daten vermieden werden, indem die folgenden Schritte ausgeführt werden:
- Definieren Sie eine Klasse, die Daten pro Addon-Instanz enthält und ein statisches Mitglied der Form hat
- Belegen Sie eine Instanz dieser Klasse im Addon-Initialisierer auf dem Heap. Dies kann mit dem Schlüsselwort
new
erreicht werden. - Rufen Sie
node::AddEnvironmentCleanupHook()
auf und übergeben Sie ihm die oben erstellte Instanz und einen Zeiger aufDeleteInstance()
. Dies stellt sicher, dass die Instanz gelöscht wird, wenn die Umgebung heruntergefahren wird. - Speichern Sie die Instanz der Klasse in einem
v8::External
, und - Geben Sie das
v8::External
an alle Methoden weiter, die JavaScript zur Verfügung gestellt werden, indem Sie es anv8::FunctionTemplate::New()
oderv8::Function::New()
übergeben, wodurch die nativen JavaScript-Funktionen erstellt werden. Der dritte Parameter vonv8::FunctionTemplate::New()
oderv8::Function::New()
akzeptiert dasv8::External
und macht es im nativen Callback über die Methodev8::FunctionCallbackInfo::Data()
verfügbar.
Dies stellt sicher, dass die Daten pro Addon-Instanz jedes Binding erreichen, das von JavaScript aufgerufen werden kann. Die Daten pro Addon-Instanz müssen auch an alle asynchronen Callbacks übergeben werden, die das Addon möglicherweise erstellt.
Das folgende Beispiel veranschaulicht die Implementierung eines kontext-bewussten Addons:
#include <node.h>
using namespace v8;
class AddonData {
public:
explicit AddonData(Isolate* isolate):
call_count(0) {
// Stellen Sie sicher, dass diese Daten pro Addon-Instanz bei der Bereinigung der Umgebung gelöscht werden.
node::AddEnvironmentCleanupHook(isolate, DeleteInstance, this);
}
// Daten pro Addon.
int call_count;
static void DeleteInstance(void* data) {
delete static_cast<AddonData*>(data);
}
};
static void Method(const v8::FunctionCallbackInfo<v8::Value>& info) {
// Rufen Sie die Daten pro Addon-Instanz ab.
AddonData* data =
reinterpret_cast<AddonData*>(info.Data().As<External>()->Value());
data->call_count++;
info.GetReturnValue().Set((double)data->call_count);
}
// Initialisieren Sie dieses Addon als kontext-bewusst.
NODE_MODULE_INIT(/* exports, module, context */) {
Isolate* isolate = context->GetIsolate();
// Erstellen Sie eine neue Instanz von `AddonData` für diese Instanz des Addons und
// verknüpfen Sie ihren Lebenszyklus mit dem der Node.js-Umgebung.
AddonData* data = new AddonData(isolate);
// Verpacken Sie die Daten in ein `v8::External`, damit wir sie an die Methode übergeben können, die wir
// verfügbar machen.
Local<External> external = External::New(isolate, data);
// Stellen Sie die Methode `Method` JavaScript zur Verfügung und stellen Sie sicher, dass sie die
// Daten pro Addon-Instanz erhält, die wir oben erstellt haben, indem Sie `external` als
// dritten Parameter an den `FunctionTemplate`-Konstruktor übergeben.
exports->Set(context,
String::NewFromUtf8(isolate, "method").ToLocalChecked(),
FunctionTemplate::New(isolate, Method, external)
->GetFunction(context).ToLocalChecked()).FromJust();
}
Worker-Unterstützung
[Verlauf]
Version | Änderungen |
---|---|
v14.8.0, v12.19.0 | Bereinigungs-Hooks können nun asynchron sein. |
Um aus mehreren Node.js-Umgebungen, wie z. B. einem Hauptthread und einem Worker-Thread, geladen zu werden, muss ein Add-on entweder:
- Ein Node-API-Add-on sein, oder
- Als kontextabhängig deklariert werden, indem
NODE_MODULE_INIT()
wie oben beschrieben verwendet wird.
Um Worker
-Threads zu unterstützen, müssen Add-ons alle Ressourcen bereinigen, die sie möglicherweise zugeordnet haben, wenn ein solcher Thread beendet wird. Dies kann durch die Verwendung der Funktion AddEnvironmentCleanupHook()
erreicht werden:
void AddEnvironmentCleanupHook(v8::Isolate* isolate,
void (*fun)(void* arg),
void* arg);
Diese Funktion fügt einen Hook hinzu, der ausgeführt wird, bevor eine gegebene Node.js-Instanz heruntergefahren wird. Falls nötig, können solche Hooks entfernt werden, bevor sie mit RemoveEnvironmentCleanupHook()
ausgeführt werden, welches die gleiche Signatur hat. Callbacks werden in der Reihenfolge Last-in-First-out ausgeführt.
Falls nötig, gibt es ein zusätzliches Paar von AddEnvironmentCleanupHook()
und RemoveEnvironmentCleanupHook()
-Überladungen, wobei der Bereinigungs-Hook eine Callback-Funktion verwendet. Dies kann zum Herunterfahren asynchroner Ressourcen verwendet werden, wie z. B. alle vom Add-on registrierten libuv-Handles.
Das folgende addon.cc
verwendet 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;
// Hinweis: Verlassen Sie sich in einer realen Anwendung nicht auf statische/globale Daten.
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);
}
// Initialisiere dieses Addon als kontextabhängig.
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);
}
Testen Sie in JavaScript, indem Sie Folgendes ausführen:
// test.js
require('./build/Release/addon')
Erstellung
Nachdem der Quellcode geschrieben wurde, muss er in die Binärdatei addon.node
kompiliert werden. Dazu erstellen Sie eine Datei namens binding.gyp
im obersten Verzeichnis des Projekts, die die Build-Konfiguration des Moduls in einem JSON-ähnlichen Format beschreibt. Diese Datei wird von node-gyp verwendet, einem speziell zum Kompilieren von Node.js-Addons entwickelten Tool.
{
"targets": [
{
"target_name": "addon",
"sources": ["hello.cc"]
}
]
}
Eine Version des node-gyp
-Hilfsprogramms ist als Teil von npm
gebündelt und wird mit Node.js verteilt. Diese Version wird Entwicklern nicht direkt zur Verfügung gestellt und soll nur die Verwendung des Befehls npm install
zum Kompilieren und Installieren von Addons unterstützen. Entwickler, die node-gyp
direkt verwenden möchten, können es mit dem Befehl npm install -g node-gyp
installieren. Weitere Informationen, einschließlich plattformspezifischer Anforderungen, finden Sie in den Installationsanweisungen von node-gyp
.
Nachdem die Datei binding.gyp
erstellt wurde, verwenden Sie node-gyp configure
, um die entsprechenden Projektbuilddateien für die aktuelle Plattform zu generieren. Dies erzeugt entweder eine Makefile
(auf Unix-Plattformen) oder eine vcxproj
-Datei (unter Windows) im Verzeichnis build/
.
Als Nächstes rufen Sie den Befehl node-gyp build
auf, um die kompilierte Datei addon.node
zu generieren. Diese wird in das Verzeichnis build/Release/
gelegt.
Wenn Sie npm install
verwenden, um ein Node.js-Addon zu installieren, verwendet npm seine eigene gebündelte Version von node-gyp
, um dieselbe Reihe von Aktionen auszuführen und eine kompilierte Version des Addons für die Plattform des Benutzers bei Bedarf zu generieren.
Nach dem Build kann das binäre Addon von innerhalb von Node.js verwendet werden, indem require()
auf das erstellte addon.node
-Modul verweist:
// hello.js
const addon = require('./build/Release/addon')
console.log(addon.hello())
// Gibt aus: 'world'
Da der genaue Pfad zur kompilierten Addon-Binärdatei je nach Kompilierungsmethode variieren kann (d. h. manchmal befindet er sich in ./build/Debug/
), können Addons das Paket bindings verwenden, um das kompilierte Modul zu laden.
Während die Implementierung des bindings
-Pakets ausgefeilter darin ist, wie es Addon-Module findet, verwendet sie im Wesentlichen ein try…catch
-Muster ähnlich wie:
try {
return require('./build/Release/addon.node')
} catch (err) {
return require('./build/Debug/addon.node')
}
Verlinken auf Bibliotheken, die in Node.js enthalten sind
Node.js verwendet statisch verknüpfte Bibliotheken wie V8, libuv und OpenSSL. Alle Addons müssen mit V8 verlinkt werden und können auch mit anderen Abhängigkeiten verlinkt werden. Typischerweise ist dies so einfach wie das Einfügen der entsprechenden #include <...>
-Anweisungen (z. B. #include <v8.h>
) und node-gyp
findet die entsprechenden Header automatisch. Es gibt jedoch einige Einschränkungen, die zu beachten sind:
- Wenn
node-gyp
läuft, erkennt es die spezifische Release-Version von Node.js und lädt entweder das vollständige Quell-Tarball oder nur die Header herunter. Wenn die vollständige Quelle heruntergeladen wird, haben Addons vollständigen Zugriff auf den vollständigen Satz von Node.js-Abhängigkeiten. Wenn jedoch nur die Node.js-Header heruntergeladen werden, sind nur die von Node.js exportierten Symbole verfügbar. node-gyp
kann mit dem Flag--nodedir
ausgeführt werden, das auf ein lokales Node.js-Quellbild zeigt. Mit dieser Option hat das Addon Zugriff auf den vollständigen Satz von Abhängigkeiten.
Laden von Addons mit require()
Die Dateinamenerweiterung der kompilierten Addon-Binärdatei ist .node
(im Gegensatz zu .dll
oder .so
). Die Funktion require()
ist so geschrieben, dass sie nach Dateien mit der Erweiterung .node
sucht und diese als dynamisch verknüpfte Bibliotheken initialisiert.
Beim Aufruf von require()
kann die Erweiterung .node
normalerweise weggelassen werden, und Node.js findet und initialisiert das Addon trotzdem. Eine Einschränkung ist jedoch, dass Node.js zuerst versucht, Module oder JavaScript-Dateien zu finden und zu laden, die denselben Basisnamen haben. Wenn sich beispielsweise eine Datei addon.js
im selben Verzeichnis wie die Binärdatei addon.node
befindet, dann gibt require('addon')
der Datei addon.js
den Vorrang und lädt sie stattdessen.
Native Abstraktionen für Node.js
Jedes der in diesem Dokument dargestellten Beispiele verwendet direkt die Node.js- und V8-APIs für die Implementierung von Addons. Die V8-API kann sich und hat sich von einem V8-Release zum nächsten (und von einem Haupt-Node.js-Release zum nächsten) dramatisch verändert. Bei jeder Änderung müssen Addons möglicherweise aktualisiert und neu kompiliert werden, um weiterhin funktionieren zu können. Der Node.js-Release-Zeitplan soll die Häufigkeit und die Auswirkungen solcher Änderungen minimieren, aber Node.js kann wenig tun, um die Stabilität der V8-APIs zu gewährleisten.
Die Native Abstractions for Node.js (oder nan
) bieten eine Reihe von Tools, deren Verwendung Addon-Entwicklern empfohlen wird, um die Kompatibilität zwischen vergangenen und zukünftigen Versionen von V8 und Node.js zu gewährleisten. Siehe die nan
-Beispiele für eine Veranschaulichung der Verwendung.
Node-API
[Stabil: 2 - Stabil]
Stabil: 2 Stabilität: 2 - Stabil
Node-API ist eine API zum Erstellen von nativen Addons. Sie ist unabhängig von der zugrunde liegenden JavaScript-Laufzeitumgebung (z. B. V8) und wird als Teil von Node.js selbst gewartet. Diese API wird über verschiedene Node.js-Versionen hinweg stabil bezüglich der Application Binary Interface (ABI) sein. Sie soll Addons vor Änderungen in der zugrunde liegenden JavaScript-Engine schützen und ermöglichen, dass Module, die für eine Version kompiliert wurden, ohne Neukompilierung auf späteren Versionen von Node.js ausgeführt werden können. Addons werden mit dem gleichen Ansatz/den gleichen Tools erstellt/verpackt, die in diesem Dokument beschrieben sind (node-gyp usw.). Der einzige Unterschied besteht in den APIs, die vom nativen Code verwendet werden. Anstatt die V8- oder Native Abstractions for Node.js-APIs zu verwenden, werden die in der Node-API verfügbaren Funktionen verwendet.
Das Erstellen und Verwalten eines Addons, das von der von Node-API bereitgestellten ABI-Stabilität profitiert, bringt bestimmte Implementierungsaspekte mit sich.
Um Node-API im obigen "Hello World"-Beispiel zu verwenden, ersetzen Sie den Inhalt von hello.cc
durch Folgendes. Alle anderen Anweisungen bleiben gleich.
// hello.cc mit 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
Die verfügbaren Funktionen und deren Verwendung werden in C/C++-Addons mit Node-API beschrieben.
Addon-Beispiele
Im Folgenden finden Sie einige Beispiel-Addons, die Entwicklern den Einstieg erleichtern sollen. Die Beispiele verwenden die V8-APIs. Weitere Informationen zu den verschiedenen V8-Aufrufen finden Sie in der Online-V8-Referenz, und im Embedder's Guide von V8 erhalten Sie Erklärungen zu verschiedenen verwendeten Konzepten wie Handles, Scopes, Funktionsvorlagen usw.
Jedes dieser Beispiele verwendet die folgende binding.gyp
-Datei:
{
"targets": [
{
"target_name": "addon",
"sources": ["addon.cc"]
}
]
}
Wenn mehr als eine .cc
-Datei vorhanden ist, fügen Sie einfach den zusätzlichen Dateinamen dem sources
-Array hinzu:
"sources": ["addon.cc", "myexample.cc"]
Sobald die binding.gyp
-Datei fertig ist, können die Beispiel-Addons mit node-gyp
konfiguriert und erstellt werden:
node-gyp configure build
Funktionsargumente
Addons stellen in der Regel Objekte und Funktionen bereit, auf die von JavaScript zugegriffen werden kann, das in Node.js ausgeführt wird. Wenn Funktionen von JavaScript aufgerufen werden, müssen die Eingabeargumente und der Rückgabewert auf den C/C++-Code abgebildet und von diesem abgebildet werden.
Das folgende Beispiel zeigt, wie Funktionsargumente gelesen werden, die von JavaScript übergeben werden, und wie ein Ergebnis zurückgegeben wird:
// 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;
// Dies ist die Implementierung der "add"-Methode
// Eingabeargumente werden über die
// const FunctionCallbackInfo<Value>& args Struktur übergeben
void Add(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
// Überprüfen Sie die Anzahl der übergebenen Argumente.
if (args.Length() < 2) {
// Wirft einen Fehler, der an JavaScript zurückgegeben wird
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate,
"Falsche Anzahl an Argumenten").ToLocalChecked()));
return;
}
// Überprüfen Sie die Argumenttypen
if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate,
"Falsche Argumente").ToLocalChecked()));
return;
}
// Führen Sie die Operation aus
double value =
args[0].As<Number>()->Value() + args[1].As<Number>()->Value();
Local<Number> num = Number::New(isolate, value);
// Setzen Sie den Rückgabewert (mit dem übergebenen
// 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
Nach dem Kompilieren kann das Beispiel-Addon in Node.js benötigt und verwendet werden:
// test.js
const addon = require('./build/Release/addon')
console.log('Das sollte acht sein:', addon.add(3, 5))
Callbacks
Es ist gängige Praxis in Addons, JavaScript-Funktionen an eine C++-Funktion zu übergeben und diese von dort aus auszuführen. Das folgende Beispiel veranschaulicht, wie solche Callbacks aufgerufen werden:
// 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
Dieses Beispiel verwendet eine Zwei-Argument-Form von Init()
, die das vollständige module
-Objekt als zweites Argument erhält. Dies ermöglicht es dem Addon, exports
vollständig mit einer einzelnen Funktion zu überschreiben, anstatt die Funktion als Eigenschaft von exports
hinzuzufügen.
Zum Testen führen Sie das folgende JavaScript aus:
// test.js
const addon = require('./build/Release/addon')
addon(msg => {
console.log(msg)
// Gibt aus: 'hello world'
})
In diesem Beispiel wird die Callback-Funktion synchron aufgerufen.
Objekt-Factory
Addons können neue Objekte innerhalb einer C++-Funktion erstellen und zurückgeben, wie im folgenden Beispiel gezeigt. Ein Objekt wird erstellt und mit einer Eigenschaft msg
zurückgegeben, die die an createObject()
übergebene Zeichenkette wiederholt:
// 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
Zum Testen in JavaScript:
// test.js
const addon = require('./build/Release/addon')
const obj1 = addon('hello')
const obj2 = addon('world')
console.log(obj1.msg, obj2.msg)
// Gibt aus: 'hello world'
Funktionsfabrik
Ein weiteres häufiges Szenario ist die Erstellung von JavaScript-Funktionen, die C++-Funktionen umschließen und diese an JavaScript zurückgeben:
// 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
Zum Testen:
// test.js
const addon = require('./build/Release/addon')
const fn = addon()
console.log(fn())
// Gibt aus: 'hello world'
Umschließen von C++-Objekten
Es ist auch möglich, C++-Objekte/Klassen so zu umschließen, dass neue Instanzen mit dem JavaScript new
-Operator erstellt werden können:
// 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
Dann, in myobject.h
, erbt die Wrapper-Klasse von 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
In myobject.cc
werden die verschiedenen Methoden implementiert, die verfügbar gemacht werden sollen. Im folgenden Code wird die Methode plusOne()
verfügbar gemacht, indem sie dem Prototyp des Konstruktors hinzugefügt wird:
// 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 Feld für MyObject::New()
Local<Object> addon_data =
addon_data_tpl->NewInstance(context).ToLocalChecked();
// Konstruktortemplate vorbereiten
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New, addon_data);
tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
// Prototyp
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()) {
// Als Konstruktor aufgerufen: `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 {
// Als einfache Funktion aufgerufen `MyObject(...)`, in Konstruktoraufruf umwandeln.
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
Um dieses Beispiel zu erstellen, muss die Datei myobject.cc
der Datei binding.gyp
hinzugefügt werden:
{
"targets": [
{
"target_name": "addon",
"sources": ["addon.cc", "myobject.cc"]
}
]
}
Testen Sie es mit:
// test.js
const addon = require('./build/Release/addon')
const obj = new addon.MyObject(10)
console.log(obj.plusOne())
// Gibt aus: 11
console.log(obj.plusOne())
// Gibt aus: 12
console.log(obj.plusOne())
// Gibt aus: 13
Der Destruktor für ein Wrapper-Objekt wird ausgeführt, wenn das Objekt vom Garbage Collector freigegeben wird. Für Destruktortests gibt es Befehlszeilenflags, mit denen die Garbage Collection erzwungen werden kann. Diese Flags werden von der zugrunde liegenden V8 JavaScript-Engine bereitgestellt. Sie können sich jederzeit ändern oder entfernt werden. Sie sind weder von Node.js noch von V8 dokumentiert und sollten niemals außerhalb von Tests verwendet werden.
Während des Herunterfahrens des Prozesses oder von Worker-Threads werden Destruktoren nicht von der JS-Engine aufgerufen. Daher ist es die Verantwortung des Benutzers, diese Objekte zu verfolgen und eine ordnungsgemäße Zerstörung sicherzustellen, um Ressourcenlecks zu vermeiden.
Fabrik für verkapselte Objekte
Alternativ ist es möglich, ein Factory-Pattern zu verwenden, um die explizite Erstellung von Objektinstanzen mit dem JavaScript new
-Operator zu vermeiden:
const obj = addon.createObject()
// anstatt:
// const obj = new addon.Object();
Zuerst wird die Methode createObject()
in addon.cc
implementiert:
// 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
In myobject.h
wird die statische Methode NewInstance()
hinzugefügt, um die Instanziierung des Objekts zu behandeln. Diese Methode ersetzt die Verwendung von new
in 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
Die Implementierung in myobject.cc
ähnelt dem vorherigen Beispiel:
// 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;
// Warnung! Dies ist nicht threadsicher, dieses Addon kann nicht für Worker-Threads verwendet werden.
Global<Function> MyObject::constructor;
MyObject::MyObject(double value) : value_(value) {
}
MyObject::~MyObject() {
}
void MyObject::Init(Isolate* isolate) {
// Vorbereitung der Konstruktorvorlage
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()) {
// Aufgerufen als Konstruktor: `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 {
// Aufgerufen als einfache Funktion `MyObject(...)`, in Konstruktoraufruf umwandeln.
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
Um dieses Beispiel zu erstellen, muss die Datei myobject.cc
erneut zur binding.gyp
hinzugefügt werden:
{
"targets": [
{
"target_name": "addon",
"sources": ["addon.cc", "myobject.cc"]
}
]
}
Testen Sie es mit:
// test.js
const createObject = require('./build/Release/addon')
const obj = createObject(10)
console.log(obj.plusOne())
// Gibt aus: 11
console.log(obj.plusOne())
// Gibt aus: 12
console.log(obj.plusOne())
// Gibt aus: 13
const obj2 = createObject(20)
console.log(obj2.plusOne())
// Gibt aus: 21
console.log(obj2.plusOne())
// Gibt aus: 22
console.log(obj2.plusOne())
// Gibt aus: 23
Weitergabe verpackter Objekte
Zusätzlich zum Verpacken und Zurückgeben von C++-Objekten ist es möglich, verpackte Objekte weiterzugeben, indem sie mit der Node.js-Hilfsfunktion node::ObjectWrap::Unwrap
ausgepackt werden. Das folgende Beispiel zeigt eine Funktion add()
, die zwei MyObject
-Objekte als Eingabeargumente entgegennehmen kann:
// 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
In myobject.h
wird eine neue öffentliche Methode hinzugefügt, um nach dem Auspacken des Objekts auf private Werte zuzugreifen.
// 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
Die Implementierung von myobject.cc
ähnelt der vorherigen Version:
// 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;
// Warnung! Dies ist nicht threadsicher, dieses Addon kann nicht für Worker-Threads verwendet werden.
Global<Function> MyObject::constructor;
MyObject::MyObject(double value) : value_(value) {
}
MyObject::~MyObject() {
}
void MyObject::Init(Isolate* isolate) {
// Vorbereitung der Konstruktorvorlage
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()) {
// Als Konstruktor aufgerufen: `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 {
// Als einfache Funktion `MyObject(...)` aufgerufen, in Konstruktoraufruf umwandeln.
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
Testen Sie es mit:
// 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)
// Gibt aus: 30