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):

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

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):
  1. #include <map>  
  2. #include <exception>  
  3.   
  4. template<class K, class V>  
  5. class TestCollection  
  6. {  
  7. public:  
  8.     typedef K key_type;  
  9.     typedef V value_type;  
  10. private:  
  11.     std::map<key_type, value_type> collection_;  
  12. public:  
  13.     value_type getObjectById(const key_type & id) const  
  14.     {  
  15.         std::map<key_type, value_type>::const_iterator itt = collection_.find(id);  
  16.         if(itt != collection_.end())  
  17.         {  
  18.             return itt->second;  
  19.         }  
  20.         else  
  21.         {  
  22.             throw std::exception("Object not found");  
  23.         }  
  24.     }  
  25.     void setObject(key_type id, value_type val)  
  26.     {  
  27.         collection_[id] = val;  
  28.     }  
  29. };  

Définissons nous ensuite un objet global (oui, je sais, c'est laid):
  1. TestCollection<std::string, int> collection;  

Puis, créons les deux callbacks:
  1. v8::Handle<v8::Value> setObject(const v8::Arguments &args)  
  2. {  
  3.     if(args.Length() == 2)  
  4.     {  
  5.         v8::HandleScope handleScope;  
  6.         v8::String::Utf8Value key(args[0]); // La clé, c-a-d une string  
  7.         v8::Local<v8::Value> value(args[1]); // La valeur, c-a-d un int  
  8.         collection.setObject(*key, value->Int32Value()); // On appelle la méthode setObject de l'objet  
  9.     }  
  10.     return v8::Undefined();  
  11. }  
  12.   
  13. v8::Handle<v8::Value> getObjectById(const v8::Arguments &args)  
  14. {  
  15.     if(args.Length() == 1)  
  16.     {  
  17.         v8::HandleScope handleScope;  
  18.         v8::String::Utf8Value str(args[0]); // On va chercher la clé  
  19.         try  
  20.         {  
  21.             const std::string key(*str); // On créé une std::string à partir de la clé  
  22.             int i = collection.getObjectById(key); // On va chercher la valeur  
  23.             return v8::Int32::New(i); // On la retourne  
  24.         }  
  25.         catch(std::exception e)  
  26.         {  
  27.             // L'objet n'a pas été trouvé, on affiche le message  
  28.             std::cout << e.what();  
  29.         }  
  30.     }  
  31.     return v8::Null();  
  32. }  

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

  1. int main()  
  2. {  
  3.     v8::HandleScope handleScope;  
  4.      // On créé l'objet global  
  5.     v8::Handle<v8::ObjectTemplate> global = v8::ObjectTemplate::New();  
  6.     // On créé la fonction print  
  7.     global->Set("print", v8::FunctionTemplate::New(print));  
  8.     // On créé la collection  
  9.     v8::Handle<v8::ObjectTemplate> collection = v8::ObjectTemplate::New();   
  10.      // On créé les deux méthodes de la collection  
  11.     collection->Set("getObjectById", v8::FunctionTemplate::New(getObjectById));  
  12.     collection->Set("setObject", v8::FunctionTemplate::New(setObject));           
  13.     // On assigne la collection comme appartenant à global  
  14.     global->Set("collection", collection);   
  15.     // On créé le contexte en spécifiant l'objet global  
  16.     v8::Handle<v8::Context> context = v8::Context::New(0, global);   
  17.     // On exécute le script  
  18.     JSScript script("script.js", context);   
  19.     script.run();  
  20.   
  21.     system("pause");  
  22. }   

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:
  1. collection.setObject("test", 2);  
  2. collection.setObject("test2", collection.getObjectById("test") + 2);  
  3. print(collection.getObjectById("test"));  
  4. 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