Skip to content

Extensions C++

Les extensions sont des objets partagés liés dynamiquement écrits en C++. La fonction require() peut charger des extensions comme des modules Node.js ordinaires. Les extensions fournissent une interface entre JavaScript et les bibliothèques C/C++.

Il existe trois options pour implémenter des extensions :

À moins qu'il n'y ait un besoin d'accès direct à des fonctionnalités non exposées par Node-API, utilisez Node-API. Consultez Extensions C/C++ avec Node-API pour plus d'informations sur Node-API.

Lorsque Node-API n'est pas utilisé, l'implémentation des extensions devient plus complexe, nécessitant la connaissance de plusieurs composants et API :

  • V8: la bibliothèque C++ que Node.js utilise pour fournir l'implémentation JavaScript. Elle fournit les mécanismes pour créer des objets, appeler des fonctions, etc. L'API de V8 est principalement documentée dans le fichier d'en-tête v8.h (deps/v8/include/v8.h dans l'arborescence source de Node.js), et est également disponible en ligne.
  • libuv: La bibliothèque C qui implémente la boucle d'événements Node.js, ses threads de travail et tous les comportements asynchrones de la plateforme. Elle sert également de bibliothèque d'abstraction multiplateforme, offrant un accès facile et de type POSIX sur tous les principaux systèmes d'exploitation à de nombreuses tâches système courantes, telles que l'interaction avec le système de fichiers, les sockets, les temporisateurs et les événements système. libuv fournit également une abstraction de threading similaire aux threads POSIX pour les extensions asynchrones plus sophistiquées qui doivent aller au-delà de la boucle d'événements standard. Les auteurs d'extensions doivent éviter de bloquer la boucle d'événements avec des E/S ou d'autres tâches gourmandes en temps en déchargeant le travail via libuv vers des opérations système non bloquantes, des threads de travail ou une utilisation personnalisée des threads libuv.
  • Bibliothèques Node.js internes : Node.js lui-même exporte des API C++ que les extensions peuvent utiliser, la plus importante étant la classe node::ObjectWrap.
  • Autres bibliothèques liées statiquement (y compris OpenSSL) : Ces autres bibliothèques sont situées dans le répertoire deps/ de l'arborescence source de Node.js. Seuls les symboles libuv, OpenSSL, V8 et zlib sont intentionnellement réexportés par Node.js et peuvent être utilisés à divers degrés par les extensions. Voir Liaison aux bibliothèques incluses avec Node.js pour plus d'informations.

Tous les exemples suivants sont disponibles en téléchargement et peuvent servir de point de départ pour une extension.

Bonjour le monde

Cet exemple "Bonjour le monde" est un simple module d'extension, écrit en C++, qui est l'équivalent du code JavaScript suivant :

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

Tout d'abord, créez le fichier 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

Tous les modules d'extension Node.js doivent exporter une fonction d'initialisation suivant le modèle :

C++
void Initialize(Local<Object> exports);
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

Il n'y a pas de point-virgule après NODE_MODULE car ce n'est pas une fonction (voir node.h).

Le module_name doit correspondre au nom de fichier du binaire final (à l'exclusion du suffixe .node).

Dans l'exemple hello.cc, la fonction d'initialisation est donc Initialize et le nom du module d'extension est addon.

Lors de la compilation des modules d'extension avec node-gyp, l'utilisation de la macro NODE_GYP_MODULE_NAME comme premier paramètre de NODE_MODULE() garantira que le nom du binaire final sera passé à NODE_MODULE().

Les modules d'extension définis avec NODE_MODULE() ne peuvent pas être chargés dans plusieurs contextes ou plusieurs threads simultanément.

Modules d'extension contextuels

Il existe des environnements dans lesquels les modules d'extension Node.js peuvent avoir besoin d'être chargés plusieurs fois dans plusieurs contextes. Par exemple, l'environnement d'exécution Electron exécute plusieurs instances de Node.js dans un seul processus. Chaque instance aura son propre cache require(), et donc chaque instance aura besoin d'un module d'extension natif pour se comporter correctement lorsqu'il est chargé via require(). Cela signifie que le module d'extension doit prendre en charge plusieurs initialisations.

Un module d'extension contextuel peut être construit en utilisant la macro NODE_MODULE_INITIALIZER, qui se développe en le nom d'une fonction que Node.js s'attend à trouver lorsqu'il charge un module d'extension. Un module d'extension peut ainsi être initialisé comme dans l'exemple suivant :

C++
using namespace v8;

extern "C" NODE_MODULE_EXPORT void
NODE_MODULE_INITIALIZER(Local<Object> exports,
                        Local<Value> module,
                        Local<Context> context) {
  /* Effectuer les étapes d'initialisation du module d'extension ici. */
}

Une autre option consiste à utiliser la macro NODE_MODULE_INIT(), qui construira également un module d'extension contextuel. Contrairement à NODE_MODULE(), qui est utilisé pour construire un module d'extension autour d'une fonction d'initialisation de module d'extension donnée, NODE_MODULE_INIT() sert de déclaration d'un tel initialisateur à suivre d'un corps de fonction.

Les trois variables suivantes peuvent être utilisées dans le corps de la fonction suivant une invocation de NODE_MODULE_INIT() :

  • Local\<Object\> exports,
  • Local\<Value\> module, et
  • Local\<Context\> context

La construction d'un module d'extension contextuel nécessite une gestion minutieuse des données statiques globales pour garantir la stabilité et l'exactitude. Étant donné que le module d'extension peut être chargé plusieurs fois, potentiellement même à partir de différents threads, toutes les données statiques globales stockées dans le module d'extension doivent être correctement protégées et ne doivent pas contenir de références persistantes à des objets JavaScript. La raison en est que les objets JavaScript ne sont valides que dans un seul contexte et risquent de provoquer un plantage lorsqu'ils sont accessibles à partir du mauvais contexte ou d'un thread différent de celui sur lequel ils ont été créés.

Le module d'extension contextuel peut être structuré pour éviter les données statiques globales en effectuant les étapes suivantes :

  • Définir une classe qui contiendra les données par instance de module d'extension et qui a un membre statique de la forme
  • Allouer en tas une instance de cette classe dans l'initialiseur de module d'extension. Cela peut être réalisé à l'aide du mot clé new.
  • Appeler node::AddEnvironmentCleanupHook(), en lui passant l'instance créée ci-dessus et un pointeur vers DeleteInstance(). Cela garantira que l'instance est supprimée lorsque l'environnement est détruit.
  • Stocker l'instance de la classe dans un v8::External, et
  • Passer le v8::External à toutes les méthodes exposées à JavaScript en le passant à v8::FunctionTemplate::New() ou v8::Function::New() qui crée les fonctions JavaScript natives. Le troisième paramètre de v8::FunctionTemplate::New() ou v8::Function::New() accepte le v8::External et le rend disponible dans le rappel natif à l'aide de la méthode v8::FunctionCallbackInfo::Data().

Cela garantira que les données par instance de module d'extension atteignent chaque liaison qui peut être appelée à partir de JavaScript. Les données par instance de module d'extension doivent également être transmises à tous les rappels asynchrones que le module d'extension peut créer.

L'exemple suivant illustre l'implémentation d'un module d'extension contextuel :

C++
#include <node.h>

using namespace v8;

class AddonData {
 public:
  explicit AddonData(Isolate* isolate):
      call_count(0) {
    // Garantir que ces données par instance de module d'extension sont supprimées lors du nettoyage de l'environnement.
    node::AddEnvironmentCleanupHook(isolate, DeleteInstance, this);
  }

  // Données par module d'extension.
  int call_count;

  static void DeleteInstance(void* data) {
    delete static_cast<AddonData*>(data);
  }
};

static void Method(const v8::FunctionCallbackInfo<v8::Value>& info) {
  // Récupérer les données par instance de module d'extension.
  AddonData* data =
      reinterpret_cast<AddonData*>(info.Data().As<External>()->Value());
  data->call_count++;
  info.GetReturnValue().Set((double)data->call_count);
}

// Initialiser ce module d'extension pour qu'il soit contextuel.
NODE_MODULE_INIT(/* exports, module, context */) {
  Isolate* isolate = context->GetIsolate();

  // Créer une nouvelle instance de `AddonData` pour cette instance du module d'extension et
  // lier son cycle de vie à celui de l'environnement Node.js.
  AddonData* data = new AddonData(isolate);

  // Envelopper les données dans un `v8::External` afin de pouvoir les passer à la méthode que nous
  // exposons.
  Local<External> external = External::New(isolate, data);

  // Exposer la méthode `Method` à JavaScript, et s'assurer qu'elle reçoit les
  // données par instance de module d'extension que nous avons créées ci-dessus en passant `external` comme
  // troisième paramètre au constructeur `FunctionTemplate`.
  exports->Set(context,
               String::NewFromUtf8(isolate, "method").ToLocalChecked(),
               FunctionTemplate::New(isolate, Method, external)
                  ->GetFunction(context).ToLocalChecked()).FromJust();
}

Prise en charge des Workers

[Historique]

VersionModifications
v14.8.0, v12.19.0Les hooks de nettoyage peuvent désormais être asynchrones.

Pour pouvoir être chargé à partir de plusieurs environnements Node.js, tels qu'un thread principal et un thread Worker, un add-on doit :

  • Être un add-on Node-API, ou
  • Être déclaré comme context-aware en utilisant NODE_MODULE_INIT() comme décrit ci-dessus.

Pour prendre en charge les threads Worker, les add-ons doivent nettoyer toutes les ressources qu'ils ont pu allouer lorsqu'un tel thread se termine. Cela peut être réalisé grâce à l'utilisation de la fonction AddEnvironmentCleanupHook() :

C++
void AddEnvironmentCleanupHook(v8::Isolate* isolate,
                               void (*fun)(void* arg),
                               void* arg);

Cette fonction ajoute un hook qui sera exécuté avant qu'une instance Node.js donnée ne s'arrête. Si nécessaire, de tels hooks peuvent être supprimés avant leur exécution à l'aide de RemoveEnvironmentCleanupHook(), qui a la même signature. Les callbacks sont exécutés dans l'ordre dernier entré, premier sorti.

Si nécessaire, il existe une paire supplémentaire de fonctions AddEnvironmentCleanupHook() et RemoveEnvironmentCleanupHook(), où le hook de nettoyage prend une fonction de rappel. Cela peut être utilisé pour arrêter les ressources asynchrones, telles que les handles libuv enregistrés par l'addon.

Le fichier addon.cc suivant utilise 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;

// Note : Dans une application réelle, ne vous fiez pas aux données statiques/globales.
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);
}

// Initialiser cet addon pour qu'il soit context-aware.
NODE_MODULE_INIT(/* exports, module, context */) {
  Isolate* isolate = context->GetIsolate();

  AddEnvironmentCleanupHook(isolate, sanity_check, nullptr);
  AddEnvironmentCleanupHook(isolate, cleanup_cb2, cookie);
  AddEnvironmentCleanupHook(isolate, cleanup_cb1, isolate);
}

Testez en JavaScript en exécutant :

js
// test.js
require('./build/Release/addon')

Compilation

Une fois le code source écrit, il doit être compilé en fichier binaire addon.node. Pour ce faire, créez un fichier nommé binding.gyp au niveau supérieur du projet, décrivant la configuration de compilation du module en utilisant un format similaire au JSON. Ce fichier est utilisé par node-gyp, un outil écrit spécifiquement pour compiler les addons Node.js.

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

Une version de l'utilitaire node-gyp est intégrée et distribuée avec Node.js dans le cadre de npm. Cette version n'est pas directement disponible pour les développeurs et est uniquement destinée à supporter la capacité d'utiliser la commande npm install pour compiler et installer les addons. Les développeurs qui souhaitent utiliser node-gyp directement peuvent l'installer en utilisant la commande npm install -g node-gyp. Consultez les instructions d'installation de node-gyp pour plus d'informations, y compris les exigences spécifiques à la plateforme.

Une fois le fichier binding.gyp créé, utilisez node-gyp configure pour générer les fichiers de projet de compilation appropriés pour la plateforme actuelle. Cela générera soit un fichier Makefile (sur les plateformes Unix) soit un fichier vcxproj (sur Windows) dans le répertoire build/.

Ensuite, invoquez la commande node-gyp build pour générer le fichier compilé addon.node. Celui-ci sera placé dans le répertoire build/Release/.

Lorsque vous utilisez npm install pour installer un addon Node.js, npm utilise sa propre version intégrée de node-gyp pour effectuer ce même ensemble d'actions, générant une version compilée de l'addon pour la plateforme de l'utilisateur à la demande.

Une fois compilé, l'addon binaire peut être utilisé depuis Node.js en pointant require() vers le module addon.node compilé :

js
// hello.js
const addon = require('./build/Release/addon')

console.log(addon.hello())
// Affiche : 'world'

Étant donné que le chemin exact vers le binaire addon compilé peut varier en fonction de la manière dont il est compilé (c'est-à-dire qu'il peut parfois se trouver dans ./build/Debug/), les addons peuvent utiliser le paquet bindings pour charger le module compilé.

Bien que l'implémentation du paquet bindings soit plus sophistiquée dans la manière dont il localise les modules addon, elle utilise essentiellement un motif try…catch similaire à :

js
try {
  return require('./build/Release/addon.node')
} catch (err) {
  return require('./build/Debug/addon.node')
}

Liaison aux bibliothèques incluses avec Node.js

Node.js utilise des bibliothèques liées statiquement telles que V8, libuv et OpenSSL. Tous les modules complémentaires doivent être liés à V8 et peuvent également être liés à toutes les autres dépendances. En général, cela est aussi simple que d'inclure les instructions #include <...> appropriées (par exemple, #include <v8.h>) et node-gyp localisera automatiquement les en-têtes appropriés. Cependant, il y a quelques mises en garde à connaître :

  • Lorsque node-gyp s'exécute, il détectera la version de publication spécifique de Node.js et téléchargera soit l'intégralité de l'archive source, soit uniquement les en-têtes. Si la source complète est téléchargée, les modules complémentaires auront un accès complet à l'ensemble des dépendances de Node.js. Cependant, si seuls les en-têtes Node.js sont téléchargés, seuls les symboles exportés par Node.js seront disponibles.
  • node-gyp peut être exécuté à l'aide de l'indicateur --nodedir pointant vers une image source Node.js locale. En utilisant cette option, le module complémentaire aura accès à l'ensemble complet des dépendances.

Chargement des modules complémentaires à l'aide de require()

L'extension de nom de fichier du fichier binaire du module complémentaire compilé est .node (contrairement à .dll ou .so). La fonction require() est écrite pour rechercher les fichiers avec l'extension .node et les initialiser en tant que bibliothèques liées dynamiquement.

Lors de l'appel de require(), l'extension .node peut généralement être omise et Node.js trouvera et initialisera toujours le module complémentaire. Cependant, une mise en garde est que Node.js tentera d'abord de localiser et de charger les modules ou les fichiers JavaScript qui partagent le même nom de base. Par exemple, s'il existe un fichier addon.js dans le même répertoire que le fichier binaire addon.node, alors require('addon') donnera la priorité au fichier addon.js et le chargera à la place.

Abstractions natives pour Node.js

