samedi 17 janvier 2009

À coup d'essais et d'erreurs, j'y suis arrivé! Enfin, presque. J'utilise, pour faire simple, un objet global. Je sais, c'est laid, mais ce n'est que pour des fins de tests.

Bon! Il faut d'abord savoir que V8 fonctionne à coup de callbacks. Et un callback V8, pour une fonction (les objets utilisent des "accesseurs" et des "intercepteurs", qui ont une signature différente), eh bien, c'est une fonction qui a le prototype suivant (remplacez "callback" par le nom de votre choix):


v8::Handle<v8::Value> callback(const v8::Arguments &args);

Un tel prototype nous apprend deux choses:

  1. Une fonction JS peut prendre un nombre arbitraire de paramètres (args encapsulant en fait un conteneur quelconque)

  2. Une fonction JS retourne toujours une valeur. Il n'existe pas de fonction "void"


M'inspirant donc de l'exemple shell.cc, j'ai créé une fonction print qui sera appelée lorsqu'un script appellera une fonction JS nommée print:

v8::Handle<v8::Value> print(const v8::Arguments &args)
{
if(args.Length() == 1) // On ne veut qu'un seul paramètre
{
v8::HandleScope handleScope; // On définit une portée pour nos Handle
v8::String::Utf8Value str(args[0]); // On obtient la valeur du premier paramètre
std::cout << *str << std::endl; // On l'affiche ainsi qu'un retour de ligne
}
return v8::Undefined(); // Les fonctions "void" sont en fait des fonctions qui retournent undefined
}


Cette fonction permettra donc à quelqu'un d'envoyer des données dans cout à travers un script JS.

Voici le concept: on veut créer un objet JS global nommé "collection" qui supportera deux méthodes: getObjectById(string), qui retourne un entier correspondant à la clé passée en paramètre (ou null si la clé n'existe pas), et setObject(string, int) qui assigne une valeur à une clé (ou la créé si la clé n'existe pas).

J'ai donc commencé par créer une classe TestCollection (un enrobage autour de std::map, en fait):

#include <map>
#include <exception>

template<class K, class V>
class TestCollection
{
public:
typedef K key_type;
typedef V value_type;
private:
std::map<key_type, value_type> collection_;
public:
value_type getObjectById(const key_type & id) const
{
std::map<key_type, value_type>::const_iterator itt = collection_.find(id);
if(itt != collection_.end())
{
return itt->second;
}
else
{
throw std::exception("Object not found");
}
}
void setObject(key_type id, value_type val)
{
collection_[id] = val;
}
};

Définissons nous ensuite un objet global (oui, je sais, c'est laid):

TestCollection<std::string, int> collection;

Puis, créons les deux callbacks:

v8::Handle<v8::Value> setObject(const v8::Arguments &args)
{
if(args.Length() == 2)
{
v8::HandleScope handleScope;
v8::String::Utf8Value key(args[0]); // La clé, c-a-d une string
v8::Local<v8::Value> value(args[1]); // La valeur, c-a-d un int
collection.setObject(*key, value->Int32Value()); // On appelle la méthode setObject de l'objet
}
return v8::Undefined();
}

v8::Handle<v8::Value> getObjectById(const v8::Arguments &args)
{
if(args.Length() == 1)
{
v8::HandleScope handleScope;
v8::String::Utf8Value str(args[0]); // On va chercher la clé
try
{
const std::string key(*str); // On créé une std::string à partir de la clé
int i = collection.getObjectById(key); // On va chercher la valeur
return v8::Int32::New(i); // On la retourne
}
catch(std::exception e)
{
// L'objet n'a pas été trouvé, on affiche le message
std::cout << e.what();
}
}
return v8::Null();
}

Ces deux fonctions ne sont en fait que des interfaces pour les méthodes du vrai objet.


int main()
{
v8::HandleScope handleScope;
// On créé l'objet global
v8::Handle<v8::ObjectTemplate> global = v8::ObjectTemplate::New();
// On créé la fonction print
global->Set("print", v8::FunctionTemplate::New(print));
// On créé la collection
v8::Handle<v8::ObjectTemplate> collection = v8::ObjectTemplate::New();
// On créé les deux méthodes de la collection
collection->Set("getObjectById", v8::FunctionTemplate::New(getObjectById));
collection->Set("setObject", v8::FunctionTemplate::New(setObject));
// On assigne la collection comme appartenant à global
global->Set("collection", collection);
// On créé le contexte en spécifiant l'objet global
v8::Handle<v8::Context> context = v8::Context::New(0, global);
// On exécute le script
JSScript script("script.js", context);
script.run();

system("pause");
}

Tout d'abord, l'objet global. Lorsque l'on appelle une fonction libre ou que l'on accède à une variable globale, en JS, en fait, on appelle une méthode ou on accède à un attribut de l'objet global. Cet objet est créé comme n'importe quel objet, à l'aide d'un ObjectTemplate.
Même chose pour l'objet collection: celui-ci est en fait un ObjectTemplate auquel on ajoute deux méthodes (lesquelles sont des FunctionTemplate) à l'aide de la méthode Set. On ajoute ensuite collection à global à l'aide de Set, et on créé le contexte en informant celui-ci qu'il devra utiliser un objet global déja créé (par défaut, il créé un objet vide).
Voilà.
Ah, oui, JSScript est une petite classe que j'ai créée pour automatiser la logique de création et d'exécution d'un script. Son contenu est trivial alors je ne la dévoilerai pas ici.

Pour ceux que ça intéresse, voici le contenu de script.js:

collection.setObject("test", 2);
collection.setObject("test2", collection.getObjectById("test") + 2);
print(collection.getObjectById("test"));
print(collection.getObjectById("test2"));

Ce qui affiche 2 et 4.

D'ailleurs, je crois avoir compris pourquoi la doc est si mince sur V8: j'ai obtenu le code sur leur SVN, et le numéro de version est 0.4.7. C'est donc une beta. J'ai passé leur code dans Doxygen, mais celui-ci est si peu documenté que ça ne donne pas grand chose.

EDIT: Vous pouvez maintenant télécharger la doc Doxygen de la v0.4.7 ici: doxyV8.zip. Je sais, c'est sur RapidShare, mais bon.


Prochaine étape: se débarrasser de cet horrible objet global.

2 commentaires:

Andy a dit…

je reste toujours quelque peu retissant quant à l'utilisation de librairie en beta. Surtout considérant l'existence de librairie un peu plus mature non?

Etienne de Martel a dit…

Dans les moteurs JavaScript implémentant une forme de compilation JIT (et donc, ayant des performances décentes), V8 est le plus abouti. Et son code est plutôt propre.

Mais je voudrais bien voir TraceMonkey, de Mozilla. Sauf que c'est basé sur SpiderMonkey lequel est très vieux, et donc potentiellement sale. Et surtout, c'est en C.

Enregistrer un commentaire