Journal Ulfius: framework pour faire des API Web en C

Posté par (page perso) . Licence CC by-sa
33
1
juin
2016

Sommaire

TL;DR

Ulfius est un framework pour développer des webservices et des API REST en C facilement et rapidement.

Intro

Ca fait quelques mois que je travaille sur ce projet, et il a atteint une certaine maturité pour avoir envie d'en parler plus largement en espérant lui donner un nouvel élan.

Il y a quelques temps, je parlais ici de mon projet de serveur domotique à base d'API REST en C. Depuis, je continue à le faire évoluer tranquillement, et j'en reparlerai peut-être bientôt de ce que c'est devenu. Et bien que je sois content de libmicrohttpd pour la partie serveur web, je trouvais cette lib quand même assez lourde pour développer les API REST, j'accumulais une dette technique de plus en plus grande, et la lisibilité et la maintenabilité s'en ressentaient.

Lors du commencement de la 3e génération de mon système domotique, j'ai voulu me faciliter la tâche, et créer un framework web plus simple à utiliser, pour me concentrer sur la programmation des API en tant que telle, et ne pas dupliquer inutilement le code de préparation des paramètres en entrée et en sortie. J'ai aussi voulu intégrer de manière plus facile du JSON dans les API. Pour ca, Jansson est un très bon outil facile à utiliser. En m'inspirant vaguement de ce que j'ai vu dans d'autres frameworks web, comme NodeJS express, j'ai voulu utiliser des endpoints avec une syntaxe simple, des urls paramétrables, des paramètre faciles à lire et à envoyer, et des fonctions callbacks qui traitent la requête http. Le but étant d'avoir un service web rapide à l'exécution et avec une utilisation mémoire petite.

Grands principes

Webservice

Une application Ulfius utilisera une instance par port TCP, dans cette instance elle déclarera des endpoints, et pour chaque endpoint elle donnera un format d'URL et la fonction callback qui sera appelée lorsque l'URL spécifiée sera appelée par un client. Une instance Ulfius est non bloquante, elle tourne dans un thread séparée, et l'application peut donc vaquer à d'autres occupations pendant que le service web tourne.

Un petit exemple de webservice qui fait un Hello World:

/**
 * test.c
 * Small Hello World! example
 * to compile with gcc, run the following command
 * gcc -o test test.c -lulfius
 */
#include <ulfius.h>
#include <string.h>
#include <stdio.h>

#define PORT 8080

/**
 * Callback function for the web application on /helloworld url call
 */
int callback_hello_world (const struct _u_request * request, struct _u_response * response, void * user_data) {
  ulfius_set_string_response(response, 200, "Hello World!");
  return U_OK;
}

/**
 * main function
 */
int main(void) {
  struct _u_instance instance;

  // Initialize instance with the port number
  if (ulfius_init_instance(&instance, PORT, NULL) != U_OK) {
    fprintf(stderr, "Error ulfius_init_instance, abort\n");
    return(1);
  }

  // Endpoint list declaration
  ulfius_add_endpoint_by_val(&instance, "GET", "/helloworld", NULL, NULL, NULL, NULL, &callback_hello_world, NULL);

  // Start the framework
  if (ulfius_start_framework(&instance) == U_OK) {
    printf("Start framework on port %d\n", instance.port);

    // Wait for the user to press <enter> on the console to quit the application
    getchar();
  } else {
    fprintf(stderr, "Error starting framework\n");
  }
  printf("End framework\n");

  ulfius_stop_framework(&instance);
  ulfius_clean_instance(&instance);

  return 0;
}

Dans cet exemple, on initialise l'instance ulfius avec ulfius_init_instance, on y ajoute un endpoint qui va se déclencher lors d'un GET /helloworld et appeler la fonction callback callback_hello_world.

La fonction callback a trois paramètres en entrée: une structure contenant les paramètres de la requête, une structure initialisée pour la réponse, et un pointeur user_data facultatif qui contiendra ce que l'utilisateur voudra bien, comme la configuration de son application et les handlers de connexion de base de donnée par exemple.

Les structures struct _u_request et struct _u_response se veulent lisibles et contiennent ce dont on a besoin pour son API:

struct _u_request {
  char *               http_verb;
  char *               http_url;
  int                  check_server_certificate;
  struct sockaddr *    client_address;
  char *               auth_basic_user;
  char *               auth_basic_password;
  struct _u_map *      map_url;
  struct _u_map *      map_header;
  struct _u_map *      map_cookie;
  struct _u_map *      map_post_body;
  json_t *             json_body;
  json_error_t *       json_error;
  int                  json_has_error;
  void *               binary_body;
  size_t               binary_body_length;
};

struct _u_response {
  long               status;
  char             * protocol;
  struct _u_map    * map_header;
  unsigned int       nb_cookies;
  struct _u_cookie * map_cookie;
  char             * string_body;
  json_t           * json_body;
  void             * binary_body;
  unsigned int       binary_body_length;
  int             (* stream_callback) (void * stream_user_data, uint64_t offset, char * out_buf, size_t max);
  void            (* stream_callback_free) (void * stream_user_data);
  size_t             stream_size;
  unsigned int       stream_block_size;
  void             * stream_user_data;
};

Envoi de requête http

Si l'on veut faire des requêtes HTTP sortantes, pour simplifier la vie, il y a des fonctions qui englobent les appels libcurl nécessaires pour envoyer une requête HTTP via un struct _u_request et récupèrent la réponse dans un struct _u_response (les mêmes que pour les webservices):

int ulfius_send_http_request(const struct _u_request * request, struct _u_response * response);

Pour envoyer un mail via smtp, y'a aussi une fonction pour ca:

int ulfius_send_smtp_email(const char * host, 
                           const int    port, 
                           const int    use_tls, 
                           const int    verify_certificate, 
                           const char * user, 
                           const char * password, 
                           const char * from, 
                           const char * to, 
                           const char * cc, 
                           const char * bcc, 
                           const char * subject, 
                           const char * mail_body);

La documentation est plus complète pour ceux qui se voudraient en savoir plus sur l'API Ulfius.

Plus loin

Ce framework répond à mes besoins initiaux pour implémenter facilement des API REST et au delà, mais il est basé sur Libmicrohttpd qui a ses propres limitations, comme l'absence de websocket ou encore la gestion du protocole HTTP/2 pour ceux que j'ai pu identifier. Le framework a aussi la manie de faire des mallocs quand il en a besoin, et la gestion mémoire n'est pas optimale à cause de ca, il y a probablement beaucoup de fragmentation.

Pour ce dernier point, étant donné que je n'ai pas relevé de faiblesse lors de l'exécution, je n'ai pas ressentit le besoin, c'est pour cela que je n'ai pas cherché à gérer la mémoire de manière plus poussée, et même sur un RPi Zero, ca tourne plutôt bien et plutôt vite. Cela dit, je pense que ce n'est pas un gros travail de changer la gestion mémoire, du moment que je j'ai de bonnes bases.

C'est sous licence LGPL, et si ca peut servir à d'autres, gênez-vous pas!

