Thomas Touhey
:
fopencookie - faites vos propres flux !
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 :
1
2
3
4
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 :
1
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 :
1
2
3
4
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
72
#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 !