Le jour où j'ai codé un service de paste en C

Posted on Thu 06 February 2020 in code, annonce

En ce moment j'aime bien refaire du C. Pourquoi ? Parce que le C est un langage simple et son élégance m'avait manqué.

Bien que j'aime le C++ moderne, je ne peux m'empêcher d'être plutôt d'accord avec ceux qui pensent que c'est un langage de plus en plus complexe. C'est le cas et chaque version amène un lot de complexité supplémentaire.

C++

Rapide coup d'œil sur les parties du C++ qui ont tendance à faire grincer des dents.

L'initialisation

Parce qu'une image vaut plus que des mots :

init

Il existe autant de moyens d'initialiser une variable en C++ qu'il existe de politiciens corrompus. En C++20 il y en a encore deux autres : constinit et consteval. Ces deux mots clés sont principalement utilisés pour gérer les problèmes liés à l'initialisation statique.

Les templates

Je n'ai pas besoin d'écrire un livre, un simple morceau de code suffit :

In file included from /usr/include/c++/9.2.0/map:60,
                 from test.cpp:1:
/usr/include/c++/9.2.0/bits/stl_tree.h: In instantiation of 'std::pair<std::_Rb_tree_node_base*, std::_Rb_tree_node_base*> std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_M_get_insert_unique_pos(const key_type&) [with _Key = int; _Val = std::pair<const int, int>; _KeyOfValue = std::_Select1st<std::pair<const int, int> >; _Compare = main()::myalloc; _Alloc = std::allocator<std::pair<const int, int> >; std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::key_type = int]':
/usr/include/c++/9.2.0/bits/stl_tree.h:2413:19:   required from 'std::pair<std::_Rb_tree_iterator<_Val>, bool> std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_M_emplace_unique(_Args&& ...) [with _Args = {int, int}; _Key = int; _Val = std::pair<const int, int>; _KeyOfValue = std::_Select1st<std::pair<const int, int> >; _Compare = main()::myalloc; _Alloc = std::allocator<std::pair<const int, int> >]'
/usr/include/c++/9.2.0/bits/stl_map.h:575:64:   required from 'std::pair<typename std::_Rb_tree<_Key, std::pair<const _Key, _Tp>, std::_Select1st<std::pair<const _Key, _Tp> >, _Compare, typename __gnu_cxx::__alloc_traits<_Alloc>::rebind<std::pair<const _Key, _Tp> >::other>::iterator, bool> std::map<_Key, _Tp, _Compare, _Alloc>::emplace(_Args&& ...) [with _Args = {int, int}; _Key = int; _Tp = int; _Compare = main()::myalloc; _Alloc = std::allocator<std::pair<const int, int> >; typename std::_Rb_tree<_Key, std::pair<const _Key, _Tp>, std::_Select1st<std::pair<const _Key, _Tp> >, _Compare, typename __gnu_cxx::__alloc_traits<_Alloc>::rebind<std::pair<const _Key, _Tp> >::other>::iterator = std::_Rb_tree_iterator<std::pair<const int, int> >]'
test.cpp:9:16:   required from here
/usr/include/c++/9.2.0/bits/stl_tree.h:2095:11: error: no match for call to '(main()::myalloc) (const key_type&, const int&)'
 2095 |    __comp = _M_impl._M_key_compare(__k, _S_key(__x));
/usr/include/c++/9.2.0/bits/stl_tree.h:2106:7: error: no match for call to '(main()::myalloc) (const int&, const key_type&)'
 2106 |       if (_M_impl._M_key_compare(_S_key(__j._M_node), __k))
      |       ^~
