31 janv. 2011

[Code source] carray-0.3.0 (mise à jour)

carray est un petit utilitaire qui permet de générer un tableau C à partir d'un fichier quelconque. Je vous livre le code source de ce petit programme (ridiculement petit d'ailleurs), parce que je ne sais pas quoi en faire d'autre. Si vous avez des idées d'amélioration, n'hésitez pas !

Mise à jour: version 0.3.0 en téléchargement (prise en compte de la remarque de gilles sur EOF ; cf. son commentaire ci-dessous) Mise à jour: version 0.2.0 en téléchargement

Quelques précisions

Il me semble qu'une introduction ne sera pas de trop. Il règne une telle passion tout autour de ce sujet que je n'en reviens pas moi-même.

Sur le but du programme

Visiblement, il y a un malentendu sur le but de ce programme (si tant est qu'un programme aussi simple en ait un). On peut le voir comme un hexdump amélioré permettant de créer des fichiers C ou C++. On peut le voir comme un bin2c amélioré. Mais on ne doit pas le voir comme un système permettant à un programme de lire un fichier. Il n'y a pas de notion de lecture ; il n'y a pas de notion d'optimisation de lecture. La problématique est toute entière contenue dans chacun des mots de cette double question : qu'est-ce que je fais lorsque je veux insérer le contenu d'un fichier dans un programme, et faire référence au contenu de ce fichier dans le programme lui-même.

Si je n'ai que besoin d'accéder au contenu d'un fichier, j'utilise les fonctions de lectrue de fichier (y compris mmap() si le fichier est gros ; en pratique c'est rarement nécessaire dès lors que votre fichier à une taille inférieure à plusieurs MB, quoi qu'en disent les afficionados de cette fonction, à moins de besoins particuliers). Si je n'ai besoin que d'intégrer le fichier dans l'exécutable, d'autres solutions existent qui permettent la concaténation d'un fichier dans un exécutable. Mais si j'ai ces deux besoins réunis, alors j'ai besoin d'utiliser une fonctionnalité similaire.

Sur la manière de faire

Oui, on peut faire un script shell - si l'OS qu'on utilise possède un shell suffisament puissant (c'est le cas de, disons, 7% des PC actuels). Oui, on peut utiliser un autre programme existant, parce qu'il en existe à peu près autant que d'informaticiens. Oui, on peut aller voir du coté de perl ou de python (après tout, que diable, nous sommes des informaticiens ! nous connaissons tous perl et/ou python !). Oui, la réponse à la question nous est donnée par internet.

Bref : oui, personne n'a l'obligation d'utiliser cet outil. Je n'y vois aucun problème particulier. Je ne comprends même pas que ça soit un sujet de débat en fait.

Quand à l'utilisation de cmake : qu'on me pointe sur un outil qui génère des Makefiles sous *nix (ainsi que, histoire de me faciliter le travail à moi, des projets Eclipse ou code::blocks) et des projets Visual Studio corrects sous Windows (y compris pour les versiosn récentes de Visual Studio), qui me permette d'écrire des tests de plateforme afin que je puisse écrire du code portable. Alors je regarderais cet outil avec bienveillance, et s'il est stable, meilleur que cmake et bien documenté, je l'utiliserais peut-être.

En attendant, je continuerais d'utiliser cmake, dans lequel je vois un outil pratique et fonctionnel, et pas un jouet. Le système de build est la dernière chose sur laquelle j'ai envie de perdre du temps.

Compilation et utilisation

Pour compiler carray, il vous faut une version récente de CMake (honnêtement, ça doit pouvoir fonctionner avec toutes les versions récentes, mais j'ai mis une limite à CMake 2.6).

$ tar zxvf carray-0.3.0.tar.gz
$ mkdir bld && cd bld
$ cmake ../carray-0.3.0
$ make && sudo make install

Une fois carray compilé et installé, vous pouvez l'utiliser. Voici la liste des options en ligne de commande :

  • -help affiche l'aide en ligne (v0.1)
  • -c++ génère du code C++ (v0.1)
  • -ns=namespace : uniquement pris en compte si l'option -c++ est précisée. Permet de stocker les variables définies dans un namespace spécifique (v0.1)
  • -var=variable_name : par défaut, le nom de variable utilisé est file_content. Avec ce switch, vous pouvez en préciser un autre (v0.1)
  • -c++0x génère du code C++0x (std::array à la place d'un tableau C) (v0.2)
  • -eof ne fait rien (à partir de la version 0.3)
  • -constexpr si le fichier est généré en mode C++0x, la variable créée est constexpr au lieu de const (v0.2)
  • -no-unsigned à partir de la v0.2, la variable est du type unsigned char. Ce flag permet de forcer le type char (v0.2)

Depuis la version 0.3, si aucun nom de fichier n'est donné en argument, le programme lit l'entrée standard.

Voici un exemple de sortie, pour la ligne de commande carray carray/src/config.h.in -c++ -eof -no-unsigned -ns=files -var=config_h_in

namespace files {
const char config_h_in[1207] = { 0x2f, 0x2a, 0x20, 0x63, 0x61, 0x72, 0x72, 0x61, 0x79, 0x20, 0x2d, 0x20, 0x61, 0x20, 0x43, 0x2f, 0x43, 0x2b, 0x2b, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x20, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x74, 0x6f, 0x20, 0x65, 0x6d, 0x62, 0x65, 0x64, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x20, 0x69, 0x6e, 0x74, 0x6f, 0x20, 0x61, 0x20, 0x62, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x0a, 0x0a, 0x20, 0x20, 0x43, 0x6f, 0x70, 0x79, 0x72, 0x69, // ... (j'omets quelques lignes) 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x42, 0x55, 0x49, 0x4c, 0x44, 0x40, 0x0a, 0x0a, 0x23, 0x65, 0x6e, 0x64, 0x69, 0x66, 0x20, 0x2f, 0x2f, 0x20, 0x63, 0x61, 0x72, 0x72, 0x61, 0x79, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x68, 0x0a };
}

En mode C++0x, la ligne de commande carray carray/src/config.h.in -c++0x -constexpr -ns=files -var=config_h_in génère le code suivant :

#include <array>
namespace files {
constexpr std::array<unsigned char, 1207> config_h_in = { 0x2f, 0x2a, 0x20, 0x63, 0x61, 0x72, 0x72, 0x61, 0x79, 0x20, 0x2d, 0x20, 0x61, 0x20, 0x43, 0x2f, 0x43, 0x2b, 0x2b, 0x20, 0x63, 0x6f, 0x64, 0x65, 0x20, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x74, 0x6f, 0x20, 0x65, 0x6d, 0x62, 0x65, 0x64, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x20, 0x69, 0x6e, 0x74, 0x6f, 0x20, 0x61, 0x20, // ... 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x42, 0x55, 0x49, 0x4c, 0x44, 0x40, 0x0a, 0x0a, 0x23, 0x65, 0x6e, 0x64, 0x69, 0x66, 0x20, 0x2f, 0x2f, 0x20, 0x63, 0x61, 0x72, 0x72, 0x61, 0x79, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x68, 0x0a };
}

Amusez vous bien !

Commentaires

1. Le mardi, février 1 2011, 16:05 par 3DArchi

Salut,
En général pour ce genre de tableaux, je préfère unsigned char à char.
Pourquoi la taille calculée à partir du fichier est utilisée pour la taille de la variable mais pas pour le tableau. J'ai tendance à définir la taille du tableau comme un sizeof()/sizeof([0]). Si la taille calculée doit être utilisée, je penche pour celle du tableau var_[TAILLE]. Je n'ai comme ça pas de risque d'incohérence entre la taille du tableau et la variable taille associée. Et les outils d'analyse statique de code ou le compilateur peuvent me dire si je déborde ou si je ne remplis pas entièrement le tableau.

Dans les amélioration possible, l'ajout d'un paramètre C++0x/TR1 pour générer un std::array<>

permets
partidr
libvre
pPermet
config_h_in[]

2. Le mardi, février 1 2011, 17:53 par Emmanuel Deloget

Ravi de te revoir ici !

En ce qui concerne tes remarques :

  1. la taille du tableau n'est pas la taille réelle du fichier ; le caractère EOF est ajouté à la fin, histoire de se baser sur lui dans certains algorithmes. Peut être qu'un switch -eof pourrait être utilisé pour le mettre si besoin, et l'oublier dans la majeure partie des cas (et du coup, la taille du tableau pourrait être déduite du tableau en lui même)
  2. unsigned char vs. char : voui ; c'est une bonne chose. Je vais rajouter un switch -no-unsigned pour l'enlever au cas où.
  3. std::array<> en C++1x : bonne idée. Activé par un switch -std=c++0x, comme sous g++ :) (bien que le nom du switch mériterait de montrer que le standard n'est sorti avant 2010).

Que des bonnes idées (et j'ai corrigé les fautes mentionnées).

Merci !

3. Le mercredi, février 2 2011, 15:29 par lol

C'est cadeau:
http://linux.die.net/man/2/mmap

4. Le mercredi, février 2 2011, 15:45 par Emmanuel Deloget

C'est quoi le rapport ? mmap() permet tout à coup de créer du code source C ou C++ à partir d'un fichier quelconque et donc d'intégrer le contenu de ce fichier un programme, une librairie ou un module ?

Mince ! Moi qui pensait que ça permettait de mapper en mémoire le contenu d'un fichier sur disque. Ce qui semble en plus en adéquation avec son nom - memory map. Mais bon, je peux me tromper, hein, tout la monde n'a pas ta culture incroyable, monsieur Courageux.

Au revoir monsieur Courageux. Bonne journée, monsieur Courageux.

5. Le mercredi, février 2 2011, 16:38 par Monsieur patate

avec hexdump, on peut faire des trucs sympas:

@@
$ echo hello | hexdump -v -e '/1 "0x%02X, "' ; echo
0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x0A,
@@

6. Le mercredi, février 2 2011, 16:58 par lol

C'est un peu ça l'idée Monsieur patate :).

Trois cas:

1/ Dans 90% des cas le fichier sera présent sur le disque, mmap suffira donc.

2/ Il y a un absolu besoin d'avoir le contenu d'un ou plusieurs fichier(s) dans un binaire: hexdump. (Ok, si tu passe 3 jours à comprendre comment on s'en sert tu pourras toujours y faire un script "d'encapsulation", si possible avec du CMake, et le poster sur le blog).

3/ T'es sous windows et n'a pas hexdump MAIS t'as internet:
http://www.google.com/search?hl=en&...

Monsieur Courageux.

7. Le mercredi, février 2 2011, 19:16 par Emmanuel Deloget

1) le truc c'est pas de traiter le chargement d'un fichier. je ne comprends pas pourquoi on parle encore de mmap. Ce n'est pas comme si ouvrir un fichier ou le mapper en mémoire était une tâche qui méritait qu'on s'y attarde. L'idée c'est de stocker le contenu d'un fichier dans un programme. Un peu comme les ressources sous Windows, par exemple. Ca n'a strictement rien à voir avec mmap ou quoi que ce soit qui ait un rapport de prêt ou de loin avec la présence sur le disque d'un fichier.

2) pour une ligne de commande déjà complexe, hexdump fait seulement la moitié du travail. La différence entre faire un script shell qui va générer le fichier complètement et faire un programme C++ qui le fait (et d'autant plus que j'ai fait ce programme à titre d'exemple ; je n'oblige personne à l'utiliser) est assez difficile à voir pour moi. Les deux demandent à peu près autant de temps, c'est à dire de l'ordre d'une quinzaine de minutes - debuggé et testé. Je n'ai jamais prétendu offrir une solution de remplacement à hexdump ou à quoi que ce soit d'autre. L'introduction dit bien parce que je ne sais pas quoi en faire d'autre. Et puis ce programme a été reposant à écrire.

3) les solutions proposées sont (en vrac) un script python (installer l'interpréteur python), un script perl (installer l'interpreteur perl), un editeur hexadécimal qui peut générer des fichiers C à partir des données qu'il présente (?!).... Le tout de manière inflexible au possible, puisqu'il faut s'amuser à modifier le fichier généré pour obtenir ce qu'on veut (changer le type de données, mettre la variable dans un namespace, etc). A ne point en douter, c'est une grande amélioration par rapport à la solution proposée.

Je ne vois pas très bien ce que vous essayez de prouver, à par peut être le fait qu'il existe d'autres solutions au même problème, certaines plus élégantes, d'autres moins - et vu qu'on est en 2011 et que le problème date du début de l'ère de l'informatique, vous utilisez avec passion un arme de destruction massive pour enfoncer une porte que tout le monde sait être ouverte.

Autre chose : si le sujet ne vous plait pas, ne vous forcez pas à le commenter. Je peux vous aider si vous voulez. Ce n'est pas comme si ça allait provoquer chez moi une inquiétude démesurée.

8. Le jeudi, février 3 2011, 22:05 par gilles

Cf scripts/bin2c.c dans les sources du kernel Linux.

Il me semble que l'idée de mettre EOF dans le tableau et de vouloir baser des "algorithmes" (un peu pompeux non pour parler de fonctions qui accèdent à un tableau de caractère...) dessus est mauvaise:
EOF est, de fait exprès, un entier qui n'est pas représentable par un char. Il permet, en sortie d'une fonction comme getchar() de savoir si on a vraiment reçu un caractère, ou si on a atteint la fin du fichier.

L'insérer dans le tableau provoquera une conversion en char. Ce qui sera dans le tableau sera donc un caractère, généralement 255(unsigned) ou 127(signed), pas EOF. Mais qu'est-ce qui garantit du coup que ce caractère n'est pas déjà dans le blob et que donc vos "algorithmes" ne s'arrêteront pas prématurément?

D'autre part, il me semble qu'écrire un programme (en fait un script, un coup de awk, de sed, ou même de perl/python/ruby, ou même une commande de substitution emacs), qui crache simplement la liste des entiers
séparés par des virgules permet de s'affranchir des questions de langage, namespace, structure de donnée, etc, et d'obtenir une séparation saine entre code et données.

On peut simplement alors utiliser:

unsigned char foo = {
include <foo.h>
};

Pour ce qui est de mmap, je crois que vous n'avez pas compris la suggestion de "lol". Ce qu'il veut dire, c'est que:

bin2c blob > foo.h

unsigned char foo[] = {
include <foo.h>
};
size_t foo_size = sizeof(foo); /* / sizeof(foo[0]) would be preposterous here */

fournit le même résultat que:

int fd = open("/path/to/blob", O_RDONLY);
if (fd < 0) {
perror("open");
/* handle error... */
}
struct stat sb;
if (fstat(fd, &sb) < 0) {
perror("fstat");
/* handle error... */
}
size_t foo_size = sb.st_size;
unsigned char *foo = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (foo == (unsigned char *) MAP_FAILED) {
perror("mmap");
/* handle error... */
}

Pourvu que l'on s'autorise à avoir le fichier "blob" sur le disque au moment de l'exécution du programme. Ca ne parait pas être contraignant outre mesure, étant donné le peu de systèmes qui, de nos jours, fonctionnent sans accès à un système de fichiers.

Nous avons tous un jour écrit ce programme. L'intérêt de l'écrire est qu'en fait, cela prend moins de temps que de le chercher sur internet (on le trouve partout, jusque dans les sources du kernel linux). Mais alors, si l'on y réfléchit, vouloir le partager est un peu inutile (et fait perdre le temps qu'on avait gagné, au profit de personne d'autre).

<paragraphe supprimé à la demande de l'auteur>

Cordialement.

9. Le jeudi, février 3 2011, 23:25 par gilles

Sans compter que les "algorithmes" risquent de ne pas s'arrêter quand vous comparez foo[i] à EOF, la comparaison risque d'être toujours fausse même si foo[i] vaut (unsigned char)EOF.

Non, très mauvaise idée en fait.

10. Le vendredi, février 4 2011, 10:49 par Emmanuel Deloget

Bien : Gilles, je me suis permis de vous répondre par mail, afin de clarifier certaines choses qui n'ont pas à être publiques. En ce qui concerne EOF, vous avez tout à fait raison. C'est une erreur grossière, et je vais la corriger rapidement. En ce qui concerne mmap(), comment pouvez vous dans le même message citer bin2c et cette fonction ? bin2c montre que vous avez compris la problématique, et si vous avez compris la problématique à laquelle réponds ce programme, vous savez que mmap() n'a strictement rien à y voir.

Pour cmake - le jouet - je l'utilise parce que généralement, mon code doit être compilé avec le compilateur natif de la plateforme - soit gcc sous Linux et Mac OS X, et les versions les lus récente de MSC++ pour Windows. Si on me trouve un système de build portable qui est capable de produire les même résultats tout en restant simple d'approche, je ne vais pas dire non. Mais à ma connaissance, un tel produit n'existe pas, ou alors sous une forme qui n'a pas de sens pour moi. Non, automake/autoconf ne fait pas ce que je demande.

Quand à mes raisons pour proposer telle ou telle chose, elle ne regarde pour l'instant que moi. Je ne mets un couteau sous la gorge de personne. Va-t-on me dénier le droit de faire ce que je désire faire ? Ca va poser un problème philisophique grave : peut on interdire lorsqu'on se considère comme faisant partie du mouvement du logiciel libre ?

C'est étonnant comme une si petite chose peut générer autant de "passion". J'ai du mal à saisi ce qui se passe, et j'ai du mal à trouver du sens à ces quelques derniers messages.

(PS: j'ai pris la liberté de remettre en page ce que vous avez écrit, pour plus de clarté. Le texte n'a pas été modifié). 

Ajouter un commentaire

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Fil des commentaires de ce billet