Quelques frameworks web C++

Article linuxfr.org : partie 1 et partie 2

Actuellement, il existe de nombreux langages et frameworks intéressants pour le développement web backend. Dans ce domaine, le C++ n’est pas le langage le plus à la mode mais il possède cependant des atouts intéressants. En effet, le C++ possède de nombreuses bibliothèques (dont des frameworks web), il est réputé pour ses performances, enfin ses dernières normes le rendent plus agréable à utiliser.

L’objectif de cet article est de donner un aperçu des outils C++ disponibles pour le développement web backend, à partir d’un exemple d’application. Les codes sources présentés ici sont disponibles sur ce dépôt git. Les différents frameworks utilisés sont résumés en annexe. Enfin, une liste de bibliothèques C++ est disponible sur Awesome C++.

Sommaire :

Exemple d’application

Application finale

On veut implémenter une application qui permet d’afficher des images d’animaux stockées sur le serveur. Un formulaire permet d’indiquer le début du nom des animaux à afficher. On peut afficher l’image complète en cliquant sur la vignette et on peut afficher une page d’information via un lien en bas de page. Les données des animaux (noms et fichiers) sont stockées dans une base Sqlite sur le serveur.

Ici, la génération des pages HTML est effectuée sur le serveur, même si la tendance actuelle est plutôt de proposer une API côté serveur et de générer le code HTML côté client.

Architecture MVC

De façon très classique, on peut organiser le code de cette application selon une architecture de type MVC, c’est-à-dire en distinguant les données (Modèle), leur affichage (Vue) et leur gestion (Contrôleur).

Pour notre application, les images sont disponibles sur le serveur et on utilise une base de données Sqlite contenant une table avec les noms et fichiers des animaux. Fichier animals.sql:

CREATE TABLE animals (
  id INTEGER PRIMARY KEY, 
  name TEXT,
  image TEXT 
);

INSERT INTO animals (name, image) VALUES('dolphin', 'dolphin-marine-mammals-water-sea-64219.jpg');
INSERT INTO animals (name, image) VALUES('dog', 'night-garden-yellow-animal.jpg');
INSERT INTO animals (name, image) VALUES('owl', 'owl.jpg');
...

La partie Modèle se résume alors à un type Animal et à une fonction getAnimals qui interroge la base de données et retourne les Animal dont le nom commence par le préfixe donné. Fichier Animal.hpp:

#include <string>
#include <vector>

// Animal datatype
struct Animal {
  std::string name;
  std::string image;
};

// query database (select animals whose name begins with myquery)
std::vector<Animal> getAnimals(const std::string & myquery);

La partie Vue contient deux fonctions retournant des pages au format HTML : renderAbout retourne la page d’information et renderHome retourne la page principale avec les animaux demandés par l’utilisateur. Fichier View.hpp :

#include "Animal.hpp"

// render the about page to HTML
std::string renderAbout();

// render the home page to HTML
std::string renderHome(
    const std::string & myquery, 
    const std::vector<Animal> & animals);

Enfin la partie Contrôleur récupère les événements du client puis met à jour le modèle et la vue. Pour notre application, il n’y a pas de traitement compliqué à réaliser, juste à récupérer les requêtes HTTP et à appeler les fonctions précédentes.

Exemple en Javascript

Avant de voir comment développer cette application en C++, voici une implémentation possible en Javascript, basée sur le classique framework Node.js.

Pour accéder à la base de données, on peut utiliser le paquet better-sqlite3. Il suffit d’ouvrir la base de données, d’exécuter une requête SQL et de récupérer les données au format JSON. Fichier animals-nodejs/src/animals.js :

"use strict";

const db = require("better-sqlite3")("animals.db");

// query database (select animals whose name begins with myquery)
exports.getAnimals = myquery =>
    db.prepare("SELECT name,image FROM animals WHERE name LIKE ?||'%'").all(myquery);

Pour la vue, le paquet pug permet de générer du code HTML à partir d’une chaine de caractères, en utilisant un formatage particulier. Ceci apporte plusieurs avantages : le formatage utilisé est plus concis à écrire que du code HTML, il n’y a pas de risque d’oublier de fermer une balise HTML, on peut traiter facilement des données d’entrée, par exemple la liste des animaux à afficher… Fichier animals-nodejs/src/view.js :

"use strict";

const pug = require('pug');

// render the about page to HTML
const aboutFunc = pug.compile(`
doctype html
html
  head
    link(rel="stylesheet", type="text/css", href="static/style.css")
  body
    h1 About (Node.js)
    p Generated by 
        a(href="https://nodejs.org/en/") Node.js
        | , 
        a(href="https://expressjs.com/") Express
        | , 
        a(href="https://github.com/JoshuaWise/better-sqlite3") Better-sqlite3
        |  and  
        a(href="https://pugjs.org/api/getting-started.html") Pug
    a(href="/") Home
`);
exports.renderAbout = aboutFunc;

// render the home page to HTML
const homeFunc = pug.compile(`
doctype html
html
  head
    link(rel="stylesheet", type="text/css", href="static/style.css")
  body
    h1 Animals (Node.js)
    form(action="/", method="get")
        input(name="myquery", value=myquery)
    each animal in animals
        a(href="static/"+animal.image)
            div(class="divCss")
                p= animal.name
                img(src="static/"+animal.image, class="imgCss")
    p(style="clear:both")
        a(href="/about") About
`);
exports.renderHome = (myquery, animals) => homeFunc(myquery, animals);

À noter cependant que pug ne vérifie les types de balises. Par exemple, si on demande une balise toto, pug génèrera bien le code <toto> … </toto>, alors que cette balise n’existe pas dans la norme HTML.

