Flux, sérialisation, fichiers binaires...

Gros soucis de compréhension et de choix de conception, d'outils, etc.

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Pour expliquer succinctement mon problème, j'aimerais parser un fichier binaire dont je connais le format à l'aide de C++.

J'ai cherché maintes solutions / bibliothèques pour réussir à parvenir à mon objectif.

Le framework Boost semble proposer moult solutions comme :

  • Les archives : mais cela me semble ne pas être adapté à mon besoin (1) et le framework semble utiliser un format propre à lui-même en ajoutant ses propres en-têtes (2) ;
  • Le module Spirit de Boost : intéressant, mais je ne vois pas comment l'utiliser, d'autant plus qu'il n'y a pas de désérialisation, je crois ;
  • La lib cereal, que j'ai testée, ne semble pas serializer correctement les données quand je teste leur code d'exemple ( http://uscilab.github.io/cereal/ ). J'obtiens un fichier binaire de quatre octets alors que le code serialize trois entiers, ce qui devrait faire au moins 12 octets (32 bits fois trois)…

Une autre question que je me pose : quand je "déserialise" un fichier binaire, dois-je travailler avec le concept d'archives ? De flux ? De "devices" ?

Exemple concret : si je veux "parser" un fichier image au format PNG, sachant que celui-ci contient un en-tête de huit octets (ou sept, je ne sais plus), puis des entiers représentant sa taille… Comment m'y prendre correctement ? Existe-t-il une solution déjà pensée et conçue pour ça ?

Je suis perdu avec toutes ces notions. Si quelqu'un pouvait m'éclairer sur la bonne manière de faire pour parser un fichier binaire et stocker des informations désérialisée dans des variables / attributs de classe, je suis preneur. :)

D'avance je vous présente mes excuses si mon message paraît maladroit / si c'est le cafouillis… C'est juste que mes connaissances en C++ sont très limitées et que j'aimerais les parfaire, tout naturellement. ^^

Merci d'avance pour les âmes salvatrices qui daigneraient se pencher sur mon problème voire dissiper mes doutes et estomper ma grande détresse. :ange:

+0 -0

J'ai déjà fait de la sérialisation en Java (je pense que c'est exactement le même principe en C#), puis PHP. Je ne connais pas la sérialisation des libs en C++ mais je suppose qu'il y a des notions similaires grâce aux libs d'introspection Boost.

En Java (ou C#) la sérialisation permet de générer un clone de l'objet sous forme de flux binaire (qu'on peut échanger via le réseau ou stocker dans un fichier) et interopérable, le seul problème qu'il pourrait y avoir à ce niveau c'est une différence de version trop importante entre une JVM (ou CLR) qui sérialise et une autre qui désérialise le flux, pas l'OS. Là où je veux en venir est qu'il ne faut pas forcément se fier au flux (comme le nombre d'octet en sortie) à moins de connaitre sur le bout des doigts le protocole car ce flux possède forcément son propre standard géré par la librairie. Autrement l'avantage avec ces 2 langages est que le module de sérialisation est inclue dans la librairie standard, on n'a pas l'embarra du choix mais c'est plus simple ainsi, de plus c'est très fiable (je n'ai pas rencontré de bug jusqu'à présent).

En PHP la sérialisation se réalise via les fonctions serialize() et unserialize(), le principe est bien moins complexe car ce n'est pas un flux mais un message qui est manipulé (avec un peu de jugeote tu peux toi-même recréer ces fonctions via l'introspection) :

1
2
3
4
5
6
7
<?php

class PHP {
    private $a = 10, $b = 9;
}

echo serialize(new PHP()); // O:3:"PHP":2:{s:6:"PHPa";i:10;s:6:"PHPb";i:9;}

PS1: Je ne connais pas le module Spirit de Boost, mais généralement si tu as de la sérialisation tu as aussi de la désérialisation. Sinon quel intérêt de sérialiser si le format n'est pas réversible ?

PS2: Pour la lib cereal, tes attributs étaient static ou pas ? Parce qu'un attribut static n'est pas sérialisé.

+0 -1

Une question : pourquoi "-1" ?

