Entries for category Programming

Wed Sep 15 2010

C++, MongoDB, Programming

asio, boost, bson, c++, mongodb, mongoxx, proto

9 comments

mongoxx, driver C++ alternatif pour MongoDB

J'utilise MongoDB pour une poignée de projets mais je n'ai jamais eu besoin des différents drivers python, php ou encore javascript. Aucun, sauf le driver C++. Bon, OK, si on considère que le shell mongo est un driver javascript, alors oui, je l'utilise beaucoup[1].

Le driver C++ est intimement lié au projet MongoDB dans la mesure où il est maintenu par les core developers -une partie du staff de 10gen- et qu'il partage la plupart de ses sources avec celles du server.

Il y a des avantages à ce qu'un tel driver soit à ce point corrélé au server. Premièrement, il fait partie du cercle fermé des drivers officiels, ceux développés par 10gen. Deuxièmement, des améliorations au niveau du server peuvent être répercutées sur le driver puisque les sources sont communes (par exemple les mécanismes de réplication). Enfin, comme la plupart des tests unitaires et fonctionnels du projet s'appuient sur ce driver, nul besoins de préciser que ses bugs sont corrigés en priorité.

Maintenant, les désavantages, car il y en a.

Compiler le driver, c'est à dire obtenir libmongoclient et une série de fichiers headers n'est pas si simple que ça. Le build system de MongoDB est basé sur SCons, un software construction tool écrit en python. SCons est un très bon outil. Un projet est construit (compilé, installé etc) à l'aide d'instructions python présentes dans un fichier SConstruct ainsi que, optionnellement, autant de fichiers SConscript qu'il y a de sous-répertoires et sous-projets.

MongoDB est une collection de projets. Le server principal mongod, mais aussi des tools tels que mongosniff, mongodump, le shell mongo, des exécutables pour les tests ou le driver C++ qui est, comme nous l'avons vu, une librairie qui se compile et est pleinement intégrée au source tree principal.

MongoDB n'a recours qu'à un seul et unique fichier SConstruct pour compiler et installer tous les sous-projets pour toutes les plateformes (diverses distribs Linux, Solaris, Windows, 32bit, 64bits etc). Le fichier a grossi exponentiellement. Celui-ci est régulièrement tweaké au gré des besoins imposés par un rythme de développement soutenu.

Cela n'est pas gênant dans la mesure où la méthode conseillée pour installer MongoDB est de télécharger des packages prêts à l'emploi pour une plateforme donnée. Nul besoin de lancer ni même installer SCons. Comme la plupart des applications utilisant MongoDB sont écrites en ruby, python ou php[2] et que leurs drivers respectifs sont développés sur des canaux séparés, la majorité des utilisateurs n'a que faire des subtilités du build system.

Seul les core devs, les buildbots et les utilisateurs du driver C++ (et bien sûr les hackers en tout genre;) sont amenés à s'adapter. C'est mon cas. Depuis le début, je maintiens un fork qui me permet de m'accomoder en douceur, si je puis dire.

Pendant un temps, j'ai soumis quelques patches en fonctions des problèmes que je rencontrais. Par exemple le fait d'utiliser spidermonkey en provenance du dépot mercurial de mozilla-central plutôt que des tarballs individuels ou des packages (j'ai depuis switché pour V8). Ou encore le fait d'utiliser diverses versions de GCC avec différents flags.

Le driver C++ a toujours eu un statut un peu bâtard, principalement parce que personne ne l'utilise à part le projet MongoDB, ou presque. Récemment, les choses se sont dégradées et à moins de bidouiller le build system, il n'est plus possible d'effectuer une installation conforme dans un répertoire cible contenant les includes et lib nécessaires, et donc ne pas pénaliser les projets qui dépendent d'une telle installation.

Grâce à mon fork, ça va, je gère.. pour l'instant. Ce n'est pas le cas de tout le monde. Les gars de 10gen ont prévu de normaliser la situation. Comme ils savent un peu (légèrement;) mieux que moi ce qu'il faut faire pour opérer un découplage propre sans briser tout le reste, je prends mon mal en patience.

...jusqu'à il y a quelques jours. Après tout, quel intérêt ai-je à dépendre de ce driver, qui même correctement compilé et installé, n'est pas dénué de défauts comme nous allons le voir.

Quand j'ai commencé à écrire mes premiers codes utilisant ce driver, ce qui m'a le plus étonné et frustré était à quel point les développeurs ont été peu précautionneux au regard des pratiques élémentaires d'importation des namespaces C++.

En gros, le moindre include de header mongo avait pour conséquence d'importer tous les symbols de la librairie standard, mais aussi quelques namespaces de la librairie Boost, dans le namespace global. C'est simple, Il était impossible d'utiliser le driver C++ pour un projet autre que MongoDB lui-même. Heureusement, un fix d'antologie -par votre serviteur- a permis de limiter les dégâts à moindre frais ;) Maintenant, c'est le namespace mongo qui est pollué. Un soin curatif complet demanderait une qualification complète de tous les noms dans tous les fichiers headers et des importations sur demande dans les fichiers sources. Une tâche longue et fastidieuse que personne n'a eu le courage d'entreprendre.