Enfin, on utilise le très classique express, pour lancer un serveur avec routage des requêtes HTTP. Fichier animals-nodejs/src/app.js :

"use strict";

const port = 3000;

const view = require("./view.js")
const animal = require("./animal.js")

const express = require("express");
const app = express();

// serve the about page
app.get("/about", function (request, response) {
    const html = view.renderAbout();
    response.send(html);
});

// serve the home page (and filter the animals using the myquery parameter)
app.get("/", function (request, response) {
    const myquery = request.query.myquery ? request.query.myquery : ""
    const animals = animal.getAnimals(myquery);
    const html = view.renderHome({myquery, animals});
    response.send(html);
});

// serve static files (located in the "static" directory)
app.use("/static", express.static("./static"));

// run a server listening on port 3000
app.listen(port, function () {
    console.log(`Listening on port ${port}...`);
});

À noter que Node.js est un framework asynchrone, c’est-à-dire que des fonctions peuvent être appelées de façon non-bloquante. Ceci permet d’optimiser les performances de l’application générale, au prix d’un peu de complexité de programmation (Promise/callback, async/await…). Pour notre application, cela n’a pas vraiment d’influence car la principale fonction potentiellement concernée (la fonction d’accès à la base de données, via better-sqlite3) est bloquante.

Exemple en Haskell

L’application est également simple à implémenter en Haskell, avec le framework scotty.

Pour le modèle, on définit un type Animal et une fonction d’accès à la base de données par requête SQL via sqlite-simple. Fichier animals-scotty/src/Animal.hs :