L'auteur (qui pratique principalement de l'ASM/C/C++) dit être perdu dans toutes ces notions nouvelles et je ne fait qu'apporter ma contribution.

Gugelhupf

Tu as répondu à côté de la plaque, en effet…

Même si ta réponse avait du sens dans l'absolu, ce n'est pas du tout ce que je cherche à savoir. Désolé.

@Stranger : le reinterpret_cast me semble idéal en effet. J'ai réussi à faire ça :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <cstdint>

int main(int argc, char** argv) {
  if(argc < 2) {
    std::cout << "Usage: " << argv[0] << " <file>" << std::endl;
    ::exit(EXIT_FAILURE);
  }

  std::ifstream mystream(argv[1], std::ios::in | std::ios::binary);
  uint32_t number;
  mystream.read(reinterpret_cast<char*>(&number), sizeof(uint32_t));
  std::cout << "Just read " << number << std::endl;
  return EXIT_SUCCESS;
}

Voici ce que j'obtiens avec le fichier suivant :

1
2
3
4
5
6
 % hd file.bin 
00000000  04 00 00 00                                       |....|
00000004

 % ./main file.bin 
Just read 4

A mon sens j'arrive bien à "désérialiser" mon entier. Cela dit à force de progresser dans mes recherches, j'ai l'impression que le terme est mal choisi ?

Et je voulais savoir si c'était la bonne manière de faire car j'ai eu une discussion avec germinolegrand et il en ressort que :

1
2
3
4
5
6
16:13 < germinolegrand> Ge0: pour ta question en C++, en général on fait ça à la main, ou du moins à l'aide le la librairie standard
16:14 < germinolegrand> après si tu veux lire un format spécifique et connu, comme le png, y'a des petites libs en header only qui font ça très bien 
                        et de manière optimisée
16:16 < Ge0> à la main donc
16:16 < Ge0> genre avec fstream.read()/.write() ?
16:17 < germinolegrand> ça c'est pour la lecture du fichier, c'est un problème séparé

Pourtant les opérateurs de flux << et >> sont normalement utilisés pour gérer du contenu "textuel" et non "binaire". Dans ce cas, la solution idéale consisterait donc à :

  • mapper le fichier en mémoire (mais s'il est trop gros, pourquoi ne pas avoir recours à un flux ? Y a -t-il autre chose ?) puis…
  • … Parser le buffer en mémoire à coup de reinterpret_cast ?

Avec reinterpret_cast, il faut faire gaffe à l'endianness si on veut être portable (big endian/little endian).

Pour la lecture, perso je passe par des filebuf et sgetn. Normalement, les implémentations sont suffisamment bien faite pour ne pas utiliser le buffer interne s'il n'y en a pas besoin. De toute façon les accès disque sont suffisamment lents, même une mauvaise implémentation n'aura pas beaucoup d’impact.

Mmap est une solution mais non portable.

Autant reinterpret_cast peut être pratique pour ce que tu veux, autant je suis pas sûr que la création d'une structure représentant l'en-tête soit forcément stocké exactement comme on peut le supposer. Il y a souvent des histoires d'alignement qui peuvent rentrer en compte et décaler les champs : Alignement en mémoire

Il faut aussi prendre en compte le fait que certains types n'ont pas toujours la même taille selon les architectures et les systèmes.

Il me semble donc important de lire les données élément par élément et d'utiliser exclusivement des types à taille fixe (genre int32_t pour un entier signé 32 bits).

Comme dit par jo_link_noir, il faut aussi vérifier l'endianness de la source et de l'architecture sur laquelle tourne ton programme. Pour le coup, je n'ai pas de solution miracle. Les décalages de bits ne forcent les opérandes à être des ints, ce qui ne colle pas avec les types à taille fixe.

Lu'!

Ta problématique me semble bizarrement tournée. D'un côté, tu parles de sérialisation/désérialisation et d'un autre le traitement de format standard existant. Si le second est inclut dans le premier, les solutions les plus rapides à adopter ne sont pas les mêmes.

En l'occurrence, quand on a un format standardisé comme PNG, on se prend pas la tête : on chope libpng et la désérialisation/décodage on l'a.

Pour ce qui est du module Spirit de Boost, tu tapes un peu à côté, leur but est de permettre de faire des parseurs mais je dirai plus dans l'optique d'en faire un AST par exemple. Avoir une analyse lexicale.

Pour ce qui est de reinterpret_cast, son défaut a été pointé : tes fichiers deviennent non-portables.

Finalement ma question : quel est le problème concret, spécifique, auquel tu cherches une solution ? Si c'est sérialiser/désérialiser : boost.serialize. Si c'est un format spécifique connu, la lib libre qui le propose déjà. Si c'est désérialiser des données d'un format que tu connais mais dont tu n'as pas de bibliothèque de manipulation alors de l'huile de coude et le cerveau réglé sur le plus haut niveau de paranoia que tu as de disponible.

Pour un format connu, je rejoins les avis précédents, soit il existe une bibliothèque, soit on le fait manuellement, en général à la C. Avec du simple fopen/fread et des structures qui représentent les différents blocs de données à taille fixe, ça marche assez bien. IL faut effectivement faire attention à l'endianess et à l'alignement, mais en général ça passe sans aucun problème. La plupart des formats binaires pas trop exotiques et pas trop vieux sont en little-endian comme la plupart des ordinateurs aujourd'hui, et justement avec des alignements calculés pour que ce soit simple à programmer sans se prendre la tête. Le seul qui fait encore abondamment du big-endian à ma connaissance aujourd'hui, c'est Java…

Pour être sûr d'avoir des entiers de taille fixe quelque soit l'architecture, il y a stdint.h qui définit des types comme uint8_t, int16_t, etc. IL vaut mieux les utiliser pour éviter toute surprise.

Je ne peux pas plus t'aider pour PNG, mais je l'ai déjà fait pour les fichiers Wave. C'est, au demeurant, assez facile. Si c'est un format ouvert ou suffisament répandu, les docs indiquent clairement la taille des en-têtes et de chaque champ. Pour le Wave c'est 44 octets par exemple.

+1 -0

@Ksass`Peuk

En l'occurrence, quand on a un format standardisé comme PNG, on se prend pas la tête : on chope libpng et la désérialisation/décodage on l'a.

Le format PNG était choisi à titre d'exemple. Si tu vas me dire d'utiliser une bibliothèque déjà écrite pour n'importe quel format que je te propose, tu ne répondras pas à ma question et on ne sera pas sorti de l'auberge.

C'est comme si je souhaitais, par exemple, stocker des informations sur une personne représentée par la classe suivante :

1
2
3
4
5
6
class Person {
private:
  std::string name_;
  int age_;
// ...
};

Au format binaire. Et là tu vas me dire "mais fais ça en XML/JSON" ? …

Pour ce qui est du module Spirit de Boost, tu tapes un peu à côté, leur but est de permettre de faire des parseurs mais je dirai plus dans l'optique d'en faire un AST par exemple. Avoir une analyse lexicale.

Tu seras d'accord pour admettre que le sujet prête un peu à confusion au vu de ce résultat de recherche.

Pour ce qui est de reinterpret_cast, son défaut a été pointé : tes fichiers deviennent non-portables.

Peux-tu développer ?

Si c'est sérialiser/désérialiser : boost.serialize

Les classes proposées par Boost permettent certes d'archiver des classes, mais cette notion d'en-tête pré-ajoutée avant mes données me pose problème. Si tu prends l'exemple d'un fichier BINAIRE - mettons, un ELF, tu l'avais peut-être pas venue venir, celle-là - est-ce qu'il possède un en-tête écrit par Boost ? Non. Donc ce n'est pas ce que je cherche même si ça s'en rapproche.

J'avais trouvé ça mais ça n'est utilisé qu'à titre d'exemple, sans être présent dans Boost… On y est presque.

Si c'est désérialiser des données d'un format que tu connais mais dont tu n'as pas de bibliothèque de manipulation alors de l'huile de coude et le cerveau réglé sur le plus haut niveau de paranoia que tu as de disponible.

Peux-tu me dire pourquoi il est question de paranoïa ?

@QuentinC

Tu me parles de choses que je comprends déjà.

On s'éloigne de la question initiale et pourtant le seul qui m'a apporté une réponse pertinente, c'est Stranger.

J'ai un format binaire. Je veux le parser en C++. Quelle est la bonne manière de faire ?

La solution que j'envisage pour le moment, c'est de "mapper" le fichier en mémoire et d'itérer sur les offsets à coup de reinterpret_cast. Je m'étonnais surtout qu'il n'existe pas nécessairement de module présent dans la STL ou dans Boost qui permette de faire simplement ce genre de chose.

Ou alors, peut-être que, oui, je suis à côté de la plaque au vu de mon cruel manque de compréhension / de compétences en informatique.

Sinon, il y a une méthode qui n'est pas très sexy et qui est un peu ma solution de dernier recours : créer une classe contenant un vector ou tableau de char. Puis créer des accesseurs qui vont lire et écrire les valeurs de chaque champ à la bonne position.

Elle permet de s'abstraire des problèmes d'endianess et de taille et positions des champs assez facilement (très utile dans l'implémentation des protocoles polymorphes). Par contre, ça t'oblige à réfléchir un minimum à l'organisation des données si leurs tailles sont dynamiques…

Un gros avantage, également : tu peux générer facilement les classes de codecs à partir d'une description formelle.

+0 -0

Je vais quand même répondre aux questions qui n'ont pas été répondues et commenter deux trois trucs.

Pour ce qui est de reinterpret_cast, son défaut a été pointé : tes fichiers deviennent non-portables.

Peux-tu développer ?

Ge0

créer une classe contenant un vector ou tableau de char. Puis créer des accesseurs qui vont lire et écrire les valeurs de chaque champ à la bonne position.

Elle permet de s'abstraire des problèmes d'endianess et de taille et positions des champs assez facilement

RomHa Korev

Si seulement. Mais a priori non. L'endianess c'est pas seulement l'ordre des bits :/ .

C'est comme si je souhaitais, par exemple, stocker des informations sur une personne représentée par la classe suivante :

1
2
3
4
5
6
class Person {
private:
  std::string name_;
  int age_;
// ...
};

Au format binaire. Et là tu vas me dire "mais fais ça en XML/JSON" ? …

Ge0

Non, m'en fout moi. Mais si tu étais sur un format standard, c'était gagné. Après effectivement, si tu sérialises/désérialises un format particulier, c'est toi qui choisit ton matos. On voulait juste être surs que tu réinventais pas la roue ;) .

Pour ce qui est du module Spirit de Boost, tu tapes un peu à côté, leur but est de permettre de faire des parseurs mais je dirai plus dans l'optique d'en faire un AST par exemple. Avoir une analyse lexicale.

Tu seras d'accord pour admettre que le sujet prête un peu à confusion au vu de ce résultat de recherche.

Ge0

On en apprend tous les jours avec boost. Du coup, ça pourrait en partie répondre à ton besoin. Cela dit, de ce que je me souviens de l'usage de Spirit, il te demande quand même de décrire la grammaire de tes données donc ça peut être chiant (à voir).

Si c'est désérialiser des données d'un format que tu connais mais dont tu n'as pas de bibliothèque de manipulation alors de l'huile de coude et le cerveau réglé sur le plus haut niveau de paranoia que tu as de disponible.

Peux-tu me dire pourquoi il est question de paranoïa ?

Ge0

Parce que c'est toujours le problème avec le parsing. Il y a toutes les raisons du monde pour que ça foire donc il faut vraiment être braqué en mode "le monde veut ma mort" pour ne pas oublier de cas.

Les classes proposées par Boost permettent certes d'archiver des classes, mais cette notion d'en-tête pré-ajoutée avant mes données me pose problème. Si tu prends l'exemple d'un fichier BINAIRE - mettons, un ELF, tu l'avais peut-être pas venue venir, celle-là - est-ce qu'il possède un en-tête écrit par Boost ? Non. Donc ce n'est pas ce que je cherche même si ça s'en rapproche.

Ge0

Non, je ne l'avais pas vu venir effectivement, mais ç'aurait pu être pas mal de le préciser, tu ne crois pas ? Donc c'est du ELF que tu veux parser ou c'était encore un exemple pris au hasard ?

C'était encore un exemple pris au hasard.

On voulait juste être surs que tu réinventais pas la roue ;) .

Encore une fois… Je n'ai pas demandé "quelle bibliothèque pour parser tel format".

Merci pour ta réponse, mais pour le coup, je crois que j'ai mieux fait d'aller poster sur StackOverflow en remaniant ma question.

IL me semble que là tu as obtenu toutes les possibilités imaginables. Je ne vois pas ce que tu pourrais chercher d'autre comme genre de réponse.

Tu parses un format standardisé ? alors il y a le plus souvent des bibliothèques en C déjà écrites pour le faire.

Tu parses un format que tu as conçu perso ? Alors le plus simple est d'utiliser une bibliothèque genre boost::serialize.

Tu parses un fichier binaire dans un format non standardisé et non perso, p.ex. un ami t'a indiqué le format utilisé, ou bien ce n'est pas toi qui a enregistré le fichier et donc tu ne peux pas le modifier ? Alors tu n'as pas le choix, il faut faire ta tambouille perso avec les techniques déjà exposées.

IL existe des langages pour décrire des structures de données binaires, des parseurs et des générateurs de parseurs correspondants, p.ex. ASN. Mais je suis d'avis que ça va te prendre 12 fois plus de temps que faire ton petit truc perso, si tu ne dois pas gérer un grand nombre de cas hyper génériques.

Je crois qu'en général, la tendance est de moins en moins stocker des données directement en binaire là où ce n'est pas absolument nécessaire, est de privilégier au contraire les formats texte comme JSON ou XML. Oui, un parseur JSON ou XML, c'est lourd, mais ça a aussi plein d'avantages, dont celui de ne pas se casser la tête inutilement avec du parsing chiant à coder et facilement buggé.

+1 -1

Pour répondre très rapidement, parser un fichier en binaire est quelque chose de très commun en C++ (selon les secteurs d'industrie). Et oui, les outils pour le faire ne sont pas forcément très évolués malheureusement.

Et qu'on ait à gérer de l'endianess ou pas il faudra passer par du reinterpret_cast.

Et il faudra inventer soi-même les méthodes de sérialisation. Exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Person {
private:
  std::string name_;
  int age_;
// ...
};

Person p{"GeO", 25};
stream.write(reinterpret_cast<char*>(p.name_.size()), sizeof(p.name_.size()));
stream.write(reinterpret_cast<char*>(p.name_.begin()), p.name_.size());
stream.write(reinterpret_cast<char*>(&p.age_), sizeof(p.age_));

Person rp;
auto rp_name_size = rp.name_.size();
stream.read(reinterpret_cast<char*>(rp_name_size), sizeof(rp_name_size));
rp.name_.resize(rp_name_size);
stream.read(reinterpret_cast<char*>(rp.name_.begin()), rp.name_.size());
stream.read(reinterpret_cast<char*>(&rp.age_), sizeof(rp.age_));

Un autre exemple ici : http://en.cppreference.com/w/cpp/io/basic_istream/read

Il n'y a pas de flux standard qui offre des "flux binaires".

Ouvrir le fichier en mode binaire permet de s'assurer qu'il prendra pas un double \0 comme la fin du fichier (problème classique).

+1 -0

Deux-trois détails sur ton code :

  • reinterpret_cast<char*>(p.name_.begin()) devrait être remplacé par p.name_.c_str(), il est fait pour ça.
  • La lecture des strings ne doit pas se faire de la manière dont tu le fais. D'ailleurs, le compilateur est sensé ne pas te l'autoriser : p.name_.c_str() donne un const char*. La solution est d'avoir un buffer assez grand et de mettre ensuite son contenu dans la std::string via une affectation.
  • Si l'enregistrement se fait sur une machine big-endian et que la lecture se fait sur une machine little-endian, tu vas avoir de très mauvaises surprises.

L'endianess c'est pas seulement l'ordre des bits :/ .

Ksass`Peuk

Pour le coup, je serais curieux de savoir ce que ça peut être si c'est pas l'ordre des bytes.

+0 -0
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