0 commentaires lundi 19 janvier 2009

Victoire! Je me suis débarrassé de cet ignoble objet global et j'ai en même temps appris l'existence du type v8::External, qui est un enrobage autour d'un void *.

J'ai tout d'abord commencé par définir un typedef pour éviter une verbosité excessive:


typedef TestCollection<std::string, int> Collection;

Commençons d'abord par le main:

int main()
{
// La collection
Collection testCollection;
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éé le template de la collection
v8::Local<v8::ObjectTemplate> collectionTemplate = v8::ObjectTemplate::New();
// On créé les deux méthodes du template
collectionTemplate->Set("getObjectById", v8::FunctionTemplate::New(getObjectById));
collectionTemplate->Set("setObject", v8::FunctionTemplate::New(setObject));
// On informe le template qu'il aura un champ interne
// (si on ne le fait pas, on aura un débordement de tableau plus tard)
collectionTemplate->SetInternalFieldCount(1);
// On créé le contexte
v8::Handle<v8::Context> context = v8::Context::New(0, global);
// On créé le script
JSScript script("script.js", context);
// On instancie la collection à partir du template
v8::Local<v8::Object> collection = collectionTemplate->NewInstance();
// On assigne un pointeur sur testCollection à la collection pour qu'on puisse y accéder plus tard
collection->SetInternalField(0, v8::External::New(&testCollection));
// On assigne la collection comme appartenant à l'objet global
context->Global()->Set(v8::String::New("collection"), collection);
// On exécute le script
script.run();
}

On remarque d'abord l'objet global et la fonction print(). Puis, le template de la collection. Or, ça s'arrête là. J'ai en effet réalisé que l'on n'était pas obligé d'utiliser le template comme objet. On pourrait voir un template comme une classe: on le créé, on définit des méthodes, puis on instancie à l'aide de NewInstance().
Ah, NewInstance(). Cette méthode m'a donnée du fil à retordre. Originellement, elle causait une erreur de segmentation, jusqu'à ce que je réalise qu'on devait obligatoirement créer un Context et un ContextScope avant. Un objet ne peut en effet exister que dans un contexte: on ne peut créer un objet "dans le vide".
Une fois l'objet instancié à l'aide de NewInstance(), on peut lui assigner divers méthodes ou attributs à l'aide de Set, ou des données internes (similaires en utilisation à des attributs privés) à l'aide de ce qu'on appelle un "internal field". Un champ interne est un attribut qui ne sera pas accessible par le script. On peut donc s'en servir pour stocker diverses informations. Ici, je m'en sert pour stocker un External, qui contiendra un pointeur sur la collection. Finalement, on déclare collection comme un objet appartenant à l'objet global. Remarquez la syntaxe différente: on assigne pas des objets comme on assigne des templates. L'effet est le même, par contre.

Mais comment utiliser ce pointeur? Voici la fonction setObject():

v8::Handle<v8::Value> setObject(const v8::Arguments &args)
{
if(args.Length() == 2)
{
// args.Holder() retourne l'objet possédant la fonction: dans ce cas précis, l'objet de collection
// On va donc chercher le External dans l'objet à l'aide de GetInternalField()
// L'appel à la méthode Cast est nécessaire, car GetInternalField() retourne un Object
// alors que nous voulons un External
v8::Local<v8::External> external = v8::Local<v8::External>::Cast(args.Holder()->GetInternalField(0));
// Puis, on prend le pointeur contenu dans le External.
// Celui-ci sera un void *, alors un cast est nécessaire
Collection * collection = static_cast<Collection *>(external->Value());
// Le reste est identique
v8::HandleScope handleScope;
v8::String::Utf8Value key(args[0]);
v8::Local<v8::Value> value(args[1]);
collection->setObject(*key, value->Int32Value());
}
return v8::Undefined();
}

getObjectById() est similaire. L'important ce situe dans les premières lignes du if: le reste est inchangé (ou presque: on utilise maintenant le pointeur sur l'objet).

Voilà!

Prochaine étape: faire en sorte que getObjectById() retourne des objets JS complexes plutôt que des entiers. Cela me permettra d'expérimenter les accesseurs.

Comme toujours, j'attends vos commentaires.

Ah, oui, j'en profite pour laisser un lien sur des benchmarks des divers moteurs JavaScript sur le marché:
JavaScript Performance Rundown.
Comme vous pouvez le voir, IE ne joue pas dans la même ligue que le reste.

2 commentaires 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.

3 commentaires vendredi 16 janvier 2009

Je me lance donc dans l'aventure bloggienne. On verra ce que ça va donner. Pour plus de détails, voir le message de droite.

Mon stage me laissant beaucoup de temps libre, j'ai dépoussiéré quelques vieux projets, dont un avec lequel j'ai le goût d'expérimenter des bindings C++ - JavaScript. V8 étant sortit il n'y a pas très longtemps, je me suis dit: pourquoi pas?

Déjà, la compilation m'a donné du fil à retordre. Il faut en effet utiliser Scons, outil avec lequel j'étais très peu familier. Déjà, j'ai eu besoin de quelques recherches Google pour y arriver. Puisque Scons refusait de fonctionner sous Mac OS X, j'ai finalement lancé Parallels Desktop pour y aller sur Windows. Victoire! Ça fonctionne!

J'obtint alors un gros .lib de près de 70 Mo, que je m'empressait d'inclure à un nouveau projet VC++. Réalisant à quel point cette lib était grosse, je me suis dit qu'il devait bien y avoir une meilleure façon de procéder. Je suis donc retourné dans la doc et j'ai appris qu'on pouvait décider d'obtenir une DLL à la place en entrant un certain paramètre. Tadam! J'ai maintenant une jolie DLL (compilée en release, naturellement) et un petit .lib de 150 Ko. Bon, la compilation de mon projet me donne 10 warnings qui me disent tous que telle ou telle méthode doit avoir une interface de DLL pour les clients d'une certaine classe (warning C4251, pour être plus précis). Si quelqu'un sait de quoi il s'agit, qu'il me le dise. Parce que moi et les DLL, c'est un peu nébuleux.

Mais avant tout, il me fallait évidemment du code pour voir si la configuration de mon IDE était bonne. Alors, naturellement, je suis allé sur le site officiel de V8 (aka quelques pages sur Google Code) et j'ai ramassé le "Hello World". C'était assez simple à comprendre. Je dois dire qu'à vu de nez, comme ça, ça m'a l'air assez propre, comme code.

Résultat des courses: un joli programme qui affiche "Hello World!"

Prochaine étape: faire apparaître un objet C++ comme un objet JS. Ça devrait être assez complexe, vu que la documentation de V8 se résume à un hello world, un "embedding guide" qui ne fait que survoler les fonctionnalités de la bibliothèque, ainsi que deux exemples. Et Google donne peu de résultats sur le sujet. Je risque fort probablement de me baser sur ces exemples pour mes essais.

Donc, si vous voulez vous aussi binder C++ et JavaScript, eh bien, comme disent les Anglophones: Stay tuned!