{-# LANGUAGE OverloadedStrings #-}

module Animal where

import qualified Data.Text.Lazy as L
import qualified Database.SQLite.Simple as SQL 
import           Database.SQLite.Simple.FromRow (FromRow, fromRow, field)

-- Animal datatype
data Animal = Animal 
    { animalName :: L.Text
    , animalImage :: L.Text
    } deriving Show

-- deserialize an Animal from the database
instance FromRow Animal where
    fromRow = Animal <$> field <*> field 

-- query database (select animals whose name begins with myquery)
getAnimals :: L.Text -> IO [Animal]
getAnimals myquery = do
    let req = "SELECT name,image FROM animals WHERE name LIKE ?||'%'" 
    SQL.withConnection "animals.db" 
        (\conn -> SQL.query conn req (SQL.Only myquery))

Pour la génération du code HTML et du code CSS, Haskell dispose de DSL (Domain Specific Languages), ici lucid et clay. Ceci permet non seulement d’assurer le formatage correct des balises mais également que ces balises sont bien correctes. Ainsi, si on essaie de générer une balise toto (qui n’existe pas dans la norme HTML), le compilateur indiquera une erreur. Fichier animals-scotty/src/View.hs :

{-# LANGUAGE OverloadedStrings #-}

module View where

import           Animal
import qualified Clay as C
import           Control.Monad(forM_)
import           Lucid
import qualified Data.Text.Lazy as L

-- render the about page to HTML
aboutPage :: L.Text
aboutPage = renderText $ html_ $ do
    head_ $ style_ $ L.toStrict $ C.render $ myCss
    body_ $ do
        h1_ "About (Scotty)"
        p_ $ do 
            "Generated by "
            a_ [href_ "http://hackage.haskell.org/package/scotty"] "Scotty"
        p_ $ a_ [href_ "/"] "Home"

-- render the home page to HTML
homePage :: L.Text -> [Animal] -> L.Text
homePage myquery animals = renderText $ html_ $ do
    head_ $ style_ $ L.toStrict $ C.render $ myCss
    body_ $ do
        h1_ "Animals (Scotty)"
        -- add the HTML form
        form_ [action_ "/", method_ "get"] $ do
            input_ [name_ "myquery", value_ $ L.toStrict myquery]
        -- add every animal in a HTML div
        forM_ animals $ \ animal -> do
            let img = L.toStrict $ L.concat ["./img/", animalImage animal]
            a_ [href_ img] $ div_ [class_ "divCss"] $ do
                p_ $ toHtml $ animalName animal
                img_ [src_ img, class_ "imgCss"]
        p_ [style_ "clear: both"] $ a_ [href_ "/about"] "About"

-- our CSS styles
myCss :: C.Css
myCss = do
    C.a C.# C.byClass "aCss" C.? do
        C.textDecoration  C.none
        C.color           C.inherit
    C.body C.? do
        C.backgroundColor  C.azure
    C.div C.# C.byClass "divCss" C.? do
        C.backgroundColor  C.beige
        C.border           C.solid (C.px 1) C.black
        C.margin           (C.em 1) (C.em 1) (C.em 1) (C.em 1)
        C.width            (C.px 320)
        C.textAlign        C.center
        C.float            C.floatLeft
    C.img C.# C.byClass "imgCss" C.? do
        C.width            (C.px 320)
        C.height           (C.px 240)

Enfin, le serveur principal se résume à router les requêtes HTTP en utilisant les fonctions du framework scotty. Fichier animals-scotty/src/Main.hs :

{-# LANGUAGE OverloadedStrings #-}

import           Animal
import           View
import           Control.Monad.Trans (liftIO) 
import           Network.Wai.Middleware.RequestLogger (logStdoutDev)
import           Network.Wai.Middleware.Static (addBase, staticPolicy)
import           Web.Scotty (get, html, middleware, param, rescue, scotty) 

-- run a server listening on port 3000
main = scotty 3000 $ do

    -- show logs
    middleware logStdoutDev

    -- serve the about page
    get "/about" $ html aboutPage

    -- serve the home page (and filter the animals using the myquery parameter)
    get "/" $ do
        myquery <- param "myquery" `rescue` (\_ -> return "")
        animals <- liftIO $ getAnimals myquery
        html $ homePage myquery animals

    -- serve static files (located in the "static" directory)
    middleware $ staticPolicy $ addBase "static"

À noter que, par conception, Haskell permet également de faire de l’asynchrone assez facilement (langage fonctionnel pur, runtime supportant les green-threads…).

Au final, l’implémentation Haskell est assez similaire à l’implémentation Javascript. La principale différence est que Haskell fait du typage statique, ce qui allonge légèrement le code mais permet de détecter les erreurs de type plus précocement.

Génération de HTML

Les générateurs de documents HTML

C++ ne semble pas avoir d’outils de génération de documents HTML aussi aboutis que Lucid en Haskell. La bibliothèque CTML permet de définir la structure arborescente d’un document puis d’en générer le code HTML correspondant. Cependant, sa syntaxe est assez verbeuse et il n’y a pas de vérification des balises. Exemple d’utilisation (animals-cpprestsdk/src/View.cpp) :

const string css = R"(
    body {
      background-color: azure;
    }
    ...
)";

string renderHome(const string & myquery, const vector<Animal> & animals) {

    CTML::Document doc;

    doc.AddNodeToHead(
            CTML::Node("style", css));

    doc.AddNodeToBody(
            CTML::Node("h1", "Animals (Cpprestsdk)"));

    doc.AddNodeToBody(
            CTML::Node("form")
            .AppendChild(
                CTML::Node("input")
                .UseClosingTag(false)
                .SetAttribute("type", "text")
                .SetAttribute("name", "myquery")
                .SetAttribute("value", myquery)));

    for (const Animal & animal : animals)
        doc.AddNodeToBody(
                CTML::Node("a.aCss")
                .SetAttribute("href", "static/"+animal.image)
                .AppendChild(
                    CTML::Node("div.divCss")
                    .AppendChild(
                        CTML::Node("p", animal.name))
                    .AppendChild(
                        CTML::Node("img.imgCss")
                        .UseClosingTag(false)
                        .SetAttribute("src", "static/"+animal.image))));

    doc.AddNodeToBody(
            CTML::Node("p")
            .SetAttribute("style", "clear:both")
            .AppendChild(
                CTML::Node("a", "About")
                .SetAttribute("href", "/about")));

    return doc.ToString(CTML::Readability::MULTILINE);
}

string renderAbout() {
    ...
}

Les systèmes de templating

Ces systèmes consistent à écrire des templates paramétrables, c’est-à-dire du code HTML dans lequel on utilise des paramètres qui seront remplacés par les valeurs indiquées lors du rendu du template.

Les frameworks MVC proposent généralement des systèmes de templating évolués mais il existe également des outils indépendants, par exemple mustache. Mustache est un formalisme qui possède des implémentations dans de nombreux langages, dont plusieurs en C++. Par exemple, animal-pistache/src/View.cpp utilise l’implémentation kainjow mustache et le code suivant (animals-crow/src/View.cpp) l’implémentation du framework crow :

const string css = ...

string renderHome(const string & myquery, const vector<Animal> & animals) {

  // create the template 
  const string homeTmpl = R"(
    <html>
      <head>
        <style>
          {{mycss}}
        </style>
      </head>
      <body>
        <h1>Animals (Crow)</h1>
        <form>
          <p> <input type="text" name="myquery" value="{{myquery}}"> </p>
        </form>
        {{#animals}}
        <a href="static/{{image}}">
            <div class="divCss">
              <p> {{name}} </p>
              <img class="imgCss" src="static/{{image}}" />
            </div>
          </a>
        {{/animals}}
        <p style="clear: both"><a href="/about">About</a></p>
      </body>
    </html>
  )";

  // create a context containing the data to use in the template
  crow::mustache::context ctx;
  ctx["mycss"] = css;
  ctx["myquery"] = myquery;
  for (unsigned i=0; i<animals.size(); i++) {
    ctx["animals"][i]["name"] = animals[i].name;
    ctx["animals"][i]["image"] = animals[i].image;
  }

  // render the template using the context
  return crow::mustache::template_t(homeTmpl).render(ctx);
}

string renderAbout() {
    ...
}

Génération « à la main »

Il est également relativement simple de générer du code HTML manuellement, en utilisant les flux de chaines C++. Cependant cette méthode ne facilite pas la réutilisation de code ni la vérification du code HTML produit. Exemple de génération manuelle (animals-silicon/src/main.cpp) :

string renderHome(const string & myquery, const vector<Animal> & animals) {

  // create a string stream
  ostringstream oss;

  // generate some HTML code, in the stream
  oss << R"(
    <html>
      <head>
        <link rel="stylesheet" type="text/css" href="mystatic/style.css">
      </head>
      <body>
        <h1>Animals (Silicon)</h1>
        <form>
          <p> <input type="text" name="myquery" value=")" << myquery << R"("> </p>
        </form>
    )";

  for (const Animal & a : animals) {
    oss << R"(
        <a href="mystatic/)" << a.image << R"(">
            <div class="divCss">
              <p>)" << a.name << R"(</p>
              <img class="imgCss" src="mystatic/)" << a.image << R"(" />
            </div>
          </a>)";
  }

  oss << R"(
        <p style="clear: both"><a href="/about">About</a></p>
      </body>
    </html>
  )";

  // return the resulting string
  return oss.str();
}

string renderAbout() {
    ...
}

Accès à une base de données SQL

Les connecteurs SQL

