Entries for tag json

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

Sun Nov 29 2009

Programming

boost, c++, json, variant

1 comment

Simple modélisation de JSON en C++

Il existe un certain nombre de parsers et autres librairies en C++ pour traiter du JSON (voir en bas de la page). Il manquerait Boost.PropertyTree qui a récemment été intégré dans boost.

En ce qui me concerne, je n'ai que très rarement besoins de manipuler du JSON de fond en comble, c'est à dire, par exemple, parser une chaine en JSON ou écrire dans un fichier avec ce format. Ce qui m'est utile en revanche, est de balancer des valeurs JSON-esques dans un container qui servira ensuite d'input à d'autres composants. C'est vague, je sais, c'est fait pour.

Le container en question est donc sommé d'accepter des strings, tableaux, objets, booléens etc. Bref, tout ce qui fait l'intérêt de JSON. Une des techniques que j'utilise est basée sur Boost.Variant. L'intérêt de Boost.Variant par rapport à Boost.Any est qu'il est type safe. Un mécanisme de visiteur permet de manipuler précisément les types wrappés, ce qui est utile lorsque le container en question n'est qu'un container de "passage" entre composants. L'autre intérêt de variant est sa propension à être récursif, tout comme JSON. Ca tombe bien.

Dans le code qui suit, json_type est le type généric qui modélise une valeur JSON. json_array, json_object sont des json_type. En plus, on accepte des std::string, bool etc. Ce n'est pas une implémentation de JSON fidèle au bit près. Comme je le disais, je veux juste du JSON-esque:

typedef boost::make_recursive_variant
<
    int
  , double
  , bool
  , std::string
  , std::unordered_map<std::string, boost::recursive_variant_> // object
  , std::vector<boost::recursive_variant_> // array

>::type json_type; // notre type generic

typedef std::unordered_map<std::string, json_type> json_object;

typedef std::vector<json_type> json_array;

On notera l'utilisation de std::unordered_map. Un boost::unordered_map ferait tout autant l'affaire. boost::make_recursive_variant permet de rendre le variant récursif. Il accepte dès lors sans broncher un std::vector<boost::recursive_variant_>. Vous aurez compris qu'il s'agit de la modélisation du array JSON. Un typedef qui va bien permet de déclarer json_array à partir de std::vector<json_type>. La boucle est bouclée.

A l'utilisation, ça donne ce genre de chose:

json_array arr {3.14, 21, true}; //initializer list goodies (C++0x)
  
json_object obj;
obj["key"] = 42;
obj["str"] = std::string("hello");
obj["arr"] = arr;

Ok, mais faire ça ne sert à rien. Effectivement. Rappelez-vous, il s'agit d'un container de passage. Un autre composant va devoir se démerder (encore une fois, sans broncher) pour faire quelque chose d'utile avec. C'est là que boost::static_visitor entre en jeu. On va pouvoir traiter finement les types wrappés (std::string, int, std::vector...). Le principe est le suivant:

// un type T quelconque (pseudo code)
struct json_visitor : public boost::static_visitor<T>
{
    T operator()(const json_object& val) const
    {
        //...
    }

    T operator()(const json_array& val) const
    {
        //...
    }

    T operator()(const std::string& val) const
    {
        //...
    }
    
    // ...
};

// on applique le visitor à notre json_type
boost::apply_visitor(json_visitor(), json_type);

Par exemple, faire un output au format JSON de notre json_type. On va déclarer un visitor dérivant de boost::static_visitor<std::string> qui se chargera de formater le tout:

struct json_conv_visitor : public boost::static_visitor<std::string>
{
    std::string operator()(const json_object& val) const
    {
        std::ostringstream os;
        os << "{";
        for (json_object::const_iterator i(val.begin()), e(val.end())
            ; i != e; ++i)
        {
            os << i->first << ": "
               << boost::apply_visitor(json_conv_visitor(), i->second)
               << ", ";
        }
        os << "}";
        return os.str();
    }

    std::string operator()(const json_array& val) const
    {
        std::ostringstream os;
        os << "[";
        for (json_array::const_iterator i(val.begin()), e(val.end())
            ; i != e; ++i)
        {
            os << boost::apply_visitor(json_conv_visitor(), *i) 
               << ", ";
        }
        os << "]";
        return os.str();
    }

    std::string operator()(const std::string& val) const
    {
        return std::string("\"").append(val).append("\"");
    }
    
    template<typename T> // le reste est refilé à boost::lexical_cast
    std::string operator()(const T& val) const
    {
        return boost::lexical_cast<std::string>(val);
    }
};

Appliqué à l'exemple plus haut, ça retourne:

{str: "hello", key: 42, arr: [3.1400000000000001, 21, 1, ], }

Voila, le code complet est dispo sur github ici. Si ça vous intéresse, servez-vous. Un de ces 4, on parlera d'une manière de parser une chaine JSON pour filler ce genre de variant, sans utiliser Spirit. Sur ce, a+