
Création d'addons natifs avec Node-API (N-API) anciennement NAN
Apprenez à étendre Node.js avec du C/C++ via N-API (Node-API) pour la performance ou l'intégration de code natif. Oubliez NAN, maîtrisez la méthode moderne et stable.
Pourquoi dépasser les limites de JavaScript avec des addons natifs ?
Node.js excelle dans la gestion des opérations I/O grâce à son architecture événementielle, mais le code JavaScript lui-même s'exécute sur un seul thread et peut montrer ses limites pour certaines tâches. Il existe des scénarios où les performances de JavaScript pur ne suffisent pas, ou lorsque vous avez besoin d'interagir avec des bibliothèques C ou C++ existantes, ou encore d'accéder à des fonctionnalités système de bas niveau non exposées par les API Node.js standard.
C'est là qu'interviennent les addons natifs. Un addon natif est un module compilé écrit en C ou C++ (ou d'autres langages pouvant s'interfacer avec C) qui peut être chargé dans Node.js à l'aide de la fonction `require()` comme n'importe quel autre module JavaScript. Il permet d'exécuter du code natif compilé directement au sein du processus Node.js, offrant ainsi une passerelle entre le monde JavaScript et le code natif.
Historiquement, la création d'addons natifs était complexe et fragile, principalement à cause des changements internes fréquents du moteur V8 (le moteur JavaScript de Node.js). Les développeurs utilisaient des couches d'abstraction comme NAN (Native Abstractions for Node.js) pour tenter de masquer ces changements, mais cela nécessitait souvent de recompiler les addons pour chaque nouvelle version majeure de Node.js. Heureusement, une solution moderne et stable a émergé : Node-API (N-API).
Introduction à Node-API (N-API) : la stabilité avant tout
N-API (prononcé N A P I) est une interface binaire d'application (ABI - Application Binary Interface) stable, fournie directement par Node.js et indépendante du moteur JavaScript sous-jacent (V8). C'est la différence fondamentale avec NAN et les approches précédentes. L'ABI définit la manière dont le code compilé de l'addon interagit avec Node.js au niveau binaire.
Le principal avantage de cette stabilité d'ABI est qu'un addon natif compilé avec N-API pour une version majeure de Node.js (par exemple, Node 14.x) continuera généralement à fonctionner avec les versions futures de Node.js (Node 16.x, 18.x, 20.x, etc.) sans nécessiter de recompilation. C'est une amélioration considérable pour la maintenance et la distribution des modules contenant du code natif.
N-API fournit un ensemble de fonctions C/C++ (définies dans l'en-tête `
Mettre en place l'environnement de build avec node-gyp
Pour compiler votre code C/C++ en un addon natif `.node` utilisable par Node.js, l'outil standard est `node-gyp`. Il s'agit d'un outil de construction multiplateforme basé sur GYP (Generate Your Projects), un système de méta-construction issu du projet Chromium. `node-gyp` lit un fichier de configuration et génère les fichiers de projet appropriés pour la plateforme cible (Makefiles sur Linux/macOS, projets Visual Studio sur Windows) avant d'invoquer le compilateur C++.
Avant d'utiliser `node-gyp`, vous devez avoir un environnement de compilation C++ fonctionnel installé sur votre système (par exemple, `build-essential` sur Debian/Ubuntu, Xcode Command Line Tools sur macOS, Visual Studio Build Tools sur Windows) ainsi que Python (généralement Python 3.x).
La configuration de la construction se fait via un fichier nommé `binding.gyp` placé à la racine de votre projet d'addon. Ce fichier, au format JSON-like, décrit comment construire votre addon :
- `targets` : Définit un ou plusieurs modules à construire. Typiquement, un seul target pour votre addon.
- `target_name` : Le nom de votre fichier `.node` généré (sans l'extension).
- `sources` : La liste de vos fichiers source C/C++ (`.c`, `.cc`, `.cpp`).
- `include_dirs` : Inclut les répertoires nécessaires pour les en-têtes, notamment celui fourni par `node-addon-api` si vous utilisez le wrapper C++ pour N-API, ou directement via `
- D'autres options pour les dépendances, les flags de compilation, etc.
Une fois le `binding.gyp` créé, les commandes principales sont :
- `npm install` ou `yarn install` : Si `node-gyp` est listé dans les dépendances ou via un script `install`, il sera souvent exécuté automatiquement.
- `node-gyp configure` : Génère les fichiers de projet spécifiques à la plateforme dans un répertoire `build/`.
- `node-gyp build` : Lance la compilation du code C/C++. Le résultat (`votrenom.node`) sera placé dans `build/Release/`.
Ecrire du code N-API en C/C++ : les bases
L'écriture de code N-API implique l'utilisation des fonctions C définies dans `
- `napi_env env` : Le premier argument de presque toutes les fonctions N-API. Il représente l'environnement d'exécution de l'instance Node.js et sert de contexte opaque pour les opérations N-API.
- `napi_callback_info info` : Passé aux fonctions C++ qui sont appelées depuis JavaScript. Il contient les informations sur l'appel, notamment les arguments passés depuis JS et le `this` de l'appel.
- `napi_value` : Un type opaque représentant une valeur JavaScript (number, string, object, function, etc.) côté C++.
- Gestion des erreurs : Presque toutes les fonctions N-API retournent un `napi_status` (par exemple, `napi_ok` si tout va bien). Il est essentiel de vérifier ce statut après chaque appel N-API pour détecter et gérer les erreurs potentielles qui pourraient autrement crasher le processus Node.js. N-API permet également de lever des exceptions JavaScript depuis le code C++.
Pour interagir avec JavaScript, vous utiliserez des fonctions N-API pour :
- Obtenir les arguments : `napi_get_cb_info` pour extraire les arguments (`napi_value argv[]`) et leur nombre (`size_t argc`) depuis `napi_callback_info`.
- Convertir les `napi_value` en types C/C++ : `napi_get_value_string_utf8`, `napi_get_value_double`, `napi_get_value_int32`, etc.
- Créer des `napi_value` à partir de types C/C++ : `napi_create_string_utf8`, `napi_create_double`, `napi_create_object`, `napi_create_function`, etc., pour retourner des valeurs à JavaScript.
Enfin, vous devez enregistrer les fonctions C/C++ que vous souhaitez exposer à JavaScript. Cela se fait en créant des descripteurs de propriétés (`napi_property_descriptor`) qui lient un nom JavaScript à une fonction C/C++ (`napi_callback`), puis en les ajoutant à l'objet `exports` du module. Une macro `NAPI_MODULE(module_name, InitFunction)` simplifie souvent ce processus d'initialisation.
Exemple concret : un addon simple en N-API (C)
Créons un addon très simple qui exporte une fonction `add` prenant deux nombres en arguments et retournant leur somme.
1. Fichier `binding.gyp` :
{
"targets": [
{
"target_name": "addon",
"sources": [ "addon.c" ]
}
]
}
2. Fichier `addon.c` :
#include
// La fonction C qui implémente l'addition
napi_value Add(napi_env env, napi_callback_info info) {
napi_status status;
size_t argc = 2; // Nous attendons 2 arguments
napi_value argv[2];
double value1 = 0;
double value2 = 0;
napi_value sum;
// 1. Obtenir les arguments passés depuis JavaScript
status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
if (status != napi_ok || argc < 2) {
napi_throw_type_error(env, NULL, "Deux arguments numériques attendus.");
return NULL; // Retourner NULL en cas d'erreur
}
// 2. Convertir les arguments napi_value en double C++
status = napi_get_value_double(env, argv[0], &value1);
if (status != napi_ok) {
napi_throw_type_error(env, NULL, "Le premier argument doit être un nombre.");
return NULL;
}
status = napi_get_value_double(env, argv[1], &value2);
if (status != napi_ok) {
napi_throw_type_error(env, NULL, "Le second argument doit être un nombre.");
return NULL;
}
// 3. Effectuer l'addition
double result = value1 + value2;
// 4. Convertir le résultat C++ en napi_value
status = napi_create_double(env, result, &sum);
if (status != napi_ok) {
napi_throw_error(env, NULL, "Impossible de créer la valeur de retour.");
return NULL;
}
// 5. Retourner la somme (napi_value)
return sum;
}
// Fonction d'initialisation du module
napi_value Init(napi_env env, napi_value exports) {
napi_status status;
// Décrire la propriété 'add' que nous voulons exporter
napi_property_descriptor desc =
{ "add", NULL, Add, NULL, NULL, NULL, napi_default, NULL };
// Exporter la propriété 'add' (qui pointe vers la fonction C Add)
status = napi_define_properties(env, exports, 1, &desc);
if (status != napi_ok) {
napi_throw_error(env, NULL, "Impossible de définir la propriété 'add'.");
}
return exports;
}
// Enregistrer le module avec Node.js
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
// NODE_GYP_MODULE_NAME est une macro qui prend la valeur de target_name dans binding.gyp
3. Compilation : Exécutez `npm install node-gyp` (si pas déjà fait), puis `node-gyp configure && node-gyp build`.
4. Utilisation en JavaScript (`test.js`) :
// Charger l'addon natif compilé
const addon = require('./build/Release/addon.node');
const num1 = 5.5;
const num2 = 10;
// Appeler la fonction native 'add'
const result = addon.add(num1, num2);
console.log(`${num1} + ${num2} = ${result}`); // Devrait afficher: 5.5 + 10 = 15.5
try {
addon.add(num1); // Tester l'erreur (moins de 2 arguments)
} catch (e) {
console.error('Erreur attrapée:', e.message);
}
try {
addon.add('hello', num2); // Tester l'erreur (pas un nombre)
} catch (e) {
console.error('Erreur attrapée:', e.message);
}
Cet exemple illustre le flux complet : définition C, configuration du build, compilation, et utilisation depuis JavaScript, y compris une gestion basique des erreurs.
Utilisation et considérations : quand choisir N-API ?
La création d'addons natifs avec N-API est une technique puissante mais qui introduit une complexité significative. Il faut la réserver aux situations où elle apporte un avantage clair :
- Goulots d'étranglement CPU avérés : Lorsque le profilage montre qu'une tâche spécifique, gourmande en calculs, limite les performances de votre application Node.js, et que les Worker Threads ne suffisent pas ou ne sont pas adaptés.
- Intégration de bibliothèques C/C++ existantes : Si vous devez utiliser une fonctionnalité fournie par une bibliothèque C/C++ mature et performante depuis Node.js (par exemple, une bibliothèque de traitement d'image, de calcul scientifique, de base de données spécifique).
- Accès bas niveau au système : Pour interagir avec des API du système d'exploitation ou du matériel qui ne sont pas accessibles via les modules Node.js standard.
Avant de vous lancer dans N-API, considérez toujours les alternatives :
- Optimisation JavaScript : Le code JS peut-il être optimisé ?
- Worker Threads : Pour les tâches CPU-intensives en pur JavaScript, c'est souvent la solution la plus simple et la plus sûre.
- WebAssembly (WASM) : Une alternative pour exécuter du code haute performance de manière portable dans Node.js (et les navigateurs). WASM peut être compilé depuis C/C++, Rust, etc., et offre un bac à sable sécurisé, contrairement aux addons natifs.
Les inconvénients des addons natifs sont réels :
- Complexité : Nécessite des compétences en C/C++ et la gestion d'un environnement de build natif.
- Fragilité : Une erreur dans le code C/C++ (gestion mémoire incorrecte, pointeur invalide) peut faire planter tout le processus Node.js de manière abrupte.
- Portabilité : Vous devez compiler l'addon pour chaque plateforme cible (Windows, macOS x64/arm64, Linux x64/arm64).
- Maintenance : La double base de code (JS et C/C++) augmente l'effort de maintenance.
Malgré ces défis, N-API représente la méthode moderne et stable recommandée pour le développement d'addons natifs en Node.js lorsqu'ils sont nécessaires, offrant une solution pérenne au problème historique de la recompilation fréquente.