Ils permettent de construire explicitement des requêtes SQL, de les envoyer au système de base de données et d’en récupérer le résultat. Les connecteurs SQL sont généralement faciles à utiliser (il suffit de connaitre le langage SQL) mais ils ne vérifient pas que les requêtes sont correctes.

De nombreux frameworks proposent des connecteurs SQL. Par exemple, cppcms (voir animals-cppcms/src/Animal.cpp), tntnet (voir animals-tntnet/src/Animal.cc) et silicon (voir animals-silicon/src/main.cpp). Il existe également des connecteurs indépendants, par exemple sqlite_modern_cpp (voir animals-pistache/src/Animal.cpp) :

#include "Animal.hpp"
#include <sqlite_modern_cpp.h>

using namespace sqlite;
using namespace std;

vector<Animal> getAnimals(const string & myquery) {

  vector<Animal> animals;

  try {
    // open database
    database db("animals.db");

    // query database and process results
    db << "SELECT name,image FROM animals WHERE name LIKE ?||'%'" 
      << myquery
      >> [&](string name, string image) { animals.push_back({name, image}); };
  }
  catch (exception & e) {
    cerr << e.what() << endl;
  }

  return animals;
}

Les ORM

Les ORM (Object-Relational Mapping) permettent de convertir des données d’une table SQL vers une classe C++, et réciproquement. Ceci permet d’utiliser la base de façon plus sûre car les données sont vérifiées par le système de typage et car les requêtes sont réalisés par des fonctions C++ et donc vérifiées à la compilation. Cependant, un ORM définit sa propre couche d’abstraction équivalente au SQL mais forcément moins connue.

Il existe différents ORM C++, par exemple wt dbo (voir animals-wt/src/main.cpp), sqlpp11 (voir animals-crow/src/Animal.cpp), ou sqlite_orm (voir animals-cpprestsdk/src/Animal.cpp):

#include "Animal.hpp"
#include <sqlite_orm/sqlite_orm.h>

using namespace std;
using namespace sqlite_orm;

vector<Animal> getAnimals(const string & myquery) {

    vector<Animal> animals;

    // open database and map the "animals" table to the "Animal" datatype
    auto storage = make_storage(
            "animals.db",
            make_table("animals",
                make_column("name", &Animal::name),
                make_column("image", &Animal::image)));

    // query database
    auto results = storage.get_all<Animal>(where(like(&Animal::name, myquery+"%")));

    // process results
    for(auto & animal : results)
        animals.push_back(animal);

    return animals;
}

Les frameworks web

Les micro-frameworks, à la Sinatra/Flask

Les micro-frameworks web, comme Sinatra en Ruby ou Flask en Python, ont pour objectif d’être simples et légers. Ils proposent principalement des fonctionnalités pour traiter des requêtes HTTP ainsi qu’un mécanisme de routage d’URL. Si nécessaire, ils peuvent être complétés par d’autres bibliothèques (génération de HTML, accès à une base SQL…).

Il existe plusieurs micro-frameworks C++, par exemple crow (voir animals-crow) ou silicon (voir animals-silicon) :

#include <silicon/api.hh>
#include <silicon/backends/mhd.hh>
#include <silicon/middlewares/sqlite_connection.hh>
#include <silicon/middleware_factories.hh>
#include "symbols.hh"

using namespace sl;
using namespace std;

...

int main() {

  // create app
  auto api = http_api(

    // serve the about page
    GET / _about = [] () { return renderAbout(); },

    // serve the home page (and filter the animals using the "myquery" parameter)
    GET / _animals * get_parameters(_myquery = optional(string())) =
      [] (const auto & p, sqlite_connection & c) {
        vector<Animal> animals = getAnimals(p.myquery, c);
        return renderHome(p.myquery, animals);
      },

    // serve static files (located in the "mystatic" directory)
    GET / _mystatic = file_server("./mystatic")

  );

  // create a connection factory to the database 
  auto factory = middleware_factories( sqlite_connection_factory("animals.db") );

  // run a server on port 3000
  mhd_json_serve(api, factory, 3000);
}

Ici, les fonctionnalités du C++ moderne rende le code concis et plutôt agréable à lire (par exemple la lambda pour la route _animals).

Dans une phase de prétraitement, Silicon génère le fichier symbols.hh, qui déclare les symboles définis par le programmeur, notamment les routes (_about, _home, _mystatic…). Ceci permet de vérifier statiquement que les routes sont utilisées correctement dans le code. D’autres langages utilisent l’introspection pour effectuer ce genre de vérification mais C++ ne possède pas cette fonctionnalité.

Les frameworks asynchrones, à la Node.js

Les frameworks asynchrones, comme Node.js/Express en Javascript, proposent les mêmes fonctionnalités que les micro-frameworks classiques mais via des fonctions non-bloquantes. Ainsi, si une requête a besoin d’une ressource, l’application peut passer à une autre requête en attendant que la ressource soit disponible. Ceci permet d’améliorer les performances générales de l’application mais nécessite un style de programmation particulier, à base de promesses connectées à des fonctions callbacks par des then pour former une chaine de traitements asynchrones.

Il existe différents frameworks asynchrones en C++, par exemple cpprestsdk (voir animals-cpprestsdk) et pistache (voir animals-pistache) :

#include "Animal.hpp"
#include "View.hpp"

#include <pistache/http.h>
#include <pistache/router.h>
#include <pistache/endpoint.h>

using namespace Pistache;
using namespace std;

// define server app
class App : public Http::Endpoint {

  private:
    Rest::Router router;

