Si vous ne le saviez pas déjà, ce que nous appelons communément libc est un ensemble de fonctions basiques qui sont plutôt utiles en C, pour interagir de façon très basique avec le système et la mémoire sans en connaître les rouages. Toute plateforme pour laquelle on peut compiler du C a une libc, même incomplète, donc connaître les fonctions que vous pouvez utiliser dans la libc sont plutôt importantes si vous souhaitez faire quelque chose de compatible et ré-utilisable pour porter votre projet sur d'autres plateformes.
Plusieurs normes définissent et étendent la libc. La définition la plus basique que vous pourrez en trouver se trouve dans les standards C de l'ISO directement (C89, C99, C11), il s'agit de fonctions très basiques sur la mémoire, et STREAMS (que certains n'implémentent pas pour leur plateforme, donc vous ne trouverez pas de printf() pour celles-ci). Au lieu de créer une autre bibliothèque spécifique à votre système (liblinux ou autre nom tout aussi sexy), on préfère étendre les standards derrière la libc pour des interactions plus poussées avec le système; c'est pourquoi nous avons unistd.h sur les sytèmes UNIX-like (défini dans POSIX/SUS), et windows.h pour Microsoft Windows (j'ignore si un standard définit ce header).
Puis GNU a débarqué, et après avoir implémenté ce qu'il fallait pour UNIX, y compris le compilateur C et la libc qui va bien, ils ont fini par ajouter leurs propres fonctionnalités au-delà de celles requises pour la conformité POSIX/SUS. Certaines peuvent faire sourire (strfry(), memfrob()), mais la plupart sont franchement utiles. Un exemple simple ? Vous ne devriez pas pouvoir faire de l'arithmétique de pointeurs sur void*, donc ceci ne devrait pas marcher :
char s[6];
void *p = s;
for (int i = 0; i < 6; i++)
*p++; /* oops! */
Ceux qui parmi vous peuvent utiliser GCC peuvent tester ceci en compilant ou non avec l'option -pedantic en ligne de commande, qui désactive les extensions GNU (vous devriez vraiment lire la page de manuel associée au compilateur).
Ceci n'est qu'un exemple parmi une quantité d'autres qu'on pourrait produire concernant les extensions GNU qui ont été ajoutées au compilateur C, mais ne parlions-nous pas de libc ? C'est exact, concentrons-nous sur l'implémentation de STREAMS. Vous conaissez sans doute fopen(), fclose(), fread() et fwrite(), qui créent ou utilisent un handle de type FILE. Il s'agit d'une abstraction au-dessus d'un objet de type STREAMS (généralement représenté par les file descriptors), avec des choses utiles construites au-dessus, tels que le stream buffering, ou la sauvegarde de certains éléments (le flux est-il lisible ? inscriptible ?). Cette abstraction est suffisamment avancée pour qu'on puisse utiliser autre chose qu'un objet de type STREAMS derrière ; le terme officiel pour ceci est "custom stream".
fopen() n'est qu'une méthode pour ouvrir un flux, qui exploite un objet de type STREAMS. Une autre méthode est d'utiliser fmemopen() / open_memstream() (défini dans POSIX depuis 2008), pour construire un flux au-dessus d'une zone de mémoire - pratique quand vous ne voulez pas gérer les limites et autres vous-mêmes, et plutôt pratiques pour tout ce qui est sécurité. La dernière (celle dont cet article est supposé être centré autour) est fopencookie(), qui est une extension GNU. Cette fonction vous permet d'utiliser vos propres callbacks pour lire, écrire, vous déplacer et fermer le flux, avec un cookie (les données internes du flux). Fait qui peut faire sourire, fmemopen() utilise fopencookie() dans l'implémentation de GNU.
À partir d'ici, je vous conseille vraiment d'ouvrir la page de manuel (qui, au passage, est bien faite) concernant fopencookie(), je parlerai simplement des subtilités techniques avec un exemple simple : nous parlerons d'un flux qui fait N bytes et ne retourne que des bytes qui valent 24. Nous ferons donc une fonction nommée create_tf_stream() qui aura ce prototype :
FILE *create_tf_stream(size_t size);
Tout d'abord, le flux et la création de cookie. Comme vous pouvez vous en douter en voyant le prototype de fopencookie(), il y a trois paramètres : le cookie, le mode et les callbacks. Le cookie est transféré via un paramètre en void* : la fonction prendra le pointeur et le transmettra directement au callbacks, sans lecture ni retouche. Pour notre exemple, si notre stream n'avait pas de taille, nous n'aurions pas besoin de cookie (données internes), donc nous aurions juste pu passer NULL puis ne pas utiliser le cookie dans les callbacks ; mais puisque nous avons une taille, nous avons également besoin d'un curseur. Voici la structure que nous utiliserons :
struct cookie {
size_t size;
size_t cursor;
};
Une fois le type du cookie défini, la première question évidente que vous devez vous poser est la suivante : comment créons-nous le cookie ? Eh bien, la façon évidente d'y répondre est d'utiliser le tas ("heap" en anglais), via malloc() - nous devrons alors free() le cookie dans le callback de fermeture du flux. Si vous faites partie des gens qui n'aiment pas allouer des petits bouts de mémoire juste pour ça, vous pouvez aussi utiliser une instance statique de la structure, mais vous ne pourrez utiliser qu'un seul flux de ce type à la fois (et par utilisation, je désigne le cycle complet : création, utilisation, destruction) ; mais vous devriez vraiment utiliser le tas ici.
La prochaine question que vous devez vous poser au sujet de la création de votre flux est : pourquoi fopencookie() nécessite-t-il un paramètre mode ? Pourquoi le flux ne pourrait-il pas simplement interdire la lecture lorsque le callback de lecture est NULL, ou interdire l'écriture lorsque le callback d'écriture est NULL ? En réalité, lorsqu'il n'y a pas de callback, le callback par défaut est utilisé, qui renvoie par exemple EOF (0) à chaque appel pour la lecture. Le mode est là pour définir les permissions du flux.
Maintenant que nous avons toutes les clés pour la création de notre flux, nous pouvons à présent nous intéresser aux callbacks. Les prototypes ressemblent fortement à ceux de fread(), fwrite(), fseek() et fclose(), seulement, un paramètre cookie est apparu (vous devrez le caster dans votre fonction), puis retourner ssize_t au lieu du size_t retourné par les fonctions "normales" ; ceci est dû aux cas d'erreurs, puisque vos callbacks doivent retourner directement l'erreur, en négatif. Les fonctions "normales" rempliront errno lorsque la valeur retournée par votre callback est négative.
À partir d'ici, l'écriture de notre flux personnalisé est aisé :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | #define _GNU_SOURCE /* otherwise fopencookie is not declared */ #include <stdio.h> #include <stdlib.h> #include <string.h> #define min(A, B) ((A) < (B) ? (A) : (B)) struct cookie { size_t size; size_t cursor; }; static ssize_t tf_read(void *vcookie, char *buf, size_t size) { struct cookie *cookie = vcookie; size_t toread = min(cookie->size - cookie->cursor, size); memset(buf, 24, toread); cookie->cursor += toread; return (toread); } static int tf_seek(void *vcookie, off64_t *off, int whence) { struct cookie *cookie = vcookie; size_t pos = 0; /* get pos according to whence */ switch (whence) { case SEEK_SET: case SEEK_END: if (*off < 0 || *off > cookie->size) return (-1); if (whence == SEEK_SET) pos = *off; else pos = cookie->size - *off; break; case SEEK_CUR: if (*off + cookie->cursor < 0) return (-1); pos = *off; if (pos > cookie->size) return (-1); break; } /* set the position and return */ cookie->cursor = pos; return (0); } static int tf_close(void *cookie) { free(cookie); /* take it now! */ } FILE *tf_open(size_t size) { /* create the cookie */ struct cookie *cookie = malloc(sizeof(struct cookie)); if (!cookie) return (NULL); *cookie = (struct cookie){ .cursor = 0, .size = size }; /* create the stream */ FILE *file = fopencookie(cookie, "r", (cookie_io_functions_t){tf_read, NULL, tf_seek, tf_close}); if (!file) tf_close(cookie); return (file); } |
And you can simply test by adding to the end of the file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #include <unistd.h> int main(void) { FILE *f = tf_open(5); if (!f) return (1); char buf[2]; size_t read; while ((read = fread(buf, 2, 1, f))) { printf("%zu\n", read); write(1, "/", 1); write(1, buf, read); write(1, "/\n", 2); } printf("%d\n", fseek(f, 120, SEEK_END)); printf("%zu\n", fread(buf, 1, 50, f)); } |
Mais attention, plot twist, vous vous souvenez quand je vous parlais de buffering ? Eh bien, dans mon exemple, essayez d'afficher la taille demandée pour chaque lecture dans votre callback de lecture, elle sera affichée deux fois : la première fois, 8096 est affiché, et la seconde fois (le buffer interne du FILE est vide), 0 (EOF) est affiché. Vous pouvez modifier cette valeur en utilisant setvbuf() (désactiver le buffering écrira toujours lorsque vous appelez fwrite(), au lieu d'attendre que vous remplissez le buffer interne d'écriture), mais le comportement restera le même.
Aussi, si le buffer passé à fread() n'est pas rempli par votre callback, votre callback sera continuellement appelé jusqu'à ce que le buffer soit rempli... ou jusqu'à ce que votre callback retourne EOF. Ceci m'a causé des soucis il y a un moment, lorsque je faisais une bibliothèque de communication et que j'utilisais fread(BUFFER_MAX). Pour cet usage, préférez lire graduellement (lisez le header et la taille du contenu, puis lisez le contenu, en faisant deux appels à fread()), et ne vous souciez pas de la vitesse - ces fonctions sont optimisées.
Comme conclusion, j'ajouterais que ce que j'aime au sujet de fopencookie(), c'est qu'il permet d'écrire un driver permettant de gérer une liaison de type flux de caractères, mais seulement pour le programme et sans la complexité de l'interaction des espaces kernel/utilisateur. L'abstraction des FILE est plutôt puissante et sympathique à utiliser, il est juste dommage que fopencookie() soit spécifique à la libc de GNU.
Vous voulez en savoir plus sur l'implémentation des streams de la glibc ? Rendez-vous sur la page dédiée dans leur doc !