mongoxx

Du coup j'ai entrepris d'écrire mon propre driver C++, mongoxx, entièrement découplé des sources de MongoDB. L'idée, c'est que ce soit découplé, d'accord, mais surtout explorer certaines librairies Boost que je n'ai jamais encore utilisé. Je pense à Proto ou Move dont la review est pending. Pour la partie communication, Asio bien entendu.

Quid de BSON, le format "JSON binaire" omniprésent dans MongoDB? Je n'ai pas trouvé de container suffisamment souple avec lequel travailler pour implémenter les specs BSON. Un std::vector<char> ne fait pas complètement l'affaire, pas plus qu'un boost::array.

Du coup je me suis monté un container binaire générique qui repose ultimement sur des malloc/realloc et que j'utilise pour implémenter BSON mais aussi pour construire les messages envoyés au server qui contiennent souvent des docs BSON en plus de headers/flags spécifiques.

Mes premiers essais avec Proto sont vraiment fantastiques ;) Cette librairie est une tuerie. Un petit teaser:

// 'doc_', 'genoid', 'undefined', 'now' sont des terminals proto.
// l'espace mémoire requis est calculé à la compilation
// si possible (pod types) 
 
doc d = doc_
    ("_id", genoid)
    ("machin", "ok")
    ("truc", undefined)
    ("embed", doc_
        ("val", 3.14)
        ("date", now))
;
 
d << "last" << 42; // grows as needed
 
// STL iterator ready
std::for_each(d.begin(), d.end(), [](elem const& e){
    pretty_print(e);
});

Le but c'est que l'interface du driver soit simple et à peu près conforme aux conventions de la librairie standard ainsi que Boost, ce que le driver C++ de MongoDB n'est pas du tout. Par exemple être compatible avec les itérateurs pour parcourir des documents BSON ou des results sets et donc pouvoir y appliquer les algorithms standards.

Actuellement j'ai un chantier de proofs of concept que je veux résorber et structurer, en commençant par y ajouter des tests unitaires avant d'aller plus loin. Je push ensuite tout ça sur github. Comme je souhaite me débarrasser complètement de libmongoclient, je ne vais pas vous cacher que j'ai encore pas mal de boulot ;)

[1] Une bonne partie de l'administration de ce site se fait à partir d'un shell mongo.

[2] MongoDB survey results publié le 18 Fev 2010.

Thu Jun 03 2010

PHP, Programming

php, traits

8 comments

Un point sur les Traits de PHP

Dernièrement, les Traits ont fait leur entrée dans le trunk de PHP. Je ne vais pas les présenter dans leur intégralité puisque la RFC correspondante dont les premières esquisses ont plus de deux ans, est suffisamment claire et détaillée.

Cependant, le document n'est pas très explicite en ce qui concerne l'utilisation d'interfaces et de traits au sein d'une même classe, ainsi que le statut des variables membres et leur accessibilité dans les méthodes des traits. Nous allons voir ce qu'il en est et les conséquences que cela implique.

Avant de commencer, il est vivement recommandé d'avoir lu la RFC. Juste la première partie qui concerne les traits. La seconde partie présente les Grafts que nous n'aborderons pas ici [1].

L'opération de flattening, aka le lissage

