DLFP - Dépêches - Quelques frameworks web C++ (2/2)
- Décembre 2018 -
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 (partie 2). Enfin, une liste de bibliothèques C++ est disponible sur Awesome C++.
Partie 2 : frameworks web.
- lien n°1 : Précédente dépêche
Sommaire
- Les frameworks web
- Intégration avec Nix
- Conclusion
- Annexe : résumé des projets et des frameworks présentés
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
#include
#include
#include
#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 rendent 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
#include
#include
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 %>
<% template title() %> <%= title %> <% end %>
<% template page_content() %> to be overriden in sub-templates <% end %>
<% template render() %>
<html>
<head>
<style>
body {
background-color: azure;
}
...
style>
head>
<body>
<h1><% include title() %>h1>
<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 %>
<% view AboutView uses content::MasterContent extends MasterView %>
<% template page_content() %>
<p>Generated by <a href='http://cppcms.com/wikipp/en/page/main'>Cppcmsa>p>
<p><a href='<% url "/" %>'>Homea>p>
<% end template %>
<% end view %>
<% end skin %>
Le fichier animals-cppcms/src/about.tmpl définit la vue HomeView
:
<% skin myskin %>
<% view HomeView uses content::HomeContent extends MasterView %>
<% template page_content() %>
<form method="get" action="" >
<% form as_p info %>
form>
<% 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" %>'>Abouta>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">Abouta>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
// 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 principal 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"(
About (Wt)
Generated by Wt
)";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"(
Animals (Wt)
${myquery}
${animals}
)";// 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
par nokomprendo, Benoît Sibaud, ZeroHeure, palm123, Trollnad Dump, Xavier Claude
DLFP - Dépêches
LinuxFr.org
Tribune April : Techsoup et Solidatech, instruments d'influenceAprès une première position sur Solidatech en 2020, l'April a passé à nouveau du temps pour étudier et comprendre la place des structures Solidatech (...)
TuxRun et le noyau Linux
Il y a quelques années, je vous avais présenté TuxMake, un utilitaire pour faciliter la (cross-)compilation du noyau Linux supportant une grande (...)
Retour d’expérience sur l’utilisation de GrapheneOS (ROM Android libre)
Suite à la dépêche Comparatif : GrapheneOS vs LineageOS, je souhaitais faire part d’un retour d’expérience sur l’utilisation de GrapheneOS sur un (...)
Ubix Linux, le datalab de poche
Ubix Linux est une distribution Linux libre et open-source dérivée de Debian.Le nom « Ubix » est la forme contractée de « Ubics », acronyme issu de (...)
Open Food Facts : récit d’un contributeur
Récit de mon aventure en tant que contributeur pour le projet Open Food Facts, la base de donnée alimentaire ouverte et collaborative, où je suis (...)