Lire et écrire dans un fichier binaire

Besoin de conseils

a marqué ce sujet comme résolu.

Bonjour à tous,

Il m'arrive régulièrement d'avoir besoin de lire et/ou écrire dans un fichier binaire en C++. Malheureusement, les seules fonctions disponibles dans la bibliothèque standard sont get/put et read/write, et elles ne prennent en paramètre que des valeurs de type char ou des tableaux de char.

En général, je code donc mes propres fonctions qui me permettent de lire/écrire une valeur de n'importe quel type arithmétique dans un flux (en binaire donc). Elles ressemblent généralement à ça :

 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
template<typename T>
T readValue(std::istream &input, bool bigEndian = false) noexcept
{
    assert(std::is_arithmetic<T>::value);

    std::array<char, sizeof(T)> buffer;
    input.read(buffer.data(), buffer.size());

    if(bigEndian)
        std::reverse(buffer.begin(), buffer.end());

    return *reinterpret_cast<T*>(buffer.data());
}

template<typename T>
void writeValue(std::ostream &output, T value, bool bigEndian = false) noexcept
{
    assert(std::is_arithmetic<T>::value);

    std::array<char, sizeof(T)> buffer;
    std::copy_n(reinterpret_cast<char*>(&value), buffer.size(), buffer.data());

    if(bigEndian)
        std::reverse(buffer.begin(), buffer.end());

    output.write(buffer.data(), buffer.size());
}

Et un exemple d'utilisation :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
std::fstream file("test.bin", std::ios::in | std::ios::out | std::ios::binary);

writeValue<char>(file, 'a');
writeValue<int>(file, 123456);
writeValue<float>(file, 123.456, true);

file.seekg(std::ios::beg);

std::cout << readValue<char>(file) << std::endl;
std::cout << readValue<int>(file) << std::endl;
std::cout << readValue<float>(file, true) << std::endl;

Ce code fonctionne parfaitement.

J'ai toutefois besoin de conseils. Que pensez-vous de ces fonctions ? Sont-elles efficaces ? Performantes ? Sécurisées ? Auriez-vous aussi fait ainsi ? Y a-t-il de meilleures alternatives ?

Salut,

Je ne me pronconcerai pas sur le code ne connaissant pas très bien le C++. Je note juste que ces fonctions posent problème du point de vue de la portabilité puisque la taille des types varie d'une machine à l'autre.

Sinon, en règle général (mais là je parle pour le C) le contenu des fichiers binaires (qui consiste le plus souvent en des nombres entiers) est représenté sous forme de structures et est récupéré et écrit sous forme de tableau de unsigned char, comme ci-dessous, pour éviter tout problèmes de portabilité.

 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
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct example {
        int un; /* Deux octets */
        int deux; /* Deux octets */
        long trois; /* Quatre octets */
};


void
example_write(struct example *self, FILE *dest)
{
        static unsigned char buf[8];

        buf[0] = self->un & 0xFF;
        buf[1] = (self->un >> 8) & 0xFF;
        buf[2] = self->deux & 0xFF;
        buf[3] = (self->deux >> 8) & 0xFF;
        buf[4] = self->trois & 0xFF;
        buf[5] = (self->trois >> 8) & 0xFF;
        buf[6] = (self->trois >> 16) & 0xFF;
        buf[7] = (self->trois >> 24) & 0xFF;

        if (fwrite(buf, 1, sizeof buf, dest) != sizeof buf) {
                fprintf(stderr, "fwrite: %s\n", strerror(errno));
                exit(EXIT_FAILURE);
        }
}


int
main(void)
{
        struct example example = { 1, 2, 3 };
        FILE *dest;

        dest = fopen("example.bin", "wb");

        if (dest == NULL) {
                fprintf(stderr, "fopen: %s\n", strerror(errno));
                return EXIT_FAILURE;
        }

        example_write(&example, dest);

        if (fclose(dest) == EOF) {
                fprintf(stderr, "fclose: %s\n", strerror(errno));
                return EXIT_FAILURE;
        }

        return 0;
}

Édit : corrections mineures.

+0 -0

Je note juste que ces fonctions posent problème du point de vue de la portabilité puisque la taille des types varie d'une machine à l'autre.

Taurre

