Follow me
RSS feed
My sources
My Viadeo

Développer des extensions pour Node.js

Greg | 30 Nov 2010

Dev La première fois que j'ai touché à Node.js je n'ai pas pu m'empêcher d'avoir une pensée émue pour le défunt1 Netscape LiveWire qui offrait alors la toute première implémentation SSJS. Passons...

Dans un précédent article, je vous ai présenté ma solution permettant d'utiliser GraphViz avec Node. La mise en place de cette solution passe par le développement d'un module. Je vous propose aujourd'hui de voir comment développer ce type d'extension pour Node.

La création d'extensions pour Node peut se faire de deux façons. La solution la plus évidente consiste, bien entendu, à développer en Javascipt. L'autre solution consiste à utiliser l'API C++ de V8. De ces deux méthodes en découle une troisième, évidente, qui consiste à mixer les deux précédentes.

Comme toujours, je ne serais trop vous conseiller de consulter la documentation. Pour la partie JavaScript, la documentation de Node est certes un peu light à certains moments, mais en regardant en plus le code des nombreux modules existants, vous devriez en apprendre pas mal. Pour V8, il existe également de la documentation, mais je vous conseille de regarder les sources. Pour cela, le point de départ se situe dans node.h. Ce header devrait tout naturellement vous conduire vers v8.h. Vous pouvez aussi jeter un oeil sur ev.h, header de libev2, la librairie d'évènements utilisée par Node. Parcourez enfin eio.h, header de libeio3, utilisé par Node pour faire de la gestion d'I/O de manière asynchrone.

Dans la suite de ce post, je pars du principe que vous avez une bonne connaissance de JavaScript et surtout de C++. Si tel n'est pas le cas, j'espère que vous en tirerez tout de même quelque chose ;)

JavaScript

Version fonctionnelle

Imaginons que vous ayez une librairie4 JavaScript que vous souhaitez utiliser dans Node. Quelque chose comme cela :

torus.js
var PI = 3.141593;

function area(R, r) {
  return 4 * Math.pow(PI, 2) * R * r;
}

function volume(R, r) {
  return 4 * Math.pow(PI, 2) * R * Math.pow(r, 2);
}

Pour transformer cela en module exploitable par Node, il suffit de préciser ce qui doit être exporté.

Il faut savoir que Node utilise le système mis en place dans CommonJS pour la gestion des modules. Soit, que le chargement d'un module se fait via l'instruction require à laquelle on passe le module à charger, et qui renvoie en retour un objet JavaScript listant les APIs5 exportées du module. Pour déclarer ce qui doit être exporté, dans le module, nous utilisons l'objet exports, de la façon suivante :

exports.<nom d'export> = <API a exporter>

Donc, si nous voulons transformer notre librairie torus.js en module pour Node, il nous suffira d'ajouter les deux lignes suivantes à la fin du fichier :

exports.area = area;
exports.volume = volume;

Nous avons maintenant un nouveau module à notre disposition :

var torus = require('./torus');

var A = torus.area(10,3)
// => 1184.35278931788

var V = torus.volume(10,3)
// => 3553.0583679536403

Version objet

Tout cela c'est bien beau, me direz-vous, mais "moi je n'ai pas pour habitude de faire du fonctionnel ! Je ne travaille qu'en objet !". Et bien soit, faisons de l'objet et réécrivons le contenu du fichier torus.js de la façon suivante :

var PI = 3.141593;

function Torus(R, r) {
	this.R = R;
	this.r = r;
}

Torus.prototype.area = function() {
	return 4 * Math.pow(PI, 2) * this.R * this.r;
}

Torus.prototype.volume = function() {
  return 4 * Math.pow(PI, 2) * this.R * Math.pow(this.r, 2);
}

Pour permettre l'utilisation de la classe Torus via un module Node, il suffit d'exporter la fonction Torus :

exports.Torus = Torus;

Nous pouvons maintenant utiliser la classe Torus :