Chacun des exemples illustrés dans ce document utilise directement les API Node.js et V8 pour implémenter des modules complémentaires. L'API V8 peut avoir changé radicalement d'une version V8 à l'autre (et d'une version majeure de Node.js à l'autre). À chaque modification, les modules complémentaires peuvent avoir besoin d'être mis à jour et recompilés pour continuer à fonctionner. Le calendrier de publication de Node.js est conçu pour minimiser la fréquence et l'impact de ces modifications, mais Node.js peut peu faire pour assurer la stabilité des API V8.

Les Abstractions natives pour Node.js (ou nan) fournissent un ensemble d'outils que les développeurs de modules complémentaires sont invités à utiliser pour maintenir la compatibilité entre les versions passées et futures de V8 et Node.js. Consultez les exemples nan pour une illustration de son utilisation.

Node-API

[Stable : 2 - Stable]

Stable : 2 Stability : 2 - Stable

Node-API est une API pour construire des modules natifs. Elle est indépendante du moteur JavaScript sous-jacent (par exemple, V8) et est maintenue en tant que partie intégrante de Node.js. Cette API sera stable au niveau de l'Application Binary Interface (ABI) entre les versions de Node.js. Elle vise à isoler les modules natifs des changements dans le moteur JavaScript sous-jacent et à permettre aux modules compilés pour une version de s'exécuter sur les versions ultérieures de Node.js sans recompilation. Les modules natifs sont construits/empaquetés avec la même approche/les mêmes outils décrits dans ce document (node-gyp, etc.). La seule différence réside dans l'ensemble des API utilisées par le code natif. Au lieu d'utiliser les API V8 ou Native Abstractions for Node.js, les fonctions disponibles dans Node-API sont utilisées.

La création et la maintenance d'un module natif qui bénéficie de la stabilité ABI fournie par Node-API comportent certaines considérations d'implémentation.

Pour utiliser Node-API dans l'exemple "Hello world" ci-dessus, remplacez le contenu de hello.cc par ce qui suit. Toutes les autres instructions restent les mêmes.

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

Les fonctions disponibles et leur utilisation sont documentées dans Modules C/C++ avec Node-API.

Exemples d'extensions

Voici quelques exemples d'extensions destinés à aider les développeurs à démarrer. Les exemples utilisent les API V8. Consultez la référence V8 en ligne pour obtenir de l'aide sur les différents appels V8, et le Guide de l'intégrateur de V8 pour une explication de plusieurs concepts utilisés tels que les handles, les scopes, les modèles de fonctions, etc.

Chacun de ces exemples utilise le fichier binding.gyp suivant :

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

Dans les cas où il y a plus d'un fichier .cc, il suffit d'ajouter le nom de fichier supplémentaire au tableau sources :

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

Une fois le fichier binding.gyp prêt, les extensions d'exemple peuvent être configurées et compilées à l'aide de node-gyp :

bash
node-gyp configure build

Arguments de fonction

Les extensions exposent généralement des objets et des fonctions accessibles à partir de JavaScript exécuté dans Node.js. Lorsque les fonctions sont appelées à partir de JavaScript, les arguments d'entrée et la valeur de retour doivent être mappés vers et depuis le code C/C++.

L'exemple suivant illustre comment lire les arguments de fonction passés depuis JavaScript et comment renvoyer un résultat :

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;

// Ceci est l'implémentation de la méthode "add"
// Les arguments d'entrée sont passés à l'aide de la structure
// const FunctionCallbackInfo<Value>& args
void Add(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  // Vérification du nombre d'arguments passés.
  if (args.Length() < 2) {
    // Lance une erreur qui est renvoyée à JavaScript
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate,
                            "Mauvais nombre d'arguments").ToLocalChecked()));
    return;
  }

  // Vérification des types d'arguments
  if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
    isolate->ThrowException(Exception::TypeError(
        String::NewFromUtf8(isolate,
                            "Arguments incorrects").ToLocalChecked()));
    return;
  }

  // Exécution de l'opération
  double value =
      args[0].As<Number>()->Value() + args[1].As<Number>()->Value();
  Local<Number> num = Number::New(isolate, value);

  // Définition de la valeur de retour (à l'aide de la structure passée
  // 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

Une fois compilée, l'extension d'exemple peut être requise et utilisée depuis Node.js :

js
// test.js
const addon = require('./build/Release/addon')

console.log('Ceci devrait être huit :', addon.add(3, 5))

Callbacks

Il est courant, dans les extensions, de passer des fonctions JavaScript à une fonction C++ et de les exécuter à partir de celle-ci. L'exemple suivant illustre comment appeler de tels callbacks :

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

Cet exemple utilise une forme à deux arguments de Init() qui reçoit l'objet module complet comme deuxième argument. Cela permet à l'extension de complètement écraser exports avec une seule fonction au lieu d'ajouter la fonction comme propriété de exports.

Pour le tester, exécutez le JavaScript suivant :

js
// test.js
const addon = require('./build/Release/addon')

addon(msg => {
  console.log(msg)
  // Affiche : 'hello world'
})

Dans cet exemple, la fonction callback est appelée de manière synchrone.

Factory d'objets

Les extensions peuvent créer et renvoyer de nouveaux objets depuis une fonction C++, comme illustré dans l'exemple suivant. Un objet est créé et renvoyé avec une propriété msg qui fait écho à la chaîne passée à createObject() :

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

Pour le tester en JavaScript :

js
// test.js
const addon = require('./build/Release/addon')

const obj1 = addon('hello')
const obj2 = addon('world')
console.log(obj1.msg, obj2.msg)
// Affiche : 'hello world'

Fabrique de fonctions

Un autre scénario courant consiste à créer des fonctions JavaScript qui encapsulent des fonctions C++ et à les renvoyer à JavaScript :

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();

  // omettre ceci pour le rendre anonyme
  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

Pour tester :

js
// test.js
const addon = require('./build/Release/addon')

const fn = addon()
console.log(fn())
// Affiche : 'hello world'

Encapsulation d'objets C++

Il est également possible d'encapsuler des objets/classes C++ de manière à permettre la création de nouvelles instances à l'aide de l'opérateur new de JavaScript :

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

Ensuite, dans myobject.h, la classe d'encapsulation hérite de 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

Dans myobject.cc, implémentez les différentes méthodes à exposer. Dans le code suivant, la méthode plusOne() est exposée en l'ajoutant au prototype du constructeur :

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 champ pour MyObject::New()
  Local<Object> addon_data =
      addon_data_tpl->NewInstance(context).ToLocalChecked();

  // Préparation du modèle de constructeur
  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()) {
    // Appelée comme constructeur : `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 {
    // Appelée comme fonction simple `MyObject(...)`, transformer en appel de constructeur.
    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

Pour compiler cet exemple, le fichier myobject.cc doit être ajouté à binding.gyp :

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

Testez avec :

js
// test.js
const addon = require('./build/Release/addon')

const obj = new addon.MyObject(10)
console.log(obj.plusOne())
// Affiche : 11
console.log(obj.plusOne())
// Affiche : 12
console.log(obj.plusOne())
// Affiche : 13

Le destructeur d'un objet d'encapsulation s'exécutera lorsque l'objet sera collecté par le garbage collector. Pour les tests de destructeur, il existe des drapeaux de ligne de commande qui permettent de forcer le garbage collector. Ces drapeaux sont fournis par le moteur JavaScript V8 sous-jacent. Ils sont sujets à modification ou suppression à tout moment. Ils ne sont pas documentés par Node.js ou V8, et ne doivent jamais être utilisés en dehors des tests.

Lors de l'arrêt du processus ou des threads de travail, les destructeurs ne sont pas appelés par le moteur JS. Par conséquent, il est de la responsabilité de l'utilisateur de suivre ces objets et de garantir une destruction appropriée afin d'éviter les fuites de ressources.

Fabrique d'objets encapsulés

Alternativement, il est possible d'utiliser un patron de fabrique pour éviter de créer explicitement des instances d'objets en utilisant l'opérateur new de JavaScript :

js
const obj = addon.createObject()
// au lieu de :
// const obj = new addon.Object();

Premièrement, la méthode createObject() est implémentée dans addon.cc :

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

Dans myobject.h, la méthode statique NewInstance() est ajoutée pour gérer l'instanciation de l'objet. Cette méthode remplace l'utilisation de new en 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

L'implémentation dans myobject.cc est similaire à l'exemple précédent :

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;

// Avertissement ! Ceci n'est pas thread-safe, cet addon ne peut pas être utilisé pour les threads worker.
Global<Function> MyObject::constructor;

MyObject::MyObject(double value) : value_(value) {
}

MyObject::~MyObject() {
}

void MyObject::Init(Isolate* isolate) {
  // Préparer le modèle de constructeur
  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()) {
    // Appelée comme constructeur : `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 {
    // Appelée comme fonction simple `MyObject(...)`, transformer en appel de constructeur.
    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

Encore une fois, pour construire cet exemple, le fichier myobject.cc doit être ajouté à binding.gyp :

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

Testez-le avec :

js
// test.js
const createObject = require('./build/Release/addon')

const obj = createObject(10)
console.log(obj.plusOne())
// Affiche : 11
console.log(obj.plusOne())
// Affiche : 12
console.log(obj.plusOne())
// Affiche : 13

const obj2 = createObject(20)
console.log(obj2.plusOne())
// Affiche : 21
console.log(obj2.plusOne())
// Affiche : 22
console.log(obj2.plusOne())
// Affiche : 23

Passage d'objets encapsulés

En plus d'encapsuler et de renvoyer des objets C++, il est possible de passer des objets encapsulés en les désencapsulant avec la fonction d'assistance Node.js node::ObjectWrap::Unwrap. L'exemple suivant montre une fonction add() qui peut prendre deux objets MyObject comme arguments d'entrée :

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

Dans myobject.h, une nouvelle méthode publique est ajoutée pour permettre l'accès aux valeurs privées après la désencapsulation de l'objet.

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

L'implémentation de myobject.cc reste similaire à la version précédente :

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;

// Warning! This is not thread-safe, this addon cannot be used for worker
// threads.
Global<Function> MyObject::constructor;

MyObject::MyObject(double value) : value_(value) {
}

MyObject::~MyObject() {
}

void MyObject::Init(Isolate* isolate) {
  // Prepare constructor template
  Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
  tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
  tpl->InstanceTemplate()->SetInternalFieldCount(1);

  Local<Context> context = isolate->GetCurrentContext();
  constructor.Reset(isolate, tpl->GetFunction(context).ToLocalChecked());

  AddEnvironmentCleanupHook(isolate, [](void*) {
    constructor.Reset();
  }, nullptr);
}

void MyObject::New(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  Local<Context> context = isolate->GetCurrentContext();

  if (args.IsConstructCall()) {
    // Invoked as constructor: `new MyObject(...)`
    double value = args[0]->IsUndefined() ?
        0 : args[0]->NumberValue(context).FromMaybe(0);
    MyObject* obj = new MyObject(value);
    obj->Wrap(args.This());
    args.GetReturnValue().Set(args.This());
  } else {
    // Invoked as plain function `MyObject(...)`, turn into construct call.
    const int argc = 1;
    Local<Value> argv[argc] = { args[0] };
    Local<Function> cons = Local<Function>::New(isolate, constructor);
    Local<Object> instance =
        cons->NewInstance(context, argc, argv).ToLocalChecked();
    args.GetReturnValue().Set(instance);
  }
}

void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();

  const unsigned argc = 1;
  Local<Value> argv[argc] = { args[0] };
  Local<Function> cons = Local<Function>::New(isolate, constructor);
  Local<Context> context = isolate->GetCurrentContext();
  Local<Object> instance =
      cons->NewInstance(context, argc, argv).ToLocalChecked();

  args.GetReturnValue().Set(instance);
}

}  // namespace demo

Testez avec :

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)
// Affiche : 30