Oui, évidemment. Mais pour autant que je sache, seul le type int varie d'une plateforme à l'autre. Pour cette raison, je prends généralement la peine d'être plus précis quand j'utilise des entiers : long int ou long long int. (Certes, je n'ai pas pris cette peine dans mon exemple.)

Sinon, en règle général (mais là je parle pour le C) le contenu des fichiers binaires (qui consiste le plus souvent en des nombres entiers) est représenté sous forme de structures et est récupéré et écrit sous forme de tableau de char, comme ci-dessous, pour éviter tout problèmes de portabilité.

Taurre

Oui, je connaissais cette méthode. L'avantage, c'est qu'elle permet d'avoir le contrôle totale sur l'encodage et la taille de chaque variable à lire/écrire. En revanche, cela implique d'avoir une structure de données bien précises. De plus, le décalage de bit ne fonctionne pas (à ma connaissance) sur les nombres à virgules flottantes.

J'ai codé mes fonctions pour une utilisation plus générale (elles ont la simple responsabilité de lire/écrire une valeur en binaire dans un flux, rien de plus). De cette manière, je profite aussi de la conversion avec reinterpret_cast qui "fait le boulot" à ma place.

Maintenant, la où je vois un potentiel problème, c'est par exemple si ce reinterpret_cast n'encode pas tous les types de la manière. Visiblement, c'est en little Endian par défaut, mais est-ce le cas tout le temps ? Ou cela peut-il varier d'un compilateur à l'autre ? D'une plateforme à l'autre ?

PS: Je veux pas dire d'absurdité, mais l'opération n >> 1 ne décale-t-elle pas la valeur d'un seul bit ?
Ne faudrait-il pas plutôt écrire : buf[1] = (self->un >> 8) & 0xFF; ?

EDIT: Je vois que c'est déjà corrigé. ;)

+0 -0

Oui, évidemment. Mais pour autant que je sache, seul le type int varie d'une plateforme à l'autre. Pour cette raison, je prends généralement la peine d'être plus précis quand j'utilise des entiers : long int ou long long int. (Certes, je n'ai pas pris cette peine dans mon exemple.)

Olybri

À vrai dire, tous les types varient suivant l'architecture cible sauf le type char qui a toujours une taille de un byte (la taille de celui-ci étant en revanche variable, mais le plus souvent égale à un octet).

Oui, je connaissais cette méthode. L'avantage, c'est qu'elle permet d'avoir le contrôle totale sur l'encodage et la taille de chaque variable à lire/écrire. En revanche, cela implique d'avoir une structure de données bien précises. De plus, le décalage de bit ne fonctionne pas (à ma connaissance) sur les nombres à virgules flottantes.

Olybri

Effectivement, les opérateurs de décalage ne fonctionnent qu'avec des opérandes entiers. C'est d'ailleurs pour cela qu'on évite de les utiliser dans les formats de fichiers puisqu'il pose des soucis supplémentaires.

Visiblement, c'est en little Endian par défaut, mais est-ce le cas tout le temps ? Ou cela peut-il varier d'un compilateur à l'autre ? D'une plateforme à l'autre ?

Olybri

Je ne sais pas ce que réalise l'opérateur reinterpret_cast, mais je peux te dire que le boutisme varie d'une architecture à l'autre

PS: Je veux pas dire d'absurdité, mais l'opération n >> 1 ne décale-t-elle pas la valeur d'un seul bit ?
Ne faudrait-il pas plutôt écrire : buf[1] = (self->un >> 8) & 0xFF; ?

EDIT: Je vois que c'est déjà corrigé. ;)

Olybri

Oui, j'avais visiblement les yeux dans le derrière quand j'ai écris ce code. :-°

+2 -0

Utiliser les types a taille fixe int8_t, int16_t, etc (http://en.cppreference.com/w/cpp/types/integer)

Et pour se simplifier la vie, utiliser une lib de serialisation (boost.serialise par exemple)

HS : et attention au padding quand on utilise un struct/class

+3 -0

HS : et attention au padding quand on utilise un struct/class

gbdivers

Dans le cas de mon exemple, la structure ne sert qu'à faciliter la manipulation des données par le programme. Les lectures et écritures ont toujours lieu sous forme de tableau d’unsigned char. Pour la lecture, cela donne ceci. Dans le cas contraire, la structure pose effectivement des problèmes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void
example_read(struct example *self, FILE *src)
{
        unsigned char buf[8];

        if (fread(buf, 1, sizeof buf, src) != sizeof buf) {
                fprintf(stderr, "fread: %s\n", strerror(errno));
                exit(EXIT_FAILURE);
        }

        self->un = buf[0] | buf[1] << 8;
        self->deux = buf[2] | buf[3] << 8;
        self->trois = buf[4] | buf[5] << 8 | buf[6] << 16 | buf[7] << 24;
}
+0 -0

Et pour se simplifier la vie, utiliser une lib de serialisation (boost.serialise par exemple)

gbdivers

Mon but n'est pas simplement de sérialiser des données pour les sauvegarder.

En fait, je code un outil qui apporte des modifications sur des fichiers binaires provenant d'un gros jeu vidéo (donc je n'ai aucun contrôle sur l'encodage des données), afin de modifier certains comportements de ce dernier (un patch, en quelque sorte). Par exemple, il faut que je modifie un nombre entier (un long int) à l'adresse 1000 (donc les octets 1000, 1001, 1002 et 1003), puis un float à l'adresse 2300, etc…

Jusqu'à maintenant, j'utilisais des fonctions semblables à celles de mon premier post, mais je n'ai jamais été très sûr leur efficacité. Et je connais trop peu boost pour savoir ci cette bibliothèque offre un tel service.

Ok, donc c'est du code spécifique, pour travailler sur un format de fichier spécifique dont tu n'as pas le contrôle. C'est effectivement un cas particulier, qui peut justifier de faire un code qui ne respecte pas "les règles de l'art".

Du coup, la portabilité, osef. Ajoutes au pire des assert pour vérifier que tes contraintes sont respectées (taille de type, endian, etc) et encapsule au mieux, mais ne t'embêtes pas trop. Tant que ça marche…

@Taurre: je me doute que tu connais le problème. Je faisais juste un rappel, au cas où un débutant s'amuserait à créer des structures, faire un tableau, et lire l'ensemble d'un bloc.

+2 -0

Si les données sont dans des structures POD, il n'y a même pas besoin de passer par des tableaux intermédiaires. Ca peut avoir son importance quand on manipule des gros fichiers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct foo {
uint16_t a;
uint16_t b;
int32_t c;
double d;
};

foo bar = { 1, 2, -999, 3.14 };

FILE* fp = fopen("fichier.ext", "wb");
fwrite(&bar, 1, sizeof(bar), fp);
fclose(fp);

fp = fopen("fichier.ext", "rb");
fread(&bar, 1, sizeof(bar), fp);
fclose(fp);

Bien sûr j'ai volontairement éludé la gestion des erreurs.

Tant qu'on est pas dans un cas non conventionnel et si on n'est pas un maniaque de la portabilité absolue à 100%, ça marche très bien dans 99% des cas. OK il y a l'alignement, l'endianess et tout ça mais bon, c'est vraiment que pour des cas pathologiques.

Petit bonus en utilisant la bonne vielle libc, en jouant avec l'ordre des paramètres (pour le coup je ne sais jamais dans quel ordre, j'ai peut-être inversé dans mon exemple), on peut se parer facilement des structures à moitié lues/écrites parce que le fichier est corrompu ou autre problème hardware; ceci parce que le fichier est toujours lu par blocs atomiques de la taille indiquée.

Avec les flux C++ je ne crois pas qu'on peut faire ça si simplement. Qu'on me corrige si je me trompe.

+0 -0

Rajoute man ntoh

Donc effectivement, ce n'est pas si plug and play. Mais en rien différent du C.

Et pour les ntoh & cie, je les surcharge régulièrement sous un seul nom histoire de pouvoir avoir des fonctions génériques de sérialisation binaire.

Après, je me rajoute aussi régulièrement des couches supplémentaires où je définis des listes de types (où un type (qui est une structure vide) correspond à un champ), ce qui me permet de piocher le bon champ facilement et de le convertir à la volée (et donc uniquement si j'en ai besoin) vers le bon type utilisateur.

Avec les flux C++ je ne crois pas qu'on peut faire ça si simplement. Qu'on me corrige si je me trompe.

QuentinC

write / read + reinterpret_cast + sizeof.

Ksass`Peuk

Et c'est plus ou moins ce je fais. Je passe juste par un tableau intermédiaire pour pouvoir éventuellement inverser l'ordre des bytes si je dois lire/écrire en Big endian.

@lmghs : Merci pour la réponse, je vais voir ce que je peux faire avec ça.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte