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
- Génération de HTML
- Accès à une base de données SQL
- Les frameworks web
- Intégration avec Nix
- Conclusion
- Annexe : résumé des projets et des frameworks présentés
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)
.getAnimals = myquery =>
exports.prepare("SELECT name,image FROM animals WHERE name LIKE ?||'%'").all(myquery); db
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
`);
.renderAbout = aboutFunc;
exports
// 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
`);
.renderHome = (myquery, animals) => homeFunc(myquery, animals); exports
À 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
.get("/about", function (request, response) {
appconst html = view.renderAbout();
.send(html);
response;
})
// serve the home page (and filter the animals using the myquery parameter)
.get("/", function (request, response) {
appconst myquery = request.query.myquery ? request.query.myquery : ""
const animals = animal.getAnimals(myquery);
const html = view.renderHome({myquery, animals});
.send(html);
response;
})
// serve static files (located in the "static" directory)
.use("/static", express.static("./static"));
app
// run a server listening on port 3000
.listen(port, function () {
appconsole.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
= Animal <$> field <*> field
fromRow
-- query database (select animals whose name begins with myquery)
getAnimals :: L.Text -> IO [Animal]
= do
getAnimals myquery let req = "SELECT name,image FROM animals WHERE name LIKE ?||'%'"
"animals.db"
SQL.withConnection -> SQL.query conn req (SQL.Only myquery)) (\conn
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
= renderText $ html_ $ do
aboutPage $ style_ $ L.toStrict $ C.render $ myCss
head_ $ do
body_ "About (Scotty)"
h1_ $ do
p_ "Generated by "
"http://hackage.haskell.org/package/scotty"] "Scotty"
a_ [href_ $ a_ [href_ "/"] "Home"
p_
-- render the home page to HTML
homePage :: L.Text -> [Animal] -> L.Text
= renderText $ html_ $ do
homePage myquery animals $ style_ $ L.toStrict $ C.render $ myCss
head_ $ do
body_ "Animals (Scotty)"
h1_ -- add the HTML form
"/", method_ "get"] $ do
form_ [action_ "myquery", value_ $ L.toStrict myquery]
input_ [name_ -- add every animal in a HTML div
$ \ animal -> do
forM_ animals let img = L.toStrict $ L.concat ["./img/", animalImage animal]
$ div_ [class_ "divCss"] $ do
a_ [href_ img] $ toHtml $ animalName animal
p_ "imgCss"]
img_ [src_ img, class_ "clear: both"] $ a_ [href_ "/about"] "About"
p_ [style_
-- our CSS styles
myCss :: C.Css
= do
myCss C.# C.byClass "aCss" C.? do
C.a
C.textDecoration C.none
C.color C.inheritC.? do
C.body
C.backgroundColor C.azureC.# C.byClass "divCss" C.? do
C.div
C.backgroundColor C.beige1) C.black
C.border C.solid (C.px 1) (C.em 1) (C.em 1) (C.em 1)
C.margin (C.em 320)
C.width (C.px
C.textAlign C.center
C.float C.floatLeftC.# C.byClass "imgCss" C.? do
C.img 320)
C.width (C.px 240) C.height (C.px
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
= scotty 3000 $ do
main
-- show logs
middleware logStdoutDev
-- serve the about page
"/about" $ html aboutPage
get
-- serve the home page (and filter the animals using the myquery parameter)
"/" $ do
get <- param "myquery" `rescue` (\_ -> return "")
myquery <- liftIO $ getAnimals myquery
animals $ homePage myquery animals
html
-- serve static files (located in the "static" directory)
$ staticPolicy $ addBase "static" middleware
À 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;
}
...
)";
(const string & myquery, const vector<Animal> & animals) {
string renderHome
::Document doc;
CTML
.AddNodeToHead(
doc::Node("style", css));
CTML
.AddNodeToBody(
doc::Node("h1", "Animals (Cpprestsdk)"));
CTML
.AddNodeToBody(
doc::Node("form")
CTML.AppendChild(
::Node("input")
CTML.UseClosingTag(false)
.SetAttribute("type", "text")
.SetAttribute("name", "myquery")
.SetAttribute("value", myquery)));
for (const Animal & animal : animals)
.AddNodeToBody(
doc::Node("a.aCss")
CTML.SetAttribute("href", "static/"+animal.image)
.AppendChild(
::Node("div.divCss")
CTML.AppendChild(
::Node("p", animal.name))
CTML.AppendChild(
::Node("img.imgCss")
CTML.UseClosingTag(false)
.SetAttribute("src", "static/"+animal.image))));
.AddNodeToBody(
doc::Node("p")
CTML.SetAttribute("style", "clear:both")
.AppendChild(
::Node("a", "About")
CTML.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 = ...
(const string & myquery, const vector<Animal> & animals) {
string renderHome
// 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
::mustache::context ctx;
crow["mycss"] = css;
ctx["myquery"] = myquery;
ctxfor (unsigned i=0; i<animals.size(); i++) {
["animals"][i]["name"] = animals[i].name;
ctx["animals"][i]["image"] = animals[i].image;
ctx}
// 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) :
(const string & myquery, const vector<Animal> & animals) {
string renderHome
// create a string stream
;
ostringstream oss
// generate some HTML code, in the stream
<< R"(
oss <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) {
<< R"(
oss <a href="mystatic/)" << a.image << R"(">
<div class="divCss">
<p>)" << a.name << R"(</p>
<img class="imgCss" src="mystatic/)" << a.image << R"(" />
</div>
</a>)";
}
<< R"(
oss <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;
<Animal> getAnimals(const string & myquery) {
vector
<Animal> animals;
vector
try {
// open database
("animals.db");
database db
// query database and process results
<< "SELECT name,image FROM animals WHERE name LIKE ?||'%'"
db << myquery
>> [&](string name, string image) { animals.push_back({name, image}); };
}
catch (exception & e) {
<< e.what() << endl;
cerr }
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;
<Animal> getAnimals(const string & myquery) {
vector
<Animal> animals;
vector
// open database and map the "animals" table to the "Animal" datatype
auto storage = make_storage(
"animals.db",
("animals",
make_table("name", &Animal::name),
make_column("image", &Animal::image)));
make_column
// query database
auto results = storage.get_all<Animal>(where(like(&Animal::name, myquery+"%")));
// process results
for(auto & animal : results)
.push_back(animal);
animals
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
/ _about = [] () { return renderAbout(); },
GET
// serve the home page (and filter the animals using the "myquery" parameter)
/ _animals * get_parameters(_myquery = optional(string())) =
GET [] (const auto & p, sqlite_connection & c) {
<Animal> animals = getAnimals(p.myquery, c);
vectorreturn renderHome(p.myquery, animals);
},
// serve static files (located in the "mystatic" directory)
/ _mystatic = file_server("./mystatic")
GET
);
// create a connection factory to the database
auto factory = middleware_factories( sqlite_connection_factory("animals.db") );
// run a server on port 3000
(api, factory, 3000);
mhd_json_serve}
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:
::Router router;
Rest
public:
(Address addr) : Http::Endpoint(addr) {
App
auto opts = Http::Endpoint::options()
.flags(Tcp::Options::InstallSignalHandler)
.flags(Tcp::Options::ReuseAddr);
(opts);
init
// create a route for the about page
::Routes::Get(router, "/about",
Rest[=](const Rest::Request &, Http::ResponseWriter response) {
.send(Http::Code::Ok, renderAbout());
responsereturn Rest::Route::Result::Ok;
});
// create a route for the home page
::Routes::Get(router, "/",
Rest[=](const Rest::Request & request, Http::ResponseWriter response) {
auto myquery = request.query().get("myquery").getOrElse("");
const vector<Animal> animals = getAnimals(myquery);
.send(Http::Code::Ok, renderHome(move(myquery), move(animals)));
responsereturn Rest::Route::Result::Ok;
});
// create a route for serving static files
::Routes::Get(router, "/static/:filename",
Rest[=](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
::serveFile(response, "static/" + filename)
Http.then(
[=](ssize_t s){ cout << filename << " (" << s << " bytes)" << endl; },
::NoExcept);
Asyncreturn Rest::Route::Result::Ok;
});
(router.handler());
setHandler}
};
// run server app on port 3000
int main() {
({Ipv4::any(), 3000});
App app.serve();
app}
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 {
::widgets::text myquery;
cppcms() {
InfoForm(myquery);
add}
};
// content for HomeView
// inherits from MasterContent, because HomeView inherits from MasterView
struct HomeContent : MasterContent {
std::vector<Animal> animals; // the "animals" parameter in home.tmpl
; // the "info" parameter in home.tmpl
InfoForm info};
}
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:
(cppcms::service &srv) : cppcms::application(srv) {
App// about page
().assign("/about", &App::about, this);
dispatcher().assign("about","/about");
mapper// home page
().assign("", &App::home, this);
dispatcher().assign("");
mapper// images
().assign("/img/([a-z_0-9_\\-]+\\.jpg)", &App::serveJpg, this, 1);
dispatcher// root url
().root("/animals");
mapper}
private:
void about() {
// AboutView inherits from MasterView and uses the same content type (MasterContent)
::MasterContent c;
content.title = "About (Cppcms)";
c// render the AboutView template
("AboutView", c);
render}
void home() {
// HomeView inherits from MasterView and uses its own content type
// (HomeContent, that inherits from MasterContent)
::HomeContent c;
content// data defined in MasterContent
.title = "Animals (Cppcms)";
c// data defined in HomeContent
.info.load(context());
c.animals = getAnimals(c.info.myquery.value());
c// render the HomeView template
("HomeView", c);
render}
void serveJpg(string filename) {
// open and send the image file
("img/" + filename);
ifstream ifsif (ifs) {
().content_type("image/jpeg");
response().out() << ifs.rdbuf();
response}
else {
().status(404);
response}
}
};
// create and run the application
int main(int argc, char ** argv) {
try {
::service srv(argc, argv);
cppcms.applications_pool().mount(cppcms::applications_factory<App>());
srv.run();
srv}
catch(exception const & e) {
<< e.what() << endl;
cerr }
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 {
::Tntnet app;
tnt.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();
appreturn 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 {
(argc, argv, WTHTTP_CONFIGURATION);
WServer server
// route the url "/about" to an application "AboutApp"
.addEntryPoint(EntryPointType::Application,
server[](const WEnvironment & env)
{ return make_unique<AboutApp>(env); },
"/about");
// route the url "/" to an application "HomeApp"
.addEntryPoint(EntryPointType::Application,
server[=](const WEnvironment & env)
{ return make_unique<HomeApp>(env); },
"/");
.run();
server}
catch (Dbo::Exception & e) {
<< "Dbo::Exception: " << e.what() << endl;
cerr }
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
(const Wt::WEnvironment & env) : Wt::WApplication(env) {
AboutApp// load css
({"style.css"});
useStyleSheet// create the main widget using the HTML template
()->addWidget(std::make_unique<Wt::WTemplate>(_app_template));
root}
};
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
::WText * _animalName;
Wt
public:
(const Animal & animal) {
AnimalWidget// set anchor href
const std::string imagePath = "img/" + animal.image;
(Wt::WLink(imagePath));
setLink// create a container widget, inside the anchor widget
auto cAnimal = addWidget(std::make_unique<Wt::WContainerWidget>());
->setStyleClass("divCss");
cAnimal// create a text widget, inside the container
auto cText = cAnimal->addWidget(std::make_unique<Wt::WContainerWidget>());
->setPadding(Wt::WLength("1em"));
cText= cText->addWidget(std::make_unique<Wt::WText>(animal.name));
_animalName // create an image widget, inside the container
auto img = cAnimal->addWidget(std::make_unique<Wt::WImage>(imagePath));
->setStyleClass("imgCss");
img}
void filter(const std::string & txt) {
// show the widget if txt is null or if it is a prefix of the animal name
(txt != "" and not isPrefixOf(txt, _animalName->text().toUTF8()));
setHidden}
};
// Application class implementing the home page
class HomeApp : public Wt::WApplication {
private:
// the line edit widget (for querying animal to show/hide)
::WLineEdit * _myquery;
Wt
// 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)
->filter(_myquery->text().toUTF8());
aw}
public:
// create the application
(const Wt::WEnvironment & env) : WApplication(env) {
HomeApp// load css
({"style.css"});
useStyleSheet// 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
= r->bindWidget("myquery", std::make_unique<Wt::WLineEdit>());
_myquery // connect the widget _myquery to the function filterAnimals
->textInput().connect(this, &HomeApp::filterAnimals);
_myquery
// create a container widget for the animals
auto w = r->bindWidget("animals", std::make_unique<Wt::WContainerWidget>());
// open the database
::Dbo::Session session;
Wt.setConnection(std::make_unique<Wt::Dbo::backend::Sqlite3>("animals.db"));
session.mapClass<AnimalDb>("animals");
session// query the database
::Dbo::Transaction transaction(session);
Wt::Dbo::collection<Wt::Dbo::ptr<AnimalDb>> dboAnimals = session.find<AnimalDb>();
Wtfor (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
.push_back(aw);
_animalWidgets}
}
};
À 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 :
télécharger le code source de la bibliothèque, l’installer dans un dossier local ou système et régler les variables d’environnement de façon à trouver la bibliothèque;
intégrer la bibliothèque directement au projet, par exemple via les sous-modules git, et configurer le projet de façon à trouver la bibliothèque.
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) |