  public:
    App(Address addr) : Http::Endpoint(addr) {

      auto opts = Http::Endpoint::options()
        .flags(Tcp::Options::InstallSignalHandler)
        .flags(Tcp::Options::ReuseAddr);
      init(opts);

      // create a route for the about page
      Rest::Routes::Get(router, "/about", 
        [=](const Rest::Request &, Http::ResponseWriter response) {
            response.send(Http::Code::Ok, renderAbout());
            return Rest::Route::Result::Ok;
        });

      // create a route for the home page
      Rest::Routes::Get(router, "/", 
        [=](const Rest::Request & request, Http::ResponseWriter response) {
            auto myquery = request.query().get("myquery").getOrElse("");
            const vector<Animal> animals = getAnimals(myquery);
            response.send(Http::Code::Ok, renderHome(move(myquery), move(animals)));
            return Rest::Route::Result::Ok;
        });

      // create a route for serving static files
      Rest::Routes::Get(router, "/static/:filename", 
        [=](const Rest::Request & request, Http::ResponseWriter response) {
            auto filename = request.param(":filename").as<string>();
            // the Pistache API is non-blocking; for example, serveFile returns 
            // a Promise, for attaching a callback function
            Http::serveFile(response, "static/" + filename)
              .then(
                  [=](ssize_t s){ cout << filename << " (" << s << " bytes)" << endl; },
                  Async::NoExcept);
            return Rest::Route::Result::Ok;
        });

      setHandler(router.handler());
    }
};

// run server app on port 3000
int main() {
  App app({Ipv4::any(), 3000});
  app.serve();
}

On retrouve ici une gestion classique des routes (avec le nom de la route et sa fonction de traitement). Cependant, on a désormais un fonctionnement asynchrone, via des fonctions non bloquantes. Par exemple pour la route “static”, la fonction serveFile retourne une promesse que l’on connecte à une fonction callback, qui affiche un message de log une fois la promesse résolue.

Les frameworks MVC, à la RoR/Django

Les frameworks web MVC, comme Ruby on Rails ou Python Django, sont des outils classiques dont l’objectif est d’implémenter tout type d’application web. Ils fournissent généralement toutes les fonctionnalités nécessaires : routage d’URL, système de templating, accès à des bases de données, système d’authentification… Les frameworks MVC ne semblent pas être le domaine de prédilection du C++ mais on trouve tout de même quelques outils intéressants, notamment cppcms.

En plus des fonctionnalités classiques d’un framework MVC, cppcms propose un système de templating assez évolué, avec héritage de vues et gestion de contenu. Par exemple, on peut définir une vue principale MasterView et en dériver des vues AboutView et HomeView qui héritent des caractéristiques de MasterView et les complètent. Enfin, on peut associer un contenu à ces vues (paramètres des templates), également avec un système d’héritage. En reprenant l’exemple précédent, on peut définir un contenu MasterContent pour la vue MasterView, la dériver en HomeContent pour la vue HomeView et utiliser directement MasterContent pour la vue AboutView (pas de nouveau paramètre dans le template).

Au niveau du code, le fichier animals-cppcms/src/content.h définit les contenus :

// define how to exchange data between the C++ code and the templates
namespace content  {

  // content for MasterView and AboutView
  struct MasterContent : cppcms::base_content {
    std::string title;  // the "title" parameter in master.tmpl
  };

  // datatype for the form in HomeView
  struct InfoForm : cppcms::form {
    cppcms::widgets::text myquery; 
    InfoForm() {
      add(myquery);
    }
  };

  // content for HomeView 
  // inherits from MasterContent, because HomeView inherits from MasterView
  struct HomeContent : MasterContent {
    std::vector<Animal> animals;  // the "animals" parameter in home.tmpl
    InfoForm info;  // the "info" parameter in home.tmpl
  };

}

Le fichier animals-cppcms/src/master.tmpl définit la vue MasterView :

<% c++ #include "content.h" %>

<% skin myskin %>
  <% view MasterView uses content::MasterContent %>

    <!-- "title" is set in the C++ code and used in the "render" template below -->
    <% template title() %> <%= title %> <% end %>

    <!-- "page_content" is set in the sub-template and used in the "render" template below -->
    <% template page_content() %> to be overriden in sub-templates <% end %>

    <!-- main template that uses the two previous ones and that is called from the C++ code -->
    <% template render() %>
      <html>

        <head>
          <style>
            body {
              background-color: azure;
            }
            ...
          </style>
        </head>

        <body>
          <!-- adds the "title" template defined above  -->
          <h1><% include title() %></h1>
          <!-- adds the "page_content" template defined above  -->
          <div> <% include page_content() %> </div>
        </body>

      </html>
    <% end template %> 

  <% end view %>
<% end skin %>

Le fichier animals-cppcms/src/about.tmpl définit la vue AboutView :

<% skin myskin %>

  <!-- inherits from MasterView -->
  <% view AboutView uses content::MasterContent extends MasterView %>

    <!-- defines "page_content", that is used in MasterView -->
    <% template page_content() %>
      <p>Generated by <a href='http://cppcms.com/wikipp/en/page/main'>Cppcms</a></p>
      <p><a href='<% url "/" %>'>Home</a></p>
    <% end template %> 

  <% end view %>

<% end skin %>

Le fichier animals-cppcms/src/home.tmpl définit la vue HomeView :

<% skin myskin %>

  <!-- inherits from MasterView -->
  <% view HomeView uses content::HomeContent extends MasterView %>

    <!-- defines "page_content", that is used in MasterView -->
    <% template page_content() %>

      <!-- creates a form using the "info" attribute defined in HomeContent -->
      <form method="get" action="" >
        <% form as_p info %>
      </form>

      <!-- add the HTML elements corresponding to the "animals" attribute defined in HomeContent -->
      <% foreach animal in animals %>
        <% item %>
          <a class="aCss" href="img/<%= animal.image %>" >
            <div class="divCss">
            <p><%= animal.name %></p>
              <img class="imgCss" src="img/<%= animal.image %>" />
            </div>
          </a>
        <% end %>
      <% end foreach %>

      <p style="clear:both"><a href='<% url "/about" %>'>About</a></p>

    <% end template %> 

  <% end view %>
<% end skin %>

Enfin le programme principal définit le routage d’URL et initialise les contenus avant de lancer le rendu des vues. Fichier animals-cppcms/src/main.cpp :

// main application
class App : public cppcms::application {

  public:
    App(cppcms::service &srv) : cppcms::application(srv) {
      // about page
      dispatcher().assign("/about", &App::about, this);
      mapper().assign("about","/about");
      // home page
      dispatcher().assign("", &App::home, this);
      mapper().assign("");
      // images
      dispatcher().assign("/img/([a-z_0-9_\\-]+\\.jpg)", &App::serveJpg, this, 1);
      // root url
      mapper().root("/animals");
    }

  private:
    void about() {
      // AboutView inherits from MasterView and uses the same content type (MasterContent)
      content::MasterContent c;
      c.title = "About (Cppcms)";
      // render the AboutView template
      render("AboutView", c);
    }

    void home()  {
      // HomeView inherits from MasterView and uses its own content type 
      // (HomeContent, that inherits from MasterContent)
      content::HomeContent c;
      // data defined in MasterContent 
      c.title = "Animals (Cppcms)";
      // data defined in HomeContent
      c.info.load(context());
      c.animals = getAnimals(c.info.myquery.value());
      // render the HomeView template
      render("HomeView", c);
    }

    void serveJpg(string filename)  {
      // open and send the image file
      ifstream ifs("img/" + filename);
      if (ifs) {
        response().content_type("image/jpeg");
        response().out() << ifs.rdbuf();
      }
      else {
        response().status(404);
      }
    }
};

// create and run the application
int main(int argc, char ** argv) {
  try {
    cppcms::service srv(argc, argv);
    srv.applications_pool().mount(cppcms::applications_factory<App>());
    srv.run();
  }
  catch(exception const & e) {
    cerr << e.what() << endl;
  }
  return 0;
}

Les frameworks MVC sont des outils efficaces pour implémenter des applications complexes. Cependant, ils nécessitent un apprentissage assez conséquent et peuvent être surdimensionnés pour des petites applications simples.

Les frameworks basés templates, à la PHP

Le framework tntnet propose un système basé templates, à la manière de PHP. Si ce framework est assez anecdotique dans l’ecosystème C++, il semble cependant plutôt efficace dans son approche : écrire du code HTML classique et y ajouter des sections de code C++ là où c’est nécessaire.

Par exemple, le fichier animals-tntent/src/myimg.ecpp définit une application qui affiche une image dont le nom est passé en paramètre :

<%args>
  filename;
</%args>

<html>
  <body>
    <img src="static/img/<$filename$>" />
  </body>
</html>

De même, le fichier animals-tntent/src/home.ecpp définit une application plus complexe (appel de fonction C++, génération de code HTML via une boucle en C++…) :

<%args>
  myquery;
</%args>

<%pre>
  #include "Animal.hpp"
</%pre>

<html>
  <head>
    <link rel="stylesheet" type="text/css" href="static/style.css">
  </head>

  <body>

    <h1>Animals (Tntnet)</h1>

    <form>
      <p> <input type="text" name="myquery" value="<$myquery$>"> </p>
    </form>

    <%cpp> for (const Animal & animal : getAnimals(myquery)) { </%cpp>

      <a href="myimg?filename=<$animal.image$>">
        <div class="divCss">
          <p> <$animal.name$> </p>
          <img class="imgCss" src="static/img/<$animal.image$>" />
        </div>
      </a>

    <%cpp> } </%cpp>

    <p style="clear: both"><a href="/about">About</a></p>

  </body>
</html>

Enfin, tntnet propose différents types de déploiement : programme CGI, serveur autonome, serveur d’applications tntnet compilées dans des bibliothèques dynamiques. Par exemple, pour implémenter un serveur autonome (animals-tntent/src/main.cpp) :

#include <tnt/tntnet.h>

// run server on port 3000
int main() {
  try {
    tnt::Tntnet app;
    app.listen(3000);
    app.mapUrl("^/$", "home");        // route the "/" url to the "home" application 
    app.mapUrl("^/about$", "about");  // route the "/about" url to the "about" application 
    app.mapUrl("^/myimg$", "myimg");  // ...
    app.mapUrl("^/(static/.*)", "$1", "static@tntnet");
    app.run();
    return 0;
  }
  catch (const std::exception & e) {
    std::cerr << e.what() << std::endl;
    return -1;
  }
}

À noter que ce type de framework est peut-être moins adapté au développement d’applications complexes (lisibilité des templates, réutilisation…).

Les frameworks basés widgets

Ces outils s’inspirent des frameworks d’interfaces graphiques de bureau, comme Qt ou gtkmm, c’est-à-dire basés sur une hiérarchie de widgets composant l’interface et intéragissant via un mécanisme de signal-slot.

Les frameworks web basés widgets sont étonnament peu répandus, même tous langages confondus, alors que leur potentiel semble important. En effet, ils permettent de développer une application fullstack client-serveur en utilisant une bibliothèque d’interface graphique classique et sans avoir à trop se préoccuper de l’architecture réseau de l’application.

En C++, le framework le plus abouti dans cette catégorie est certainement Wt. Wt possède de nombreux widgets classiques ou évolué, un ORM SQL, un système d’authentification, la possibilité de manipuler du HTML/CSS… En Wt, le programme principale se résume à router des URL vers les applications correspondantes (animals-wt/src/main.cpp) :

...

int main(int argc, char ** argv) {
  try {
    WServer server(argc, argv, WTHTTP_CONFIGURATION);

    // route the url "/about" to an application "AboutApp"
    server.addEntryPoint(EntryPointType::Application, 
        [](const WEnvironment & env)
        { return make_unique<AboutApp>(env); },
        "/about");

    // route the url "/" to an application "HomeApp"
    server.addEntryPoint(EntryPointType::Application, 
        [=](const WEnvironment & env)
        { return make_unique<HomeApp>(env); },
        "/");

    server.run();
  }
  catch (Dbo::Exception & e) {
    cerr << "Dbo::Exception: " << e.what() << endl;
  }
  return 0;
}

Ces applications Wt correspondent à des interfaces graphiques classiques mais avec une architecture client-serveur. Par exemple pour définir l’application “about” (page statique) via le système de template HTML/CSS, il suffit de définir la classe suivante (animals-wt/src/AboutApp.hpp) :

...

// Application class implementing the about page
class AboutApp : public Wt::WApplication {
  private:
    // main HTML template of the application
    const std::string _app_template = R"(
        <h1>About (Wt)</h1>
        <p>Generated by <a href="https://www.webtoolkit.eu/wt">Wt</a></p>
        <p><a href="/">Home</a></p>
    )";

  public:
    // create the application
    AboutApp(const Wt::WEnvironment & env) : Wt::WApplication(env) {
      // load css
      useStyleSheet({"style.css"});
      // create the main widget using the HTML template
      root()->addWidget(std::make_unique<Wt::WTemplate>(_app_template));
    }
};

Pour une application plus complexe, par exemple la page affichant les animaux, on peut définir un nouveau widget AnimalWidget qui implémente une vignette, puis utiliser cette classe pour afficher tous les animaux lus dans la base de données (voir animals-wt/src/HomeApp.hpp) :

...

bool isPrefixOf(const std::string & txt, const std::string & fullTxt) {
  return std::inner_product(std::begin(txt), std::end(txt), std::begin(fullTxt), 
      true, std::logical_and<char>(), std::equal_to<char>());
} 

// widget showing an animal (name + image + anchor) 
class AnimalWidget : public Wt::WAnchor {
  private:
    // pointer to the WText that contains the animal name
      Wt::WText * _animalName;

  public:
    AnimalWidget(const Animal & animal) {
      // set anchor href
      const std::string imagePath = "img/" + animal.image;
      setLink(Wt::WLink(imagePath));
      // create a container widget, inside the anchor widget
      auto cAnimal = addWidget(std::make_unique<Wt::WContainerWidget>());
      cAnimal->setStyleClass("divCss");
      // create a text widget, inside the container
      auto cText = cAnimal->addWidget(std::make_unique<Wt::WContainerWidget>());
      cText->setPadding(Wt::WLength("1em"));
      _animalName = cText->addWidget(std::make_unique<Wt::WText>(animal.name));
      // create an image widget, inside the container
      auto img = cAnimal->addWidget(std::make_unique<Wt::WImage>(imagePath));
      img->setStyleClass("imgCss");
    }

    void filter(const std::string & txt) {
      // show the widget if txt is null or if it is a prefix of the animal name
      setHidden(txt != "" and not isPrefixOf(txt, _animalName->text().toUTF8()));
    }
};

// Application class implementing the home page
class HomeApp : public Wt::WApplication {
  private:
    // the line edit widget (for querying animal to show/hide)
      Wt::WLineEdit * _myquery;

    // the animal widgets 
    std::vector<AnimalWidget*> _animalWidgets;

    // main HTML template of the application
    const std::string _app_template = R"(
        <h1>Animals (Wt)</h1>
        <p>${myquery}</p>
        ${animals}
        <p style="clear: both"><a href="/about">About</a></p>
    )";

    // show all animals that match the _myquery prefix
    void filterAnimals() {
      for(auto aw : _animalWidgets)
        aw->filter(_myquery->text().toUTF8());
    }

  public:
    // create the application
    HomeApp(const Wt::WEnvironment & env) : WApplication(env) {
      // load css
      useStyleSheet({"style.css"});
      // create the main widget using the HTML template
      auto r = root()->addWidget(std::make_unique<Wt::WTemplate>(_app_template));
      // create the remaining widgets and bind them to the template placeholders
      _myquery = r->bindWidget("myquery", std::make_unique<Wt::WLineEdit>());
      // connect the widget _myquery to the function filterAnimals 
      _myquery->textInput().connect(this, &HomeApp::filterAnimals);

      // create a container widget for the animals
      auto w = r->bindWidget("animals", std::make_unique<Wt::WContainerWidget>());
      // open the database
      Wt::Dbo::Session session;
      session.setConnection(std::make_unique<Wt::Dbo::backend::Sqlite3>("animals.db"));
      session.mapClass<AnimalDb>("animals");
      // query the database
      Wt::Dbo::Transaction transaction(session);
      Wt::Dbo::collection<Wt::Dbo::ptr<AnimalDb>> dboAnimals = session.find<AnimalDb>();
      for (const Wt::Dbo::ptr<AnimalDb> & dboAnimal : dboAnimals) {
        // add a widget
        auto aw = w->addWidget(std::make_unique<AnimalWidget>(*dboAnimal));
        // store a pointer, for future updates
        _animalWidgets.push_back(aw);
      }
    }
};

À première vue, cette implémentation peut sembler plus longue et plus compliquée que les implémentations précédentes. Cependant, son code devrait sembler familier à n’importe quel développeur d’interface graphique de bureau. De plus, cette implémentation gère l’ensemble de l’application (fullstack), et non la partie serveur uniquement. Par exemple, la connexion du signal _myquery->textInput() à la fonction HomeApp::filterAnimals implique des mises-à-jour en temps-réel côté client, ce qui serait nettement plus difficile à implémenter avec les frameworks précédents.

Intégration avec Nix

Avant de conclure, voici une petite remarque concernant l’intégration, dans un projet de code, des frameworks présentés. En effet, ces frameworks sont rarement présents dans les logithèques des OS et doivent donc généralement être installées manuellement. Pour cela, on choisit classiquement l’une des deux solutions suivantes :

Aucune de ces solutions n’est vraiment satisfaisante : la première solution introduit une dépendance externe peu portable qu’il faut mettre à jour manuellement; la seconde solution introduit des duplications de code potentielles et nécessite une configuration de projet particulière.

Une troisième solution, particulièrement avantageuse, consiste à utiliser le système de paquets Nix. Par exemple, pour récupérer et empaquetter cpprestsdk depuis son projet github, il suffit d’écrire le fichier Nix suivant (voir animals-cpprestsdk/nix/cpprestsdk.nix) :

{ stdenv, fetchFromGitHub, cmake, boost, openssl, websocketpp, zlib }:

stdenv.mkDerivation {

  name = "cpprestsdk";

  src = fetchFromGitHub {
    owner = "Microsoft";
    repo = "cpprestsdk";
    rev = "204a52610234ac5180e80a6883b62c0ad085f51e";
    sha256 = "0mj2m6n889zdhwxdx24ljxfqryivvn3w9vzs94ppzcx5apa5jb3w";
  };

  enableParallelBuilding = true;

  buildInputs = [ boost cmake openssl websocketpp zlib ];
}

Pour inclure, dans un projet, des frameworks ainsi empaquetés, il suffit alors de les appeler dans le fichier de configuration default.nix du projet (voir animals-cpprestsdk/default.nix) :

with import <nixpkgs> {};

let
  _cpprestsdk = callPackage ./nix/cpprestsdk.nix {};
  _ctml = callPackage ./nix/ctml.nix {};
  _sqlite_orm = callPackage ./nix/sqlite_orm.nix {};
in

stdenv.mkDerivation {
  name = "animals-cpprestsdk";
  src = ./.;

  buildInputs = [
    boost
    _cpprestsdk
    _ctml
    openssl
    sqlite
    _sqlite_orm
  ];

  buildPhase = '' 
    g++ -O2 -o animals-cpprestsdk src/*.cpp -lcpprest -lboost_system -lssl -lsqlite3
    sqlite3 animals.db < animals.sql
  '';

  installPhase = ''
    mkdir -p $out/static
    cp animals-cpprestsdk $out/
    cp animals.db $out/
    cp $src/static/* $out/static/
  '';
}

On peut alors facilement compiler et lancer le programme, avec les commandes suivantes :

nix-build
cd result
./animals-cpprestsdk

Cette méthode à l’avantage de rendre les dépendances explicites et isolées mais sans duplication. Elle facilite également la réutilisation et les mises-à-jour.

Conclusion

Pour développer des applications web backend, le C++ est une option tout à fait envisageable. Avec ses dernières évolutions, le langage est généralement plus simple et plus sûr à utiliser, sans compromis sur les performances. De nombreuses bibliothèques C++ sont disponibles pour le développement web : templating, génération de HTML, connexion SQL, ORM… Les frameworks web sont également nombreux et variés : framework MVC à la RoR/Django, micro-framework à la Sinatra/Flask, framework asynchrone à la Node.js, framework basé template à la PHP, et même framework fullstack basé widget… Enfin, on notera qu’avec Nix, il est très facile de configurer un projet intégrant ce genre de bibliothèques. Bien évidemment, tout ceci interessera essentiellement des développeurs qui connaissent déjà C++ car beaucoup d’autres langages ont également des outils très intéressant pour le développement web.

Annexe : résumé des projets et des frameworks présentés

À noter qu’il s’agit uniquement d’exemples d’associations. Il est généralement possible d’utiliser n’importe quel framework web avec n’importe quel générateur HTML et n’importe quelle interface SQL.

projet framework web générateur HTML interface SQL
animals-cppcms cppcms (framework MVC) cppcms (système de templates) cppcms (connecteur SQL)
animals-cpprestsdk cpprestsdk (framework réseau asynchrone) ctml (générateur de documents) sqlite_orm (ORM)
animals-crow crow (micro-framework) crow (système de templates) sqlpp11 (ORM)
animals-nodejs (Javascript/Node.js) express (micro-framework asynchrone) pug (générateur de documents) better-sqlite3 (connecteur SQL)
animals-pistache pistache (micro-framework asynchrone) kainjow mustache (système de templates) sqlite_modern_cpp (connecteur SQL)
animals-scotty (Haskell) scotty (micro-framework) lucid and clay (générateurs de documents) sqlite-simple (connecteur SQL)
animals-silicon silicon (micro-framework) aucun silicon (connecteur SQL)
animals-tntnet tntnet (framework basé templates) tntnet (système de templates) tntnet (connecteur SQL)
animals-wt wt (framework basé widgets) wt (système de widgets + templates) wt (ORM)