Skip to content

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:

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:

js
module.exports.hello = () => 'world'

Erstellen Sie zunächst die Datei hello.cc:

C++
// 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:

C++
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:

C++
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, und
  • Local\<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 auf DeleteInstance(). 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 an v8::FunctionTemplate::New() oder v8::Function::New() übergeben, wodurch die nativen JavaScript-Funktionen erstellt werden. Der dritte Parameter von v8::FunctionTemplate::New() oder v8::Function::New() akzeptiert das v8::External und macht es im nativen Callback über die Methode v8::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:

C++
#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.0Bereinigungs-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:

C++
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:

C++
// 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:

js
// 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.

json
{
  "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:

js
// 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:

js
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.

C++
// 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:

json
{
  "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:

json
"sources": ["addon.cc", "myexample.cc"]

Sobald die binding.gyp-Datei fertig ist, können die Beispiel-Addons mit node-gyp konfiguriert und erstellt werden:

bash
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:

C++
// 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:

js
// 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:

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

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:

js
// 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:

C++
// 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:

js
// 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:

C++
// 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:

js
// 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:

C++
// 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:

C++
// 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:

C++
// 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:

json
{
  "targets": [
    {
      "target_name": "addon",
      "sources": ["addon.cc", "myobject.cc"]
    }
  ]
}

Testen Sie es mit:

js
// 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:

js
const obj = addon.createObject()
// anstatt:
// const obj = new addon.Object();

Zuerst wird die Methode createObject() in addon.cc implementiert:

C++
// 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:

C++
// 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:

C++
// 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:

json
{
  "targets": [
    {
      "target_name": "addon",
      "sources": ["addon.cc", "myobject.cc"]
    }
  ]
}

Testen Sie es mit:

js
// 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:

C++
// 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.

C++
// 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:

C++
// 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:

js
// 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