Greboca  

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.

Sommaire

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

Home

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

About

)";// 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'influence

 -  27 mars - 

Aprè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

 -  27 mars - 

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)

 -  18 mars - 

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

 -  16 mars - 

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

 -  15 mars - 

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 (...)