var Torus = require('./torus').Torus;

var t = new Torus(10,3);

t.area();
// => 1184.35278931788

t.volume();
// => 3553.0583679536403

Maintenant que vous avez les bases, un peu de lecture et vous savez tout ce qu'il y a à savoir pour créer des modules Node en JavaScript.

C++

Nous allons voir maintenant comment développer le même module que précédemment, mais en utilisant l'API C++ de V8.

Version fonctionnelle

Lors de la création du module torus en JavaScript, nous avons commencé par une solution fonctionnelle. Si nous voulons faire la même chose avec l'API de V8, nous obtenons ceci :

torus.cc
#include <node.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <math.h>

using namespace v8;

#define PI 3.141593

Handle<Value> Area(const Arguments& args) {
  if(args.Length() < 2 || (!args[0]->IsNumber() && !args[1]->IsNumber())) {
    return ThrowException(Exception::Error(String::New("Argument error! Give me 2 number!")));
  }
  
  return Number::New(4 * pow(PI, 2) * args[0]->NumberValue() * args[1]->NumberValue());
}

// Close Standard Streams
Handle<Value> Volume(const Arguments& args) {
  if(args.Length() < 2 || (!args[0]->IsNumber() && !args[1]->IsNumber())) {
    return ThrowException(Exception::Error(String::New("Argument error! Give me 2 number!")));
  }
  
  return Number::New(4 * pow(PI, 2) * args[0]->NumberValue() * pow(args[1]->NumberValue(), 2));
}

extern "C" void init(Handle<Object> target) {
  HandleScope scope;
  
  NODE_SET_METHOD(target, "area", Area);
  NODE_SET_METHOD(target, "volume", Volume);
}

Détaillons ce code.

Pour développer un module en JavaScript, nous avons utilisé l'objet exports pour préciser quels APIs devaient être visible de l'extérieur de notre module. En C++, lors du chargement du module, Node appelle la fonction init en lui passant l'objet cible (target). Nous utilisons ensuite la macro NODE_SET_METHOD qui va permettre d'affecter à l'objet cible une méthode que l'on nommera. Ainsi, la ligne NODE_SET_METHOD(target, "area", Area); indique que quand nous appellerons la méthode aera de notre module, Node reroutera la demande vers la méthode Area.

La définition d'une méthode appelable, via un module Node, se fait en suivant le prototype Handle (const Arguments&);

Dans ces méthodes, nous recevons les arguments via un tableau d'objets Arguments. Pour chaque argument, nous pouvons tester son type via une méthode IsXxx et on récupèrera sa valeur via une méthode XxxValue.

Notre module étant codé, il faut maintenant le compiler. Pour cela nous allons utiliser l'outil node-waf. Pour cela nous devons créer un script (wscript en python) donnant les instructions pour la compilation. Dans notre cas, il n'y a rien de particulier, le script utilisé sera donc le suivant :

srcdir = "."
blddir = "build"
VERSION = "0.1.0"

def set_options(opt):
  opt.tool_options("compiler_cxx")

def configure(conf):
  conf.check_tool("compiler_cxx")
  conf.check_tool("node_addon")

def build(bld):
  obj = bld.new_task_gen("cxx", "shlib", "node_addon")
  obj.target = "torus"
  obj.source = "torus.cc"

Nous lançons ensuite la compilation via la commande node-waf configure build. Si tout s'est bien passé, nous obtenons un répertoire build/default contenant le fichier torus.node. Nous pouvons passer à la phase de tests :

var torus = require('./build/default/torus');

var A = torus.area(10,3)
// => 1184.35278931788

var V = torus.volume(10,3)
// => 3553.0583679536403

Version objet

Faire une version objet de notre module va, dans un premier temps, demander assez peu de modifications. En effet, tout ce que nous avons à faire est d'exporter une classe JavaScript, soit, de mettre en place une une fonction à laquelle on ajoutera, au prototype, les méthodes area et volume. Pour faire cela, nous modifions le fichier torus.cc de la façon suivante :

#include <node.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <math.h>

using namespace v8;

#define PI 3.141593

Handle<Value> Area(const Arguments& args) {
  if(args.Length() < 2 || (!args[0]->IsNumber() && !args[1]->IsNumber())) {
    return ThrowException(Exception::Error(String::New("Argument error! Give me 2 number!")));
  }
  
  return Number::New(4 * pow(PI, 2) * args[0]->NumberValue() * args[1]->NumberValue());
}

// Close Standard Streams
Handle<Value> Volume(const Arguments& args) {
  if(args.Length() < 2 || (!args[0]->IsNumber() && !args[1]->IsNumber())) {
    return ThrowException(Exception::Error(String::New("Argument error! Give me 2 number!")));
  }
  
  return Number::New(4 * pow(PI, 2) * args[0]->NumberValue() * pow(args[1]->NumberValue(), 2));
}

Handle<Value> New (const Arguments& args) {
  HandleScope scope;

  return args.This();
}

extern "C" void init(Handle<Object> target) {
  HandleScope scope;
  
  Local<FunctionTemplate> t = FunctionTemplate::New(New);
  
  NODE_SET_PROTOTYPE_METHOD(t, "area", Area);
  NODE_SET_PROTOTYPE_METHOD(t, "volume", Volume);
  
  target->Set(String::NewSymbol("Torus"), t->GetFunction());
}

Comme vous pouvez le voir, nous nous sommes contentés de modifier le corps de la fonction init. A ce niveau nous utilisons la classe FunctionTemplate pour créer la classe JavaScript. Pour cela nous créons un objet de type FunctionTemplate en passant au constructeur la méthode qui servira de constructeur pour la classe JavaScript. Par la suite, nous remplaçons l'utilisation de NODE_SET_METHOD par NODE_SET_PROTOTYPE_METHOD pour modifier l'objet prototype. Enfin, nous exportons notre classe.

var Torus = require('./build/default/torus').Torus;

var t = new Torus();

var A = t.area(10,3);
// => 1184.35278931788

var V = t.volume(10,3);
// => 3553.0583679536403

Avec cette version, nous ne retrouvons pas les mêmes possibilités que la version purement JavaScript de notre module. En effet, non seulement il n'est pas possible de passer les valeurs des rayons au constructeur, mais il n'y a pas non plus de getter et setter pour ces valeurs. Pour rendre la version C++ conforme à ce qui est attendu, nous allons devoir modifier l'ensemble du code.

Afin de pouvoir stocker facilement les valeurs des rayons, nous allons commencer par mettre en place une classe C++ répondant au besoin de calcul d'aire et volume. Nous obtenons quelque chose qui ressemble grosso modo à cela :

#include <math.h>

#define PI 3.141593

class Torus {
  double Area() {
    return(4 * pow(PI, 2) * torus_radius * tube_radius);
  }
  
  double Volume() {
    return(4 * pow(PI, 2) * torus_radius * pow(tube_radius, 2));
  }
    
  Torus(double torus, double tube) {
    torus_radius = torus;
    tube_radius = tube;
  }
  
  ~Torus() {
  }
  
  double torus_radius;
  double tube_radius;
};

Nous pouvons maintenant ajouter la fonction d'initialisation. Afin de ne pas nous mélanger les pinceaux, nous allons ajouter une méthode statique à la classe Torus, qui se chargera de faire ce travail.

#include <node.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <math.h>

using namespace v8;
using namespace node;

#define PI 3.141593

class Torus {
public:
  
  static void Initialize (v8::Handle<v8::Object> target) {
    HandleScope scope;

    Local<FunctionTemplate> t = FunctionTemplate::New(New);
    t->InstanceTemplate()->SetInternalFieldCount(1);

    NODE_SET_PROTOTYPE_METHOD(t, "area", Area);
    NODE_SET_PROTOTYPE_METHOD(t, "volume", Volume);
    
    target->Set(String::NewSymbol("Torus"), t->GetFunction());
  }

  double Area() {
    return(4 * pow(PI, 2) * torus_radius * tube_radius);
  }
  
  double Volume() {
    return(4 * pow(PI, 2) * torus_radius * pow(tube_radius, 2));
  }
  
protected:
  
	// ...
  
  Torus(double torus, double tube) {
    torus_radius = torus;
    tube_radius = tube;
  }
  
  ~Torus() {
  }
  
  double torus_radius;
  double tube_radius;
};


extern "C" void init(Handle<Object> target) {
  HandleScope scope;
  
  Torus::Initialize(target);
}

Il faut maintenant écrire les méthodes New, Area et Volume nécessaires au bon fonctionnement de notre classe JavaScript, et respectant donc le prototype Handle (const Arguments&);.

Dans la méthode New, nous allons récupérer les paramètres passés, puis nous créerons un objet C++ de type Torus en passant les valeurs récupérées à son constructeur. Il ne reste plus, alors, qu'à conserver l'objet de type Torus dans l'objet JavaScript renvoyé. Voici le contenu de la méthode New :

static Handle<Value> New (const Arguments& args) {
  HandleScope scope;

  if(args.Length() < 2 || (!args[0]->IsNumber() && !args[1]->IsNumber())) {
    return ThrowException(Exception::Error(String::New("Argument error! Give me 2 number!")));
  }
  
  Torus *torus = new Torus(args[0]->NumberValue(), args[1]->NumberValue());
  torus->Wrap(args.This());
  
  return args.This();
}

Pour les méthodes Area et Volume, il suffit de sortir l'objet de type Torus de l'objet JavaScript et de renvoyer le résultat des calculs correspondants :

static Handle<Value> Area(const Arguments& args) {
  Torus *torus = ObjectWrap::Unwrap<Torus>(args.This());

  HandleScope scope;
  
  return Number::New(torus->Area());
}

static Handle<Value> Volume(const Arguments& args) {
  Torus *torus = ObjectWrap::Unwrap<Torus>(args.This());

  HandleScope scope;
  
  return Number::New(torus->Volume());
}

Terminons en ajoutant les getter et setter. Pour cela, nous utilisons la méthode SetAccessor. Cette méthode, appliquée à l'objet prototype, prend en paramètre le nom nom de l'accesseur et les méthodes de set et get. Nous modifions donc la méthode Initialize :

static void Initialize (v8::Handle<v8::Object> target) {
  HandleScope scope;

  Local<FunctionTemplate> t = FunctionTemplate::New(New);
  t->InstanceTemplate()->SetInternalFieldCount(1);

  NODE_SET_PROTOTYPE_METHOD(t, "area", Area);
  NODE_SET_PROTOTYPE_METHOD(t, "volume", Volume);
  
  t->PrototypeTemplate()->SetAccessor(String::NewSymbol("R"), TorusRadiusGetter, TorusRadiusSetter);
  t->PrototypeTemplate()->SetAccessor(String::NewSymbol("r"), TubeRadiusGetter, TubeRadiusSetter);

  target->Set(String::NewSymbol("Torus"), t->GetFunction());
}

Il ne reste plus qu'à écrite les quatre méthodes TorusRadiusGetter, TorusRadiusSetter, TubeRadiusGetter et TubeRadiusSetter.

Les getters utilisent le prototype Handle (Local, const AccessorInfo&) ou le premier paramètre contient le nom de l'entité et le second les données de l'objet JavaScript. Nous pouvons donc écrire les méthodes pour notre module :

static Handle<Value> TorusRadiusGetter (Local<String> property, const AccessorInfo& info) {
  Torus *torus = ObjectWrap::Unwrap<Torus>(info.This());

  HandleScope scope;

  return Number::New(torus->torus_radius);
}

static Handle<Value> TubeRadiusGetter (Local<String> property, const AccessorInfo& info) {
  Torus *torus = ObjectWrap::Unwrap<Torus>(info.This());

  HandleScope scope;

  return Number::New(torus->tube_radius);
}

Les setters utilisent le prototype void TubeRadiusSetter (Local, Local, const AccessorInfo&). Les paramètres sont, respectivement, le nom de l'entité, la nouvelle valeur à affecter et les données de l'objet JavaScript.

static void TorusRadiusSetter (Local<String> property, Local<Value> value, const AccessorInfo& info) {
  Torus *torus = ObjectWrap::Unwrap<Torus>(info.This());

  HandleScope scope;

  if(!value->IsNumber()) {
    ThrowException(Exception::TypeError(String::New("R is NaN!")));
  }

  torus->torus_radius = value->NumberValue();
}

static void TubeRadiusSetter (Local<String> property, Local<Value> value, const AccessorInfo& info) {
  Torus *torus = ObjectWrap::Unwrap<Torus>(info.This());

  HandleScope scope;

  if(!value->IsNumber()) {
    ThrowException(Exception::TypeError(String::New("r is NaN!")));
  }

  torus->tube_radius = value->NumberValue();
}

Il ne reste plus qu'à compiler tout cela et à tester...

var Torus = require('./build/default/torus').Torus;

var t = new Torus(10, 3);

var A = t.area();
// => 1184.35278931788
var V = t.volume();
// => 3553.0583679536403

t.R = 7;
t.r = 2;

A = t.area();
// => 552.697968348344
V = t.volume();
// => 1105.395936696688

npm

Maintenant que nous avons un module, il serait bon de le mettre à la disposition de la communauté. Pour cela nous allons utiliser npm, système de gestion de packages pour node.

npm est relativement simple à utiliser. Il suffit de créer un fichier package.json dans lequel nous décrivons le contenu du package.

Voici le fichier que l'on pourrait utiliser pour la version JavaScript de notre module :

{ 
  "name" : "Torus",
  "version" : "0.1.0",
  "description" : "Torus mathematica",
  "author": "Gregoire Lejeune",
  "main": "./torus"
}

Et voici celui pour la version C++ :

{ 
  "name" : "Torus",
  "version" : "0.1.0",
  "description" : "Torus mathematica",
  "author": "Gregoire Lejeune",
  "main": "./buils/default/torus",
  "scripts": { "install": "node-waf configure build" }
}

A partir de là, nous pouvons installer notre package en nous plaçant dans le répertoire contenant le fichier package.json et en lançant la commande npm install .

Bien entendu, npm permet également de mettre à disposition de tout le mon le package sur le site du projet. Pour plus d'information sur ce sujet, je vous renvoie à la documentation.

Pour ceux qui souhaiteraient tester les exemples données dans ce post, vous pouvez télécharger les sources.

JavaScript et C++

Comme je l'ai indiqué au début de cet article, la solution consistant à mixer JavaScript et C++ est tout à fait envisageable. C'est même certainement une solution optimale dans certains cas ou la simplicité de JavaScript ferait défaut et la lourdeur de C++ serait overkill.

Je ne détaillerai pas ce type de solution. Je vous invite à consulter les sources de <pub>daemonize</pub> qui en est une illustration.


1 Je dis défunt car après avoir succombé aux charmes de Java, il ne survit plus que dans les entrailles d'Oracle iPlanet.
2 http://software.schmorp.de/pkg/libev.html
3 http://software.schmorp.de/pkg/libeio.html
4 On entend ici un ensemble de fonctions.
5 J'utilise volontairement le terme générique d'API, là où certains se seraient attendus à me voir parler de fonction. En effet, l'export ne se limite pas seulement à des fonctions.

Copyright © 2009 - 2011 Grégoire Lejeune.
All documents licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License, except ones with specified licence.
Powered by Jekyll.