Lien du projet sur Github: Ulfius

  • # Pourquoi en C ?

    Posté par (page perso) . Évalué à 9.

    Quelles sont les contraintes qui te font développer ton serveur domotique en C ?

    Je pose la question parce je fais partie d'une équipe qui développe professionnellement un serveur domotique en Perl depuis plus de 5 ans, et jamais on a atteint les limites du hardware, bien plus faible qu'un Raspberry Pi. L'appli passe son temps à attendre des évènements.
    Si c'était à refaire aujourd'hui c'est en Go ou en Rust que l'on le développerait (pas parce qu'on aime plus Perl, mais parce que le typage statique aide bien), mais surtout pas en C.

    Mainteneur de LiquidPrompt - https://github.com/nojhan/liquidprompt

    • [^] # Re: Pourquoi en C ?

      Posté par (page perso) . Évalué à 10.

      Parce que je préfère ce langage aux autres pour les applications serveur autonomes.

      Je peux gérer les ressources de manière plus fine, il y a pléthore de librairies pour faire plein d'autres choses comme accéder aux BD, communication, manipulation d'images, etc.

      J'ai rien contre les autres langages, mais je suis juste plus à l'aise en C pour un programme qui tourne en tâche de fond. C'est beaucoup une question de goût.

      • [^] # Re: Pourquoi en C ?

        Posté par (page perso) . Évalué à 2.

        C'est vrai que par choix, je choisirais plutôt de l'écrire en Go, de par sa simplicité de déploiement (compilé en statique) et de par la facilité de la cross compilation. Mais le C n'est pas si mal non plus.

    • [^] # Re: Pourquoi en C ?

      Posté par (page perso) . Évalué à 10.

      Je vais être abrupt, mais tant pis :
      - pourquoi pas en python ?
      - pourquoi pas en java ?
      - pourquoi pas en ruby ?
      - pourquoi pas en C# ?
      - pourquoi pas en Javascript ?
      - pourquoi pas en (mettre ici n'importe quel langage) ?

      Babelouest vient nous présenter son framework pour faire des API REST en C. Il est dans un contexte, il s'adapte à ce contexte (qu'il soit choisi, imposé, etc… n'a pas d'importance ici). Il a regardé l'existant, a testé libmicrohttpd et n'a pas été convaincu et a développé sa propre solution qu'il nous présente ici. Et il y en a toujours un pour dire, mais pourquoi ta pas fait ci ? Réutilisé ça ? Il a fait un choix, il nous présente ses travaux, et il faut qu'en plus il se justifie.

      Il peut y avoir pléthore de justification, découlant aussi bien de contraintes matérielles que de considérations personnelles.

      Ce qui me gène dans ce message, ce n'est pas la demande de précision sur le contexte (pourquoi en C ?), c'est de dire, illico, sans le connaitre, bah le perl aurait convenu (pourquoi pas en Perl ?). Sans doute le Rust ou le Go aussi et si je devais refaire ce que j'ai fais, c'est ce que je ferai.

      • [^] # Re: Pourquoi en C ?

        Posté par . Évalué à 4.

        Il peut y avoir pléthore de justification, découlant aussi bien de contraintes matérielles que de considérations personnelles.

        Et c'est mal de les connaître ?

        Moi ça me gêne dans ma lecture (pas pour ce journal ci) quand un projet vient se présenter sans vraiment donner ces informations là :

        • je sert à quoi ?
        • pourquoi je l'ai fait comme ça ?

        L'exemple le plus parlant c'est une nouvelle distribution. J'avais déjà fais la remarque à une dépêche en cours de rédaction. Si tu ne dis pas pourquoi tu as fait ton travail (même si cette raison, c'est que tu trouve ça rigolo), moi j'ai pas envie d'aller devoir comprendre entre les lignes.

        Après c'est général, ici ça ne m'a pas gêné.

        Ce qui me gène dans ce message, ce n'est pas la demande de précision sur le contexte (pourquoi en C ?), c'est de dire, illico, sans le connaitre, bah le perl aurait convenu (pourquoi pas en Perl ?). Sans doute le Rust ou le Go aussi et si je devais refaire ce que j'ai fais, c'est ce que je ferai.

        Tu veux dire que le gars qui connais le sujet (serveur REST pour de la domotique) et qui se pose lui-même la question de la techno (il ne choisi pas entre perl, Rust et go). Te gêne ?

        Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

        • [^] # Re: Pourquoi en C ?

          Posté par (page perso) . Évalué à 5.

          Et c'est mal de les connaître ?

          Non. D'ailleurs je le dis clairement. Je me cite : Ce qui me gène dans ce message, ce n'est pas la demande de précision sur le contexte (pourquoi en C ?)

          Moi ça me gêne dans ma lecture (pas pour ce journal ci) quand un projet vient se présenter sans vraiment donner ces informations là :
          je sert à quoi ?
          pourquoi je l'ai fait comme ça ?

          Mais il le fait, ça tombe bien ;) :
          - je sers à quoi ? A pouvoir écrire une API REST en C ;
          - pourquoi je l'ai fait comme ça ? Car j'ai un serveur domotique elle même écrite en C

          Tu veux dire que le gars qui connais le sujet (serveur REST pour de la domotique) et qui se pose lui-même la question de la techno (il ne choisi pas entre perl, Rust et go). Te gêne ?

          Non. Je me recite : c'est de dire, illico, sans le [=le contexte] connaitre , bah le perl aurait convenu

      • [^] # Re: Pourquoi en C ?

        Posté par (page perso) . Évalué à 6.

        Ce qui me gène dans ce message, ce n'est pas la demande de précision sur le contexte (pourquoi en C ?), c'est de dire, illico, sans le connaitre, bah le perl aurait convenu

        Mouai, ce n'est pas ce qui est dit, faut pas tout prendre de travers. Et d'ailleurs la réponse de Babelouest ne donne pas l'impression de l'avoir pris de la sorte.

        Il n'est pas dit "bah le perl aurait convenu" il est dit "pour notre projet on utilise perl et c'est suffisant sur du petit matos" ce qui est en lien avec un point souvent présenté sur "pourquoi C" qui est de l'utiliser pour être proche du matos et donc meilleur en terme de perf.

        Je pose la question parce je fais partie d'une équipe qui développe professionnellement un serveur domotique en Perl depuis plus de 5 ans, et jamais on a atteint les limites du hardware, bien plus faible qu'un Raspberry Pi. L'appli passe son temps à attendre des évènements.

        • [^] # Re: Pourquoi en C ?

          Posté par (page perso) . Évalué à 2.

          Il n'est pas dit "bah le perl aurait convenu" il est dit "pour notre projet on utilise perl et c'est suffisant sur du petit matos" ce qui est en lien avec un point souvent présenté sur "pourquoi C" qui est de l'utiliser pour être proche du matos et donc meilleur en terme de perf.

          C'était exactement ma pensée.

          Et d'autre par vu que le serveur domotique dont il est question semble être un projet personnel pour faire tourner dans sa propre maison, les critères de rapidité de développement (apportée par un language de plus haut niveau que C) et de maintenance sur une longue durée me paraissent plus important que la vitesse d'éxécution apportée par le C.

          J'ai bien compris que le choix du C semble surtout lié à la familiarité qu'a l'auteur avec ce language.

          Mainteneur de LiquidPrompt - https://github.com/nojhan/liquidprompt

    • [^] # Re: Pourquoi en C ?

      Posté par (page perso) . Évalué à 2. Dernière modification le 05/06/16 à 10:20.

      Si c'était à refaire aujourd'hui c'est en Go ou en Rust que l'on le développerait (pas parce qu'on aime plus Perl, mais parce que le typage statique aide bien), mais surtout pas en C.

      Je vais faire mon vieux : "je te conseille x pour y", depuis que je fais de l'info, j'ai vu :
      - la mode "PHP!"
      - la mode "Java!"
      - la mode "Python!" (et "PHP ça pue")
      - la mode "Ruby!"
      - la mode "Go!"
      - maintenant la mode "Rust!"

      Tandis que C/C++, ça a toujours été "ha ha, c'est nul comme truc" mais depuis que je fais de l'info, ça n'a certes pas été "hype" mais c'est toujours la.

      Perso je conseille des trucs "qui ne sont pas que hype et qu'on pourra retrouver dans 10 ans" comme C/C++ et PHP, c'est pas "hype", même au contraire pas mal de monde trouve ça "has been" mais c'est la pour durer comme ça a déjà traversé le temps malgré toutes les attaques "mon langage préféré à la mode il est mieux na na na". Et puis ça a un gros avantage : je connais.

      Je pose la question parce je fais partie d'une équipe qui développe professionnellement un serveur domotique en Perl depuis plus de 5 ans, et jamais on a atteint les limites du hardware, bien plus faible qu'un Raspberry Pi

      Quelles sont les contraintes qui te font développer ton serveur domotique en Perl ?

      • [^] # Re: Pourquoi en C ?

        Posté par . Évalué à 0.

        Dans ta liste, Python, Ruby et Java ont plus de 10 ans. Et je ne vois pas Python ou Java disparaître de sitôt. Comme quoi peut-être que tout ça ce n’est pas seulement des « effets de mode » mais bel et bien des évolutions qui répondent à des vrais besoins ?

        Et puis ça a un gros avantage : je connais.

        Ce n’est que de la résistance au changement de la part de fainéants qui ont la flemme d’apprendre… :)

      • [^] # Re: Pourquoi en C ?

        Posté par (page perso) . Évalué à 1.

        PHP ça date de 1995, et Java de 1996 ; il est clair que Java est un truc de jeunes qui doit encore faire ses preuves ; vu le peu d'entreprises qui l'utilise, dans 10 ans ça aura sûrement disparu (après tout ça ne fait que quelques décennies que Cobol devrait avoir disparu).

        Y en a marre de tous ces langages de hipster, alors qu'au final ça fait tout pareil que le basic de mon amstrad. Parce qu'au bout de 20 ans, il faut en apprendre un nouveau et ça c'est intolérable. En gros pour avoir ses 40 annuités il faut changer une fois (voire 2 !) de langage de prédilection. Ça c'est encore un coup de notre gouvernement et sa flexibilité du travail !

  • # libuv : Cross-platform asynchronous I/O

    Posté par (page perso) . Évalué à 6.

    Merci pour ce journal, c'est toujours intéressant d'avoir des nouvelles approches sur un ancien langage.

    Est-ce que tu utilises une librairie externe pour la gestion de ta boucle d'événements ? il me semble que la plus connue est
    libuv (utilisé par NodeJS notamment) mais il y en a d'autres. J'imagine que libcurl fournit aussi directement ce genre de support, non ?

    Concernant l'utilisation du C, je trouve que ton argument est bon : c'est un langage simple et (presque) sans librairie standard incluse. Le programmeur déclare explicitement les dépendances et bénéficie d'un grand nombre de librairies de base existantes (par exemple CCAN). La seule variation par rapport au langage "moderne" est que ces librairies ne sont pas disponibles dans une archive centralisé unique mais plutôt fournies par les gestionnaires de paquets.

    • [^] # Re: libuv : Cross-platform asynchronous I/O

      Posté par . Évalué à 5.

      Le C, simple?!?

      ok, la syntaxe est facile a comprendre, mais vu tous les pièges qui trainent de partout dans le language, l'aridité sans nom des types de bases (chaines de characteres et collections en tete), les pièges de bas niveau (taille de pointeurs, downcast subtils etc), les heisenbugs qui se glissent subrepticement et autres undefined behavior, c'est pas le premier langage qui me vient a l'esprit quand je pense simplicité.
      Et j'aborde pas la gestion de la memoire, qui nous apporté tant de failles critique de sécurité ces 2-3 dernières années.

      Linuxfr, le portail francais du logiciel libre et du neo nazisme.

      • [^] # Re: libuv : Cross-platform asynchronous I/O

        Posté par . Évalué à 2.

        l'aridité sans nom des types de bases (chaines de characteres et collections en tete),

        En même temps, y'a pas de collections en C. Et les chaînes de caractères en C, elles sont … ouai, bon, disons qu'elles sont légères. Lentes sur certaines opérations (strlen) mais légères en mémoire (1 octet gâché au lieu de sizeof (char*)).

        c'est pas le premier langage qui me vient a l'esprit quand je pense simplicité.

        C'est lequel?

        Et j'aborde pas la gestion de la memoire, qui nous apporté tant de failles critique de sécurité ces 2-3 dernières années.

        C'est vrai. Quand on touche la mémoire, on peut avoir des failles de sécurité.
        Mais ce n'est pas une fatalité, particulièrement quand l'application est simple et le codeur propre.
        Pour les failles de sécurité de ces dernières années, plusieurs auraient été détectées si:

        1. le code n'avait pas été une usine à gaz (openssl)
        2. la compilation avait été faite avec tous les warnings (code unreachable pour l'histoire du goto d'apple)

        Par contre, pour les rares failles que je connais, c'est plus dur d'exploiter des failles en natif qu'en interprété.

    • [^] # Re: libuv : Cross-platform asynchronous I/O

      Posté par (page perso) . Évalué à 2.

      Comme mon projet est surtout une encapsulation de MHD pour la partie webserver, je laisse le soin à MHD de gérer la boucle d'événements et la partie réseau bas-niveau.

  • # Nommage des structures

    Posté par (page perso) . Évalué à 2.

    Je ne sais pas pourquoi tu préfixes toutes tes structures par un _. En C++ c'est même réservé par la norme, je ne sais pas ce qu'il en est C.

    Mais aussi, je trouve ça moche.

    Sinon dans l'ensemble je trouve que le code est propre et bien documenté.

    l'azerty est ce que subversion est aux SCMs

    • [^] # Re: Nommage des structures

      Posté par . Évalué à 2.

      j'utilise cette syntaxe pour déclarer des structures chainées, mais tout mes structure sont des types définit par typedef

      //element d'une liste chainée
      typedef struct _item{
       void *data;
       int sizeData;
       struct _item voisin;
      } item;
      
      
      item header
    • [^] # Re: Nommage des structures

      Posté par (page perso) . Évalué à 5. Dernière modification le 02/06/16 à 15:59.

      Question de goût et d'habitude là aussi.

      Le C a des conventions différentes du C++, et j'ai souvent vu des syntaxes comme celles que j'utilise, je les ai donc écrites comme ca.

      On le voit ailleurs dans d'autres libs, certains types sont en fait des structures avec un typedef. Ca permet de garantir le même comportement d'une architecture ou une implémentation à l'autre, sans que le programme client ait à se poser la question de comment est implémenté telle ou telle fonction.

      Le choix de préfixer avec des blah (_) et de ne pas utiliser de typedef n'a pas de raison précise, mais c'est comme ca que je l'ai fait. Dans mon raisonnement, je me dis que préfixer un nom de structure par blah indique que c'est une structure interne à la lib, et spécifier explicitement le mot-clé struct lors de sa déclaration c'est pour dire que la structure est une structure dont tous les membres sont utilisables en tant que tels, et que je n'abstrait pas une partie du traitement à l'utilisateur. Et si l'utilisateur veut utiliser ma lib dans son programme, il sait que s'il choisit des déclarations de types qui commencent par _u_, ou des déclarations de fonctions qui commencent par ulfius_, il risque d'écraser une des miennes, c'est pour ca que j'ai utilisé un préfixe qui est le même partout et ne laisse pas place à l’ambiguïté.

      Mais je suis pas fermé aux changements d'habitudes si ca se justifie, on n'est pas des bêtes ;)

      Merci pour le compliment sur le code, là aussi je me dis que si je veux que ca soit utilisé, autant que ce soit accessible.

  • # ESP8266

    Posté par . Évalué à 5.

    Est-ce que tu sais si ça peut fonctionner sur l'ESP8266 ?

    Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

    • [^] # Re: ESP8266

      Posté par (page perso) . Évalué à 4.

      J'offre une board de dev à celui qui veut porter le projet dessus.

      • [^] # Re: ESP8266

        Posté par . Évalué à 4.

        Ben en fait, je pense avoir prochainement (d'ici quelques semaines/mois) d'un serveur web sur un ESP8266. J'essaierais de l'utiliser et de te faire un retour (ou de te demander de l'aide) :)

        Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

    • [^] # Re: ESP8266

      Posté par (page perso) . Évalué à 3.

      J'ai déjà bricolé sur un ESP8266 mais avec l'IDE de l'Arduino, je saurais pas te dire si Ulfius et ses dépendances pourraient tenir sur un chip comme ca.

      Libmicrohttpd dit qu'il peut tenir sur un binaire de 32Ko en enlevant ssl et quelques options, libcurl peut être minimisé aussi, voire enlevé si on n'utilise pas les requêtes sortantes, et jansson est assez petit également (~50Ko sur ma debian).

      Par contre pour l'ESP8266, j'utilisais justement une lib dont la syntaxe rappelle aussi ce genre de framework: https://github.com/babelouest/taulas/blob/master/taulas_esp8266_webserver/taulas_esp8266_webserver.ino

      • [^] # Re: ESP8266

        Posté par . Évalué à 1.

        Je ne suis pas sûr que retirer le tls soit une bonne idée niveau sécurité.

        • [^] # Re: ESP8266

          Posté par (page perso) . Évalué à 3.

          Ca dépend du contexte, mais si tu as confiance en ton réseau et qu'il est inaccessible aux non connectés, ou si tu te fout qu'on y écoute ce qui y passe, genre le capteur de la température extérieure, c'est pas très grave…

          Maintenant je suis d'accord que les cas où tu désactives le tls doivent être des exceptions mûrement réfléchies et non pas:

          "je désactive tls parce que j'ai la flemme"

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.