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 :
- Node-API
nan
(Abstractions natives pour Node.js)- utilisation directe des bibliothèques V8, libuv et Node.js internes
À 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 :
module.exports.hello = () => 'world'
Tout d'abord, créez le fichier 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
Tous les modules d'extension Node.js doivent exporter une fonction d'initialisation suivant le modèle :
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 :
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
, etLocal\<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 versDeleteInstance()
. 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()
ouv8::Function::New()
qui crée les fonctions JavaScript natives. Le troisième paramètre dev8::FunctionTemplate::New()
ouv8::Function::New()
accepte lev8::External
et le rend disponible dans le rappel natif à l'aide de la méthodev8::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 :
#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]
Version | Modifications |
---|---|
v14.8.0, v12.19.0 | Les 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()
:
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
:
// 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 :
// 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.
{
"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é :
// 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 à :
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.
// 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 :
{
"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
:
"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
:
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 :
// 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 :
// 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 :
// 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 :
// 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()
:
// 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 :
// 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 :
// 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 :
// 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 :
// 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
:
// 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 :
// 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
:
{
"targets": [
{
"target_name": "addon",
"sources": ["addon.cc", "myobject.cc"]
}
]
}
Testez avec :
// 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 :
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
:
// 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 :
// 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 :
// 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
:
{
"targets": [
{
"target_name": "addon",
"sources": ["addon.cc", "myobject.cc"]
}
]
}
Testez-le avec :
// 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 :
// 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.
// 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 :
// 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 :
// 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