/usr/include/c++/9.2.0/bits/stl_tree.h: In instantiation of 'static const _Key& std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_S_key(std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_Const_Link_type) [with _Key = int; _Val = std::pair<const int, int>; _KeyOfValue = std::_Select1st<std::pair<const int, int> >; _Compare = main()::myalloc; _Alloc = std::allocator<std::pair<const int, int> >; std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_Const_Link_type = const std::_Rb_tree_node<std::pair<const int, int> >*]':
/usr/include/c++/9.2.0/bits/stl_tree.h:2413:50:   required from 'std::pair<std::_Rb_tree_iterator<_Val>, bool> std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_M_emplace_unique(_Args&& ...) [with _Args = {int, int}; _Key = int; _Val = std::pair<const int, int>; _KeyOfValue = std::_Select1st<std::pair<const int, int> >; _Compare = main()::myalloc; _Alloc = std::allocator<std::pair<const int, int> >]'
/usr/include/c++/9.2.0/bits/stl_map.h:575:64:   required from 'std::pair<typename std::_Rb_tree<_Key, std::pair<const _Key, _Tp>, std::_Select1st<std::pair<const _Key, _Tp> >, _Compare, typename __gnu_cxx::__alloc_traits<_Alloc>::rebind<std::pair<const _Key, _Tp> >::other>::iterator, bool> std::map<_Key, _Tp, _Compare, _Alloc>::emplace(_Args&& ...) [with _Args = {int, int}; _Key = int; _Tp = int; _Compare = main()::myalloc; _Alloc = std::allocator<std::pair<const int, int> >; typename std::_Rb_tree<_Key, std::pair<const _Key, _Tp>, std::_Select1st<std::pair<const _Key, _Tp> >, _Compare, typename __gnu_cxx::__alloc_traits<_Alloc>::rebind<std::pair<const _Key, _Tp> >::other>::iterator = std::_Rb_tree_iterator<std::pair<const int, int> >]'
test.cpp:9:16:   required from here
/usr/include/c++/9.2.0/bits/stl_tree.h:772:16: error: static assertion failed: comparison object must be invocable with two arguments of key type
  772 |  static_assert(__is_invocable<_Compare&, const _Key&, const _Key&>{},
      |                ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/9.2.0/bits/stl_tree.h: In instantiation of 'std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::iterator std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_M_insert_node(std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_Base_ptr, std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_Base_ptr, std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_Link_type) [with _Key = int; _Val = std::pair<const int, int>; _KeyOfValue = std::_Select1st<std::pair<const int, int> >; _Compare = main()::myalloc; _Alloc = std::allocator<std::pair<const int, int> >; std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::iterator = std::_Rb_tree_iterator<std::pair<const int, int> >; std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_Base_ptr = std::_Rb_tree_node_base*; std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_Link_type = std::_Rb_tree_node<std::pair<const int, int> >*]':
/usr/include/c++/9.2.0/bits/stl_tree.h:2415:20:   required from 'std::pair<std::_Rb_tree_iterator<_Val>, bool> std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_M_emplace_unique(_Args&& ...) [with _Args = {int, int}; _Key = int; _Val = std::pair<const int, int>; _KeyOfValue = std::_Select1st<std::pair<const int, int> >; _Compare = main()::myalloc; _Alloc = std::allocator<std::pair<const int, int> >]'
/usr/include/c++/9.2.0/bits/stl_map.h:575:64:   required from 'std::pair<typename std::_Rb_tree<_Key, std::pair<const _Key, _Tp>, std::_Select1st<std::pair<const _Key, _Tp> >, _Compare, typename __gnu_cxx::__alloc_traits<_Alloc>::rebind<std::pair<const _Key, _Tp> >::other>::iterator, bool> std::map<_Key, _Tp, _Compare, _Alloc>::emplace(_Args&& ...) [with _Args = {int, int}; _Key = int; _Tp = int; _Compare = main()::myalloc; _Alloc = std::allocator<std::pair<const int, int> >; typename std::_Rb_tree<_Key, std::pair<const _Key, _Tp>, std::_Select1st<std::pair<const _Key, _Tp> >, _Compare, typename __gnu_cxx::__alloc_traits<_Alloc>::rebind<std::pair<const _Key, _Tp> >::other>::iterator = std::_Rb_tree_iterator<std::pair<const int, int> >]'
test.cpp:9:16:   required from here
/usr/include/c++/9.2.0/bits/stl_tree.h:2358:8: error: no match for call to '(main()::myalloc) (const int&, const int&)'
 2357 |       bool __insert_left = (__x != 0 || __p == _M_end()
      |                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 2358 |        || _M_impl._M_key_compare(_S_key(__z),
      |        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 2359 |             _S_key(__p)));
      |             ~~~~~~~~~~~~~
/usr/include/c++/9.2.0/bits/stl_tree.h:1006:7: warning: 'std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::iterator std::_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::begin() noexcept [with _Key = int; _Val = std::pair<const int, int>; _KeyOfValue = std::_Select1st<std::pair<const int, int> >; _Compare = main()::myalloc; _Alloc = std::allocator<std::pair<const int, int> >]' used but never defined
 1006 |       begin() _GLIBCXX_NOEXCEPT
      |       ^~~~~

Les ranges

En C++20, une fonctionnalité très appréciée dans les autres langages fait son apparition. Il s'agit des “ranges”.

Sur le papier c'est intéressant. C'est assez proche d'un système de « flux » ou “pipelines” où nous passons des données d'une fonction à l'autre.

Seulement, la syntaxe utilise l'opérateur | qui à proprement parler, est celle d'un « ou » binaire. Il s'agit encore d'un détournement d'une syntaxe originale vers une autre utilisation ce qui peut paraître étrange au départ. Il n'est pas obligatoire d'utiliser cette syntaxe car chaque algorithme est tout simplemenent dupliqué.

En effet, une bonne partie des algorithmes de l'entête « algorithm » se retrouve aussi dans les ranges comme std::any_of et std::ranges::any_of.

Les concepts

Les concepts c'est un peu comme Half-Life 2. On en a entendu parler, on a attendu et c'est jamais sorti. En fait si, en C++20 mais d'abord annoncé pour C++11 soit neuf ans après. Cela en dit long sur la complexité de cette fonctionnalité.

À vrai dire, c'est pas un mal en soit. Il s'agit de spécifier fortement les templates et vous le savez en C++ on aime avoir le maximum d'erreur à la compilation. Cela signifie qu'il est possible de définir une fonction ou une classe template qui demande des pré requis sur le dit template.

Exemple, je veux prendre un paramètre qui soit “hashable”. C'est à dire qu'il peut-être utilisé via std::hash.

Voici la syntaxe :

template <typename T>
concept Hashable = requires(T a) {
    { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};

Question élégance on aura quand même connu mieux. Comme vous vous l'imaginez, cela implique d'avoir une palanquée de contraintes par défaut ce qui allonge une fois de plus la complexité de la bibliothèque standard.

En revanche, pour l'utilisation ça reste un avantage car le compilateur pourra enfin émettre de vrais avertissements en cas de non-respect des templates :

template <Hashable T>
void hash(const T& obj)
{
    // ici je sais que je peux utiliser std::hash avec T.
}

Un mal pour un bien, mais une complexité en plus.

Modules

Une fonctionnalité largement attendue est la présence des modules. Beaucoup pensent à tort qu'il s'agit d'un système de briques logicielles afin de faciliter l'inclusion de bibliothèques externes. Ce n'est pas le cas (et pour une fois je dirais tant mieux). Il s'agit simplement d'un remplacement des en-têtes par véritable systèmes d'export / import de code.

Ce n'est pas une mauvaise chose en soit, mais tant que les modules ne seront pas utilisés par toutes les bibliothèques externes, nous continuerons d'avoir un mélange d'en-têtes et de modules. De plus, les modules ne sont pas spécifiés vis à vis de la chaîne de compilation ce qui signifie qu'il va falloir apprendre à générer des modules selon le compilateur.

Paster, en C alors ?

Revenons à nos moutons. Je ne dénigre pas le C++ mais je ne peux nier que sa complexité devient monstrueuse et va continuer de faire fuir de nouveaux développeurs. À mon humble avis, ce langage risque de courir à sa perte si on continue de le faire évoluer de la sorte.

Alors la première version de paster était effectivement écrite en C++17. Elle utilisait Boost.Beast et mustache pour le rendu des pages. L'ensemble tient sous environ 1300 lignes de code avec client inclus.

Le code n'est pas si mal, mais Boost.Beast (comme la plupart des modules boost) est sur-conçu comme d'habitude. Il est vraiment difficile de s'en servir sans avoir un bon bagage Boost.Asio au préalable. À vrai dire même quand je développe en C++, je n'aime pas vraiment utiliser Boost, pour cette raison.

En ce moment, j'ai donc décidé de regarder ce qu'il se faisait côté web en C. Il n'y a pas grand chose et c'est plutôt normal, peu de gens apprécient développer du web en C. Mais ça existe. Pour ma part, j'aime bien le CGI car c'est neutre et simple, il n'y a juste qu'une configuration du serveur web à faire et ça tourne.

La bibliothèque que j'ai choisie est kcgi, elle est développée par un aficionado d'OpenBSD et d'UNIX et cela se ressent. La bibliothèque est minimaliste et très propre avec une excellente documentation en pages de manuel.

Résultat : en seulement deux jours j'ai terminé l'application web, le stockage en base (SQLite), le client (en shell) le tout pour environ 1200 lignes de code.

Le projet n'est pas tout à fait achevé, il reste quelques petites fonctionnalités web à rajouter (comme la recherche) et des vérifications de sureté mais globalement, ça fonctionne.

Une application kcgi, à quoi ça ressemble ?

Il suffit de lire des requêtes, kcgi fournit l'essentiel des informations HTTP comme les en-têtes, les données et un système de pages (utilisant PATH_INFO).

Exemple minimaliste :

int
main(void)
{
    struct kreq r;

    if (khttp_parse(&r, NULL, 0, NULL, 0, 0) != KCGI_OK)
        return 0;

    khttp_head(&r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
    khttp_head(&r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_TEXT_PLAIN]);
    khttp_body(&r);
    khttp_puts(&r, "Hello, world!");
    khttp_free(&r);
}

En seulement six lignes, la requête HTTP est lue et une réponse envoyée. Avec un peu de factorisation chaque page peut dont s'écrire en très peu de code.

Les templates

La plupart du temps, en développant une application web nous souhaitons écrire des morceau de pages web auxquels nous rajouterons des données au chargement. La bibliothèque kcgi fournit un système de template minimaliste.

Celui ci repose sur un système d'index, pas spécialement facile à comprendre au premier abord mais tout de même efficace.

Écrivons une page (index.html) avec deux champs que nous souhaitons remplacer :

<!DOCTYPE html>
<html>
    <body>
        <h1>@@title@@</h1>
        <p>@@text@@</p>
    </body>
</html>

Ici, nous avons deux champs title et text que nous souhaitons remplacer. Avec kcgi il faut utiliser la syntaxe @@mot@@.

Pour ce faire, nous déclarons un tableau de mots-clés autorisés et utilisons la fonction khttp_template. Celle ci appellera notre “callback” à chaque fois qu'un mot clé est trouvé.

static const char *keywords[] = {
    "title",        // @@title@@ -> index 0
    "text",         // @@text@@  -> index 1
};

static int
replace(size_t index, void *arg)
{
    struct kreq *req = arg;

    switch (index) {
    case 0:
        khttp_puts(req, "Ceci est un titre");
        break;
    case 1:
        khttp_puts(req, "Ceci est le texte");
        break;
    default:
        break;
    }
}

static void
process(struct kreq *req)
{
    const struct ktemplate kt = {
        .key    = keywords,
        .keysz  = 2,
        .cb     = replace,
        .arg    = req
    };

    khttp_template(req, &kt, "index.html");
}

Et hop, le tour est joué. Avec un peu de factorisation, le code sera à nouveau plus simple.

En réel

Bon, maintenant que je ne fais qu'en parler vous avez peut-être envie de le voir fonctionner ?

Je fais tourner une instance de test sur http://paste.markand.fr, n'hésitez pas à le tester. Pour le thème, il s'agit de siimple un petit framework CSS assez sympa.

À l'heure où j'écris cet article il n'y a pas encore de versions téléchargeable de paster, vous pouvez utiliser la version Mercurial sur le dépôt officiel.

Vous devez avoir la bibliothèque kcgi installée.

Pour cloner le dépôt :

hg clone http://hg.malikania.fr/paster

Pour installer :

make
make install

Pour installer que pasterd :

make pasterd pasterd-clean
make install-pasterd

Pour installer que le client paster (requiert uniquement curl) :

make paster
make install-paster

Et pour poster un petit fichier texte en ligne de commande :

paster foo.txt http://paste.markand.fr