Cette application et ce tutoriel sont basés sur l'exemple de l'article Quelques cadriciels Web C++ publié sur LinuxFr.org.
Actuellement, il existe de nombreux langages et frameworks intéressants pour le développement Web côté serveur. Dans ce domaine, le C++ n'est pas le langage le plus en vogue, mais il possède des atouts intéressants. En effet:
Le but de ce tutoriel est de fournir :
Les codes sources de Cutelyst sont inclus ici. Les codes sources d'autres frameworks Web C++ sont disponibles sur ce référentiel Git. Les différents cadres utilisés sont résumés en annexe. Enfin, une liste de bibliothèques C++ est disponible sur Awesome C++.
Nous souhaitons implémenter une application qui affiche des images d'animaux stockées sur le serveur. Un formulaire permet d'indiquer le début du nom des animaux à afficher. Vous pouvez afficher l'image en taille réelle en cliquant sur la vignette et vous pouvez afficher une page d'information via un lien en bas de page. Les données sur les animaux (noms et chemins de fichiers) sont stockées dans une base de données SQLite sur le serveur.
Ici, la génération des pages HTML s'effectue sur le serveur, même si la tendance actuelle est plutôt de fournir une API côté serveur et de générer du HTML côté client.
De manière 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 nous utilisons une base de données SQLite contenant un tableau avec les noms et chemins de 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 renvoie les enregistrements de type Animal dont le nom commence par le préfixe donné. Fichier Animal.hpp :
La partie vue contient deux fonctions renvoyant des pages au format HTML : renderAbout renvoie la page d'informations et renderHome renvoie la page principale avec les animaux demandés par l'utilisateur. Vue du fichier.hpp :
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é à effectuer, juste récupérer les requêtes HTTP et appeler les fonctions précédentes.
C++ ne semble pas disposer d'outils de génération de documents HTML aussi performants que Lucid dans Haskell. La bibliothèque CTML permet de définir l'arborescence d'un document puis de générer le code HTML correspondant. Cependant, sa syntaxe est assez verbeuse et il n’y a aucune vérification des balises.
Ces systèmes consistent à écrire des modèles personnalisables, c'est-à-dire du code HTML dans lequel sont utilisés des paramètres qui seront remplacés par les valeurs indiquées lors du rendu du modèle.
Les frameworks MVC proposent généralement des systèmes de modèles avancés, mais il existe également des outils indépendants, par exemple moustache. Moustache est un formalisme qui a des implémentations dans de nombreux langages, dont plusieurs en C++. Par exemple, animal-pistache/src/View.cpp utilise l'implémentation de kainjow moustache et le code suivant ( cats-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 () {
...
}
Il est également relativement simple de générer du code HTML manuellement, à l'aide de flux de canaux C++. Cependant, cette méthode ne facilite pas la réutilisation du code ni la vérification du code HTML produit. Exemple de génération manuelle ( animaux-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 () {
...
}
Ils permettent de construire explicitement des requêtes SQL, de les envoyer au système de base de données et de récupérer le résultat. Les connecteurs SQL sont généralement simples à utiliser (il suffit de connaître 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 animaux-cppcms/src/Animal.cpp), tntnet (voir animaux-tntnet/src/Animal.cc) et silicium (voir animaux-silicon/src/main.cpp). Il existe également des connecteurs indépendants, par exemple sqlite_modern_cpp (voir animaux-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;
}
Le mappage objet-relationnel (ORM) est utilisé pour convertir les données d'une table d'une base de données en une classe C++, et vice versa. Cela permet d'utiliser la base de données de manière plus sécurisée car les données sont vérifiées par le système de typage et vérifiées à la compilation puisque les requêtes sont faites par des fonctions C++. Cependant, un ORM définit sa propre couche d’abstraction équivalente à SQL, mais forcément moins connue.
Il existe différents ORM C++, par exemple wt (voir animaux-wt/src/main.cpp), sqlpp11 (voir animaux-crow/src/Animal.cpp) ou sqlite_orm (voir animaux-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 micro web, comme Sinatra en Ruby ou Flask en Python, se veulent simples et légers. Ils offrent principalement des fonctionnalités pour gérer les requêtes HTTP ainsi qu'un mécanisme de routage d'URL. Si nécessaire, ils peuvent être complétés par d'autres librairies (génération HTML, accès à une base de données en SQL...).
Il existe plusieurs micro-frameworks C++, par exemple crow (voir animaux-corbeau) ou silicium (voir animaux-silicon).
Ici, les fonctionnalités du C++ moderne rendent le code concis et plutôt agréable à lire.
Dans une phase de prétraitement, Silicon génère le fichier symboles.hh, qui déclare les symboles définis par le programmeur, dont les routes ( _about, _home, _mystatic...). Cela permet de vérifier statiquement que les routes sont utilisées correctement dans le code. D'autres langages utilisent l'introspection pour effectuer ce type de vérification, mais C++ ne dispose pas de cette fonctionnalité.
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 nécessite une ressource, l'application peut basculer sur une autre requête en attendant que la ressource soit disponible. Cela améliore les performances globales de l'application mais nécessite un style de programmation particulier, basé sur des promesses liées à des fonctions de rappel pour ensuite former une chaîne de traitements asynchrones.
Il existe différents frameworks asynchrones en C++, par exemple cpprestsdk (voir animaux-cpprestsdk) et pistache (voir animaux-pistachio).
On retrouve ici une gestion classique de route (avec le nom de la route et sa fonction de traitement). Cependant, nous disposons désormais d'un fonctionnement asynchrone, via des fonctions non bloquantes. Par exemple, pour la route « statique », la fonction serveFile renvoie une promesse connectée à une fonction de rappel, qui affiche un message de journal une fois la promesse résolue.
Les frameworks web MVC, tels que Ruby on Rails ou Python Django sont des outils classiques dont le but est d'implémenter tout type d'applications web. Ils fournissent généralement toutes les fonctionnalités nécessaires : routage d'URL, système de templates, accès aux bases de données, système d'authentification... Les frameworks MVC ne semblent pas être le domaine privilégié du C++, mais il existe quand même quelques outils intéressants, notamment cppcms et cutelyst. .
En plus des fonctionnalités classiques d'un framework MVC, cppcms propose un système de template assez avancé 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 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 utilisant l'exemple précédent, nous pouvons définir un contenu MasterContent pour la vue MasterView, le dériver HomeContent pour la vue HomeView et utiliser directement MasterContent pour la vue AboutView (pas de nouveau paramètre dans le modèle).
Les frameworks MVC sont des outils efficaces pour implémenter des applications complexes. Cependant, ils nécessitent beaucoup de formation et peuvent être surdimensionnés pour des applications petites et simples.
Le framework tntnet propose un système basé sur des modèles, similaire à PHP. Même si ce framework est plutôt anecdotique dans l'écosystème C++, il semble plutôt efficace dans son approche : écrire du code HTML classique et ajouter des sections de code C++ là où c'est nécessaire.
A noter que ce type de framework est peut-être moins adapté au développement d'applications complexes (lisibilité des templates, réutilisation...).
Ces outils s'inspirent des frameworks graphiques de bureau, tels que Qt ou gtkmm, c'est-à-dire basés sur une hiérarchie de widgets qui composent l'interface et interagissent via un mécanisme de signal-slot.
Les widgets Web sont étonnamment impopulaires, même dans toutes les langues, alors que leur potentiel semble important. En effet, ils permettent de développer une application client-serveur fullstack en utilisant une bibliothèque d'interface graphique classique et sans trop se soucier de l'architecture réseau de l'application.
En C++, le framework le plus performant dans cette catégorie est certainement Wt. Wt possède de nombreux widgets classiques ou avancés, un ORM SQL, un système d'authentification, la possibilité de manipuler du HTML et du CSS, etc. Dans Wt, le programme principal est de router les URL vers les applications correspondantes.
Ces applications Wt correspondent à des interfaces graphiques classiques, mais avec une architecture client-serveur.
Pour une application plus complexe, par exemple la page affichant les animaux, on peut définir un nouveau widget qui implémente une vignette, puis utiliser cette classe pour afficher tous les animaux lus dans la base de données.
À première vue, cette implémentation peut paraître plus longue et plus compliquée que les implémentations précédentes. Cependant, son code devrait sembler familier à tout développeur d'interface graphique de bureau. De plus, cette implémentation gère l’intégralité de l’application (fullstack), et non seulement la partie serveur. Par exemple, connecter le signal _myquery->textInput()
à la HomeApp::filterAnimals
implique des mises à jour en temps réel côté client, ce qui serait beaucoup plus difficile à mettre en œuvre avec les frameworks précédents.
Pour développer des applications Web back-end, le C++ est une option très réalisable. Avec ses dernières évolutions, le langage est globalement plus simple et plus sûr à utiliser, sans compromis sur les performances. De nombreuses bibliothèques C++ sont disponibles pour le développement web : templates, génération HTML, connexion SQL, ORM... Les frameworks web sont également nombreux et variés : frameworks MVC comme RoR et Django, micro-frameworks comme Sinatra et Flask, frameworks asynchrones comme Node.js , des frameworks basés sur des modèles PHP et même des frameworks fullstack basés sur des widgets. Bien entendu, tout cela intéressera principalement les développeurs connaissant déjà le C++, car de nombreux autres langages disposent également d’outils très intéressants pour le développement web.
Les frameworks, bibliothèques et outils suivants utilisés pour la mise en œuvre du serveur HTTP, la génération HTML et l'accès à la base de données SQL.
Projet | Cadre Web | Générateur HTML | Interface SQL |
---|---|---|---|
animaux-cppcms | cppcms (cadre web) | cppcms (système de création de modèles) | cppcms (connecteur SQL) |
animaux-cpprestsdk | cpprestsdk (cadre de réseau asynchrone) | ctml (générateur de documents HTML) | sqlite_orm (ORM) |
animaux-corbeau | http : crow (framework Web léger) | corbeau (système de modèles) | sqlpp11 (ORM) |
animaux-cutelyst | cutelyst (framework web) | bénéficiaire (système de modèles) | cutelyst (connecteur SQL) |
animaux-nodejs (Javascript/Node.js) | express (framework Web léger asynchrone) | carlin (générateur de documents) | better-sqlite3 (connecteur SQL) |
animaux-pistache | pistache (framework web léger asynchrone) | moustache kainjow (système de modèles) | sqlite_modern_cpp (connecteur SQL) |
animaux-scotty (Haskell) | scotty (framework web léger) | lucid et clay (générateurs de documents) | sqlite-simple (connecteur SQL) |
animaux-silicium | silicium (framework Web léger) | aucun | silicium (connecteur SQL) |
animaux-tntnet | tntnet (framework Web basé sur un modèle) | tntnet (système de création de modèles) | tntnet (connecteur SQL) |
animaux-poids | wt (framework d'interface graphique Web) | wt (système de widgets + modèles) | poids (ORM) |