L'ordre dans lequel les traits sont manipulés par php est le suivant:

  • Un fichier source est chargé, et comme par hasard, celui-ci contient des traits.
  • Le parser rencontre une déclaration de trait. Ce dernier est pris en compte comme n'importe quelle classe [2]. C'est à dire qu'une structure zend_class_entry est crée et complétée. Seul un flag spécial précise qu'il s'agit d'un trait.
  • Le même parser rencontre une classe qui utilise un trait via le mot clé use ou un trait qui dépend d'un autre trait (on dit qu'il s'agit d'un trait composite). A la fin de la déclaration de la classe, que l'on appelle une client class ou host class, les opérations s'enchainent ainsi:
    • Les méthodes provenant des traits sont triées selon les directives as et insteadof. Une nouvelle table temporaire est construite accueillant ces méthodes candidates à la future intégration dans la classe. Certaines méthodes peuvent être ignorées s'il y a des conflits. On se chope alors un "Warning: Trait method blah has not been applied, because there are collisions with other trait methods".
    • L'opération de flattening (lissage) commence. Pour chaque méthode candidate qui n'est pas redéfinie (overridden) dans la classe client:
      • Vérification du prototype (la signature) par rapport aux éventuelles méthodes des classes parents. S'il y a des différences, et selon la configuration, c'est là qu'on reçoit un "Strict Standards: Declaration of Machin::truc() should be compatible with..".
      • La méthode est dupliquée (son zend_op_array est littéralement copié comme le fait l'extension runkit) et est ajoutée à la table des méthodes de la classe client. Certaines opérations irréversibles se produisent, notamment le fait que la méthode perd toute notion et tout lien avec son trait d'origine [3].
    • Vérification de la bonne conformance de la classe (maintenant dotée de nouvelles méthodes provenant des traits) par rapport aux méthodes abstraites ainsi qu'aux interfaces sensées être implémentées.
  • Le parser continue son job jusqu'à la fin du fichier.

OK, voila une bonne chose de faite.

Sachant que les méthodes des traits sont injectées pratiquement "les yeux fermés", tout se passe comme si celles-ci étaient définies naturellement dans la classe client. Les accès à $this se font exactement de la même manière, rien ne change.

On peut dès lors s'interroger sur certains idiomes qui ne devraient pas tarder à fleurir sur les coins de tables en cette période pré-estivale. En effet, la nature stateless des traits tels qu'implémentés à l'heure actuelle dans php n'est pas sans conséquence quant à l'organisation du code.

L'état des traits

En fait, ils n'en ont pas. On dit qu'ils sont stateless. C'est un choix délibéré et argumenté dans les documents académiques du Software Composition Group (SCG) sur lesquels l'implémentation de php est basée.

Les traits sont uniquement fonctionnels. Ils ne sont pas liés à un quelconque état qui leur serait propre, comme cela peut être le cas dans de l'héritage multiple. Par état, il faut comprendre variables membres. Seule la classe client peut en fournir.

Or, il y a fort à parier que des traits ne manipulant aucun état se feront rares.

Donc, que faire ?

Je vais mettre de coté l'utilisation de variables globales, statiques ou de Singletons. On veut juste faire en sorte que les traits puissent utiliser les variables membres de la classe client. Il n'y a pas 50 possibilités, il y en a juste 2:

  • Accès direct aux variables via $this->
  • Recours à des getters/setters fournis par la classe client

Quoique l'on fasse, ou que je sois, rien ne t'efface, je pense à toi, le trait est dépendant de sa classe client. Accéder directement à une variable avec $this->var peut (pourrait, pourra, pouvait, a pu?) sembler briser l'encapsulation, et nous reviendrons sur ce point. Utiliser des getters et setters est la méthode conseillée par le SCG en particulier parce que leur démonstration et implémentation est basée sur le langage Smalltalk et que de toute manière ils ne peuvent faire autrement.

OK, OK. Il est grand temps qu'on balance un code illustratif car toute cette prose commence à devenir lourdingue ;)

trait T1
{
  public function doSomething() {
    $this->var = 42;
    echo $this->var;
  }
}

trait T2
{
  public function doSomethingElseButDoItWell() {
    $this->setVar(43);
    echo $this->getVar();
  }
}

class C
{
  use T1, T2;
  
  private $var;
  
  private function getVar() {
    return $this->var;
  }
  private function setVar($value) {
    $this->var = $value;
  }
}

$c = new C;
$c->doSomething();
$c->doSomethingElseButDoItWell(); // please

var_dump($c);

Cela donne, comme attendu:

4243object(C)#1 (1) { ["var":"C":private]=> int(43) }

Dépendances et requirements

La dépendance classe -> trait est simplement exprimée dans le code via le mot clé use. Celle-ci est vérifiée dès la compilation. La dépendance trait -> classe n'est quant à elle pas exprimée du tout dans le code précédent, en tout cas pas avant l'exécution. T1 dépend de la présence de la propriété $var et T2 dépend des getter et setter getVar() et setVar().

La RFC préconise l'usage de méthodes abstraites au sein des traits pour exprimer et vérifier à la compilation cette dépendance trait -> classe. Le document parle de requirements, ce qui est la même chose.

Réécrivons T2 en le bardant de méthodes abstraites afin de se conformer aux bonnes pratiques au paragraphe du dessus:

trait T2
{
  public function doSomethingElseButDoItWell() {
    $this->setVar(43);
    echo $this->getVar();
  }
  
  /*private*/ abstract function getVar(); 
  /*private*/ abstract function setVar($value);
}

Ce frêle bout de code est en fait plus profond que ce que l'on pourrait imaginer à première vue, surtout quand on sait que ça compile et que ça fonctionne.

Premièrement, bien que les getters/setters de la classe client soient de visibilité private, cela ne pose pas de problème de les déclarer public dans le trait. La règle des méthodes abstract dit pourtant que la méthode concrète doit avoir une visibilité identique ou moins restrictive que la déclaration abstraite.

Cela fonctionne parce que l'opération de lissage décrite plus haut ne concerne que les méthodes des traits qui ne sont pas redéfinies dans la classe client. Cette redéfinition ne se fait que sur le nom de la méthode. La signature n'intervient pas du tout. La méthode qui porte le même nom dans la classe va complètement masquer celle du trait. Il est donc tout à fait possible de remplacer setVar($value) dans le trait par ceci:

/*private*/ abstract function setVar($value, array $bonus);

On pourrait s'attendre à un beau "Fatal error: Declaration of C::setVar() must be compatible with that of T2::setVar()" car les signatures sont clairement différentes. Et bien non. La phase de lissage rend possible ce comportement.

Exprimer les requirements d'un trait par l'intermédiaire de méthodes abstraites n'est donc que partiel. Seul le nom de la fonction compte. La signature et les éventuels type hintings sont ignorés.

Mais alors pourquoi ça fonctionne quand même? C'est simple, toutes les méthodes abstraites du trait qui ne sont pas masquées par celles de la classe client se retrouvent dans cette même classe toujours avec le statut abstract. Là php peut détecter la non implémentation de ces méthodes et ainsi s'opère la vérification partielle des dépendances trait -> class.

Deuxièmement (le premièrement est 4 ou 5 paragraphes plus haut;) pourquoi ai-je supprimé la visibilité private pour les méthodes abstraites du trait? Après tout, les getter et setter sont privés dans la classe C.

C'est une bien sage question.

La réponse est que l'on veut éviter cet effet indésirable: "Fatal error: Abstract function T2::getVar() cannot be declared private". C'est une conséquence du fait que les traits sont traités (c'est le cas de le dire) par php comme de simples classes lors de la compilation. Ce point fut brièvement abordé plus haut ici. Or, une méthode abstraite ne peut être déclarée private car php considère que celle-ci n'est pas accessible depuis des classes dérivées et qu'il est donc impossible d'y apporter une implémentation. Quelque part, ça a du sens...

De l'existentialisme

Jusqu'ici, nos getters et setters font leur job. On n'en demande pas plus. Pourtant, un sérieux problème se profile à l'horizon.

C::getVar() retourne une copie de la variable membre $var. Sa valeur n'est pas systématiquement dupliquée, php faisant du copy-on-write, mais nous restons dans une sémantique de copie. Toute modification opérée sur ce que retourne getVar() n'aura pas de répercussion sur la véritable variable membre. Cela peut être tout à fait justifié et suffisant.

Cependant, prenons un peu de recul. Nous avons affaire à un getter privé dont l'appel se fait d'une méthode pleinement intégrée à la classe et qui pourrait tout à fait être privée elle aussi. Nous évoluons dans un no man's land à la frontière de l'encapsulation, l'indirection et l'abstraction. Le fameux triptyque des mots en ion.

Le trait pourrait avoir besoins de manipuler autre chose qu'un simple scalaire, par exemple un array. Va-t-il falloir définir toujours plus de setters pour toujours plus d'opérations telles que l'ajout, l'insertion ou le retrait d'éléments? Faut-il gonfler la classe client en méthodes annexes à faible valeur ajoutée, annulant au passage les bénéfices en copy'n'paste sensés provenir de l'utilisation même du trait?

Une solution à mi-chemin entre rien du tout, c'est à dire accéder directement à la variable membre via $this->var, et une belle série de getters/setters serait de recourir à un simple accesseur retournant une référence sur la variable. Coté trait, l'affectation classique = ou par référence =& permet de controller la nature de la liaison suivant l'opération que l'on désire effectuer, ce qui laisse à php la possibilité de faire son job routinier de passage (retour) par copie.

Il ne reste plus qu'à dégainer les variables temporaires. Oui, car comme chacun sait, php est très frileux quand il s'agit de déréférencer automatiquement les retours de fonctions. Vous aurez beau bidouiller en ajoutant des parenthèses ou des accolades, seule la dernière ligne n'est pas une erreur dans les constructions suivantes:

$this->func()[]
$this->func()()
++$this->func()
$this->func() = 
$this->func()->chain()

C'est navrant, et c'est la syntaxe qui n'est pas bonne, le problème détecté étant un parse error dans la plupart des cas. Pour contourner ce problème, il faudra d'abord récupérer le retour de la fonction dans une variable puis travailler à partir de celle-ci.

Traits et Interfaces

Nous allons clore cet article par un exemple qui réunit tout ce qui a été vu: faire endosser à un trait l'implémentation d'une interface héritée de la classe client. Cette construction est à mon sens potentiellement utile pour les auteurs de libraires et frameworks.

Une telle classe client peut être utilisée dans un contexte polymorphique ou de type hinting sans reposer pour autant sur une hiérarchie parfois bancale de classes abstraites, ni sur la présence d'adaptateurs spécifiques et verbeux dans le cadre d'une composition ou aggrégation.

On choisit d'implémenter ArrayAccess qui est automatiquement disponible dans php car fournie par la SPL.

/*
interface ArrayAccess
{
  public function offsetGet($offset);
  public function offsetSet($offset, $value);
  public function offsetExists($offset);
  public function offsetUnset($offset);
} */

trait ArrayAccessImpl
{
  abstract function &data();
  
  public function offsetGet($offset) {
    $d = $this->data();
    return $d[$offset];
  }
  
  public function offsetSet($offset, $value) {
    $d = &$this->data();
    $offset === null ? $d[] = $value : $d[$offset] = $value;
  }
  
  public function offsetExists($offset) {
    return array_key_exists($offset, $this->data());
  }
  
  public function offsetUnset($offset) {
    $d = &$this->data();
    unset($d[$offset]);
  }
}

class MyClass implements ArrayAccess
                     //, Iterator // laissé en exercice
{
  private $data = array(
    'a' => 3.14,
    'b' => null,
    'c' => true,
  );
  
  use ArrayAccessImpl;
  
  private function &data() {
    return $this->data;
  }
}

$c = new MyClass;

var_dump(isset($c['b']));
$c[] = $c['a'];
unset($c['c']);
var_dump($c);

Note: l'utilité plus que douteuse de MyClass n'est pas fortuite, il s'agit juste d'un code d'exemple (ma spécialité;)

Conclusions

Comme d'hab, RAS?

Traits're in da trunk, mais cela ne veut pas dire pour autant qu'ils seront dans le futur php, ni sous leur forme actuelle. Cependant, vu l'avancement du projet, le contraire serait étonnant.

Si tout se passe comme prévu, alors j'ai hâte de voir comment ils seront employés à l'avenir. Il s'agit d'un réel nouveau paradigme pour composer du code OOP. Par exemple, pour valider la pertinence des traits, le SCG a réimplémenté toute la hiérarchie des Collections de Smalltalk.

Je salue mageekguy pour son billet Et si on tirait des traits ? qui est à l'origine de mon soudain regain d'intérêt.

 

[1] Le statut des grafts est bien plus hypothétique puisqu'aucune implémentation n'existe à l'heure actuelle.

[2] Un trait peut déclarer des variables membres ou dériver d'une classe. Cela n'est pas interdit (pour l'instant?). Il ne peut cependant pas implémenter une interface. Cela résulte en une fatal error.

[3] Les éventuelles variables membres déclarées dans le trait auquel la méthode appartenait sont purement et simplement ignorées.

Thu Apr 08 2010

C++, NoteToMyself, Programming

boost, c++, phoenix

0 comment

Boost Phoenix 2.0 tips

Quelques tips pour Boost.Phoenix2 glanés dans la sueur et dans le sang (dans la doc aussi).

Higher higher order function

On a vite fait d'imbriquer les algorithms de Phoenix, mais il ne faut pas oublier qu'ils sont déjà des higher order functions. Leur prédicats doivent être évalués de manière lazy et même lazy lazy si je puis dire.

Un code vaut mieux qu'un long schema à main levée. Dans ce qui suit, j'ai deux std::vector de int. Je veux afficher les éléments du premier vector qui ne sont pas dans le second. Un code à la con qui a juste valeur d'exemple. Dedans y est glissée une erreur grossière. Saurez-vous la trouver ? Votre compilo saura la trouver lui, et il se fera un plaisir de générer environ 50ko de message d'erreur :)

#include <iostream>
#include <vector>
#include <algorithm>
#include <boost/spirit/include/phoenix_core.hpp>
#include <boost/spirit/home/phoenix/scope/local_variable.hpp>
#include <boost/spirit/home/phoenix/statement/if.hpp>
#include <boost/spirit/home/phoenix/scope/let.hpp>
#include <boost/spirit/home/phoenix/container.hpp>
#include <boost/spirit/include/phoenix_operator.hpp>
#include <boost/spirit/include/phoenix_algorithm.hpp>

int main()
{
    using boost::phoenix::arg_names::arg1;
    using boost::phoenix::local_names::_a;
    using boost::phoenix::ref;
    using boost::phoenix::let;
    using boost::phoenix::if_;
    using boost::phoenix::find_if;
    using boost::phoenix::end;
	
    std::vector<int> a;
    std::vector<int> b;
	
    a.push_back(1);
    a.push_back(2);
    a.push_back(3);
	
    b.push_back(2);
	
    std::for_each(a.begin(), a.end(),
        let(_a = arg1)
        [
            if_(end(ref(b)) == find_if(ref(b), arg1 == _a))
            [
                std::cout << _a << std::endl
            ]
        ]
    );
	
    return 0;
}

Le problème est que find_if attend un prédicat, dont on pourrait penser qu'il est tout à fait correcte en tant que composite phoenix arg1 == _a. Mais le bloc du find_if est déjà un composite, autrement dit une higher order function. Son prédicat doit être évalué de manière lazy lazy et non pas lazy -tout court.

La solution est fournie par phoenix: le composite lambda va permettre de transformer le prédicat récalcitrant en une higher higher order function. Le tour est joué:

std::for_each(a.begin(), a.end(),
    let(_a = arg1)
    [
        // lambda[arg1 == _a]
        if_(end(ref(b)) == find_if(ref(b), lambda[arg1 == _a]))
        [
            std::cout << _a << std::endl
        ]
    ]
);

Bind to overloaded member function

Qui n'a jamais été tenté de binder à gogo avec phoenix, tellement c'est facile ? Le prob survient le jour où on tente désespérément de binder une fonction qui a des overloads. le compilo se plaint avec un message du genre bind(<unresolved overloaded function type>, ..25 lignes de types phoenix..).

Example. Je cherche à binder std::map<K,V>::find. On va prendre int pour K et V. m est une instance quelconque de ce type map:

typedef std::map<int, int> map_t;

bind(&map_t::find, ref(m), arg1);

Pas bon. map_t::find a plusieurs overloads. Le compilo ne sait pas lequel choisir. Il faut le guider, après tout, c'est nous les boss dans l'histoire!

Méthode one liner:

bind(static_cast<
        map_t::const_iterator (map_t::*)(int const&) const
     >(&map_t::find), ref(m), arg1);

On utilise ce bon vieux static_cast.

Autre méthode, passer par un pointer sur fonction membre:


map_t::const_iterator (map_t::*find_f)(int const&) const = &map_t::find;

bind(find_f, ref(m), arg1);

Ca fait son job. On n'en demande pas plus.

Conclusion

RAS

Fri Mar 19 2010

C++, Programming

boost, c++, json, r8t, spirit, template, v8, variant

2 comments

r8t, un moteur de template pour C++ à base de javascript

L'idée de base, mais vraiment de base, consiste à clamer à qui veut bien l'entendre que JSON est un format idéal pour passer des données à un système de Vue, comprendre View, dans un MVC. Ce format a tout ce qu'il faut: simple, compact, expressif et connu de tous, ou presque.

Le constat de base, mais vraiment de base (oui vous savez), c'est qu'il y a très peu de systèmes de template pour C++ qui ont un penchant web, c'est à dire principalement orientés génération de HTML. Je ne sais pas pourquoi, peut être parce que tout le monde s'en fout ? ;) Toujours est-il que lorsque je me suis mis en recherche d'un tel système de template, pour des besoins perso, je n'ai rien trouvé d'autre que Clearsilver et google-ctemplate. Il me fallait un truc standalone et léger. J'aurai pu chercher un peu plus, mais je me suis arrêté là, pas vraiment convaincu.

C'est là que je me suis souvenu de l'idée de base: JSON. En fait ça tombait bien puisque j'utilisais déjà le moteur javascript V8 dans mon projet. Cet engine, initialement conçu par google pour le navigateur Chrome, est open source et son API est en C++. L'idée qui a commencé à émerger est la suivante:

  • On balance du "JSON" au système de template
  • La logique de présentation est controllée par javascript
  • Le système crache du text en retour (principalement du HTML)

Sympa. Reste plus qu'à implémenter ça.

Le JSON, expédié avec une technique à base de Boost.Variant récursif que j'ai présenté dans ce billet Simple modélisation de JSON en C++.

Le javascript qui controle la logique de présentation est embarqué dans les templates sous une forme proche d'un mix de Django et de la syntaxe alternative de php.

{% for (i in posts) : %}
  <div class="post">
    <h2>{%= posts[i].title %}</h2>
    ...
  </div>
{% end %}

Le principe c'est que tout ce qui est entre {% ... %} est grosso modo du javascript qui sera inchangé avant de le donner au moteur js. Une phase de parsing transforme le fichier de template (ou n'importe quel text) en une forme intermédiaire qui sera consumée par V8. Pour l'exemple ci-dessus, cela donne quelque chose proche de ceci:

for (i in posts) {__pr('  <div class="post">\n    <h2>');
__p(posts[i].title);__pr('</h2>\n    ...\n  </div>\n');};

Le parser est écrit avec la librairie Boost.Spirit Classic. __p() pour "print" et __pr() pour "print raw" sont des fonctions javascript dont l'implémentation est en C++ et qui permettent de controller la sortie textuelle finale, par exemple en appliquant des filtres d'échappement automatique pour du HTML. Ces filtres sont inspirés des modifiers de google-ctemplate. Du genre {%:h:s= comment %} pour un filtre h qui échappe du HTML et un autre s qui permet de wrapper des snippets entre des éléments <pre>.

Ce qui est sympathique c'est qu'il est possible de transformer des sources javascript textuelles en un byte code propre à V8. C'est dans l'API. On peut donc mettre en cache des templates pre compilées, un peu comme le fait APC pour php, mais ça dépote encore plus! J'avais effectué quelques benchmarks qui laissaient penser que ce moteur de template à base de V8 était plus véloce que php+APC. Il faudrait que je mesure ça plus sérieusement.

A vrai dire ce projet de système de template a été codé à l'arrache courant novembre 2009. Un simple proof of concept à l'origine. Les dernières librairies de Boost notamment Spirit 2 (ça tue!), Phoenix 2 et Fusion 2 (OK, beaucoup de 2) m'ont donné envie de réécrire un tel système. J'héberge le truc sur github. Je lui ai donné le joli nom de r8t, réunion de rat et V8. Pour l'instant, c'est succinct. Pourquoi rat? Parce que ;)

older »