Retrouvez cet article dans : Linux Magazine 84
Fichier de ressources ?
On peut imaginer un fichier de ressources comme une archive tar. Plusieurs fichiers sont stockés dans un seul et on a un moyen d’accéder individuellement à chacun. On va commencer par valider un point de détail. Est-il possible de stocker des données dans un programme ?$ cp `which ls` ./ $ ./ls / bin dev initrd lib mnt root sys var boot etc initrd.img lost+found opt sbin tmp vmlinuz cdrom home initrd.img.old media proc srv usr vmlinuz.old $ cat /etc/passwd >> ./ls $ ./ls / bin dev initrd lib mnt root sys var boot etc initrd.img lost+found opt sbin tmp vmlinuz cdrom home initrd.img.old media proc srv usr vmlinuz.oldOn se crée une copie locale du programme ls. On vérifie qu’il fonctionne. On utilise la commande cat pour y ajouter le contenu du fichier /etc/passwd. ls fonctionne toujours. Vous pouvez retrouver le contenu du fichier passwd via cat ./ls ou hexdump -C ./ls. Habituellement, les ressources sont constituées d’un en-tête. On peut ainsi y trouver les noms et tailles de ressources et éventuellement l’endroit dans le fichier où elles sont stockées, comme dans la table d’allocation de fichiers (FAT) sur une partition de disque dur. Le problème que l’on se pose ici est de stocker des ressources dans l’exécutable. Cette logique ne peut donc pas être gardée. En effet, on ne peut rien écrire en tête de l’exécutable. On doit donc inverser le raisonnement. Plutôt qu’un en-tête, on va utiliser une " queue de fichier " qui aura la forme suivante :
Tout le début du fichier sera l’exécutable lui-même.
Les ressources sont placées à la suite et doivent donc être lues depuis la fin. Pour cette raison, on " signe " le fichier, afin de faire la différence entre un fichier comprenant des ressources, de l’exécutable seul. On utilise une simple chaîne de caractères pour signifier la présence de ressources, ici SDLALLINONE suivi d’un zéro terminal.
Après avoir vérifié la présence de cette signature, on peut " remonter " dans le fichier. Juste avant la signature se trouve le nombre de couples représentant une ressource. Ce nombre est codé sur 4 octets, le sens big endian/little endian étant ignoré.
Un couple représentant une donnée est constitué des données brutes et est suivi par la taille en octets occupée par ces données. Cette taille est, elle aussi, codée sur 4 octets. Dans les faits, une donnée sera stockée sur 2 couples, le premier couple de données servant au stockage du nom de la donnée, et le second à son contenu.
Grâce à cette structure, on peut accéder à n’importe laquelle des données quelle que soit la taille de l’exécutable en introduction.
Packager : dopack
Maintenant que la structure est connue, on peut s’attaquer à la création de l’outil qui réalisera ces packs. J’ai choisi l’utilisation suivante pour l’outil qu’on appellera dopack :USAGE: dopack executable [res_top_dir]On passe à
#include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <dirent.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <limits.h> const char CHECK_SUFFIX[] = “SDLALLINONE”; const long l_size = sizeof(long); const long suffix_len = sizeof(CHECK_SUFFIX);On commence par charger les fichiers d’en-tête nécessaires et on définit trois constantes, pour la signature des fichiers, pour stocker la taille d’un long et la taille de la signature.
int main (int argc, char** argv) {
int packfd;
long nb_resources = 0;
long nb_couples = 0;
char* pack;
char* topdir = "res";
if (argc < 2 || argc > 3) {
fprintf (stderr,
"USAGE: %s executable [res_top_dir]\n",
argv[0]);
fprintf (stderr,
„ res_top_dir defautls to res\n“);
return 1;
}
pack = argv[1];
if (argc == 3) {
topdir = argv[2];
}
On déclare quelques variables. Le descripteur de fichier du pack. Le nombre de ressources et le nombre de couples contenus dans le pack, le nom de l’exécutable et le nom du répertoire contenant les ressources, fixé à int main (int argc, char** argv) {
int packfd;
long nb_resources = 0;
long nb_couples = 0;
char* pack;
char* topdir = "res";
if (argc < 2 || argc > 3) {
fprintf (stderr,
"USAGE: %s executable [res_top_dir]\n",
argv[0]);
fprintf (stderr,
„ res_top_dir defautls to res\n“);
return 1;
}
pack = argv[1];
if (argc == 3) {
topdir = argv[2];
}
On prévoit un message informatif prévenant du début de la création du pack. On ouvre l’exécutable et on se place en fin de fichier. La fonction - On affiche le nombre de fichiers ajoutés pour contrôle et on l’ajoute également à l’exécutable.
- On " signe " enfin le " pack " en y inscrivant le suffixe.
- On va à présent s’intéresser au code faisant le gros du travail, l’ajout des ressources proprement dit.
int append_res (int packfd, const char *path, long* nb_res) {
DIR *rep;
struct dirent *entry;
struct stat s;
char* tmp_path = NULL;
int isOK = 1;
if ((rep = opendir(path))) {
while ((entry = readdir(rep))) {
tmp_path = (char*)realloc (tmp_path,
strlen(path)+strlen(entry->d_name)+2);
strcpy (tmp_path, path);
strcat (tmp_path, “/”);
strcat (tmp_path, entry->d_name);
if (!strcmp (entry->d_name, “.”)) {
continue;
}
if (!strcmp (entry->d_name, “..”)) {
continue;
}
if (-1 == stat (tmp_path, &s)) {
fprintf (stderr, “Can’t stat %s\n”, tmp_path);
isOK = 0;
break;
}
if (S_ISDIR(s.st_mode)) {
append_res (packfd, tmp_path, nb_res);
} else if (S_ISREG(s.st_mode)) {
if (append_file (packfd, tmp_path)) {
++(*nb_res);
} else {
fprintf (stderr, “Can’t append %s\n”, tmp_path);
isOK = 0;
break;
}
}
}
closedir(rep);
free (tmp_path);
} else {
fprintf (stderr, “Can’t open %s\n”, path);
isOK = 0;
}
return isOK;
}
Le code de la fonction - On supprime les entrées qui ne nous intéressent pas ("." et "..").
- Si on rencontre un répertoire (
S_ISDIR), on relance une exploration. - Si on rencontre un fichier (
S_ISREG), on appelle la fonctionappend_filequi l’ajoute au " pack ".
append_file
Il ne nous reste plus qu’un morceau du mécanisme de génération des packs. L’ajout de ressources proprement dit. Ce travail est pris en charge par la fonctionint append_file (int packfd, const char *path) {
int isOK = 1;
int datafd;
char buffer[4096];
long bufferlen;
long taille;
bufferlen = strlen(path);
write (packfd, path, bufferlen);
write (packfd, &bufferlen, l_size);
printf (“Ajout de %s\n”, path);
taille = 0;
datafd = open (path, O_RDONLY);
if (-1 == datafd) {
printf ("Attention, erreur sur %s\n", path);
write (packfd, &taille, l_size);
isOK = 0;
}
do {
bufferlen = read (datafd, buffer, sizeof(buffer));
if (bufferlen > 0)
taille += write (packfd, buffer, bufferlen);
else
break;
} while (1);
write (packfd, &taille, l_size);
close (datafd);
return isOK;
}
Utiliser : Application à la SDL
Pour comprendre la suite, une courte présentation d’un aspect méconnu de la SDL, lesIntroduction aux SDL_RWOps
La bibliothèquetypedef struct SDL_RWOps {
int (SDLCALL *seek)(struct SDL_RWOps *context,
int offset, int whence);
int (SDLCALL *read)(struct SDL_RWOps *context,
void *ptr, int size, int maxnum);
int (SDLCALL *write)(struct SDL_RWOps *context,
const void *ptr, int size, int num);
int (SDLCALL *close)(struct SDL_RWOps *context);
Uint32 type;
union {
struct {
int autoclose;
FILE *fp;
} stdio;
struct {
Uint8 *base;
Uint8 *here;
Uint8 *stop;
} mem;
struct {
void *data1;
} unknown;
} hidden;
} SDL_RWOps;
Un type et une zone de données sont également prévus dans la structure extern DECLSPEC SDL_RWOps * SDLCALL SDL_RWFromFile(const char *file, const char *mode); extern DECLSPEC SDL_RWOps * SDLCALL SDL_RWFromFP(FILE *fp, int autoclose); extern DECLSPEC SDL_RWOps * SDLCALL SDL_RWFromMem(void *mem, int size); extern DECLSPEC SDL_RWOps * SDLCALL SDL_RWFromConstMem(const void *mem, int size);Ces considérations, bien qu’intéressantes, ne seront pas toutes utiles ici. Elles permettent néanmoins d’appréhender ce que sont les
Chargement de la ressource en mémoire
Le plus délicat avec le format choisi consiste à retrouver les ressources dans le pack et à en charger le contenu en mémoire. Voyons ensemble le code se chargeant de cette tâche.SDL_RWOps *SAIO_GetResource (const char* resFile, const char* resPath, char** donnees) {
La fonction int us_fd;
long taille_donnees = 0;
long nb_couples;
long i;
long taille_nom = 0;
long taille = 0;
char* nom;
char buffer[suffix_len];
us_fd = open (resFile, O_RDONLY);
if (-1 == us_fd) {
return NULL;
}
On déclare un descripteur de fichier pour ouvrir le pack, c’est-à-dire l’exécutable lui-même dans cet exemple et on s’auto-ouvre en lecture seule.
lseek (us_fd, -suffix_len, SEEK_END);
read (us_fd, buffer, suffix_len);
if (0 != strcmp(buffer, CHECK_SUFFIX)) {
close (us_fd);
fprintf (stderr, “Attention: %s n’est pas “
“une appli SAIO\n”, resFile);
return NULL;
}
lseek (us_fd, -suffix_len, SEEK_CUR);
On se place à la fin du fichier moins la longueur du suffixe. On lit le nombre de caractères correspondant à la taille du suffixe pour vérifier qu’il s’agit bien d’un exécutable contenant ses ressources. En cas d’erreur, on se referme, on affiche un message d’erreur et on retourne lseek (us_fd, -l_size, SEEK_CUR);
read (us_fd, &nb_couples, l_size);
if (0 == nb_couples) {
fprintf (stderr, “Attention: %s ne contient “
“pas de resources\n”, resFile);
return NULL;
}
lseek (us_fd, -l_size, SEEK_CUR);
On tente ensuite de lire le nombre de ressources qui se trouvent dans ce pack. Le principe de lecture est toujours le même, on remonte du nombre d’octets correspondant à la taille de la donnée que l’on souhaite lire, on la vérifie et on remonte d’autant d’octets que la lecture nous a fait avancer si le test s’est avéré bon.
i = nb_couples-1;
nom = NULL;
*donnees = NULL;
do {
lseek (us_fd, -l_size, SEEK_CUR);
read (us_fd, &taille, l_size);
lseek (us_fd, -l_size, SEEK_CUR);
if (i%2) { //données
taille_donnees = taille;
} else { // nom
taille_nom = taille;
lseek (us_fd, -taille, SEEK_CUR);
nom = (char*)realloc (nom, taille+1);
if (nom) {
memset (nom, 0, taille+1);
read (us_fd, nom, taille);
if (!strcmp (resPath, nom)) {
lseek (us_fd, l_size, SEEK_CUR);
*donnees = (char*)malloc (taille_donnees);
if (*donnees) {
read (us_fd, *donnees, taille_donnees);
}
break;
}
}
}
lseek (us_fd, -taille, SEEK_CUR);
} while (i--);
free (nom);
On remonte ensuite dans les ressources, tant qu’il en reste ou jusqu’à ce qu’on ait trouvé celle correspondant au chemin recherché.
A chaque fois, on commence par lire la taille de la ressource.
S’il s’agit de données, on stocke la taille pour l’utiliser éventuellement au tour suivant.
S’il s’agit d’un nom de ressource, on le compare à celui de la ressource recherchée. S’ils correspondent, on charge la ressource en mémoire, après avoir alloué une zone de la taille correspondante.
if (!*donnees) {
fprintf (stderr, "Attention: %s ne contient pas %s\n",
resFile, resPath);
return NULL;
}
close (us_fd);
return SDL_RWFromMem(*donnees, taille_donnees);
}
A la fin de la boucle, soit on n’a pas trouvé la ressource recherchée et la variable Images
extern DECLSPEC SDL_Surface * SDLCALL IMG_Load_RW(SDL_RWops *src, int freesrc);Le second paramètre spécifie si la structure
SDL_Surface* SAIO_IMG_Load (const char* resFile,
const char* resPath) {
SDL_Surface* sdl_surf = NULL;
SDL_RWops *rw;
char* donnees = NULL;
rw = SAIO_GetResource (resFile, resPath, &donnees);
if (!rw) {
// On essaye à l’ancienne
return IMG_Load (resPath);
}
sdl_surf = IMG_Load_RW(rw, 1);
free (donnees);
return sdl_surf;
}
Son
On a également à notre disposition les fonctionsExemple : Aliens
J’ai placé en ligne (voir liens et références) une version d’Aliens, un programme démo trouvé sur le site de la$ ./dopack aliens data Packaging de aliens Ajout de data/README Ajout de data/alien.gif Ajout de data/background.gif Ajout de data/explode.wav Ajout de data/explosion.gif Ajout de data/music.it Ajout de data/music.wav Ajout de data/player.gif Ajout de data/shot.gif Ajout de data/shot.wav 10 resourcesVous pouvez à présent déplacer l’exécutable seul, l’envoyer à vos amis, etc. Attention, le binaire reste dépendant des bibliothèques, mais on gagne toutefois en facilité de distribution, quand il s’agit d’envoyer des démos ou des versions intermédiaires, ça facilite la vie.
$ ./aliens

Conclusion
Tout au long de l’article, on a travaillé directement sur l’exécutable. On pourrait toutefois faire des fichiers de ressources suivant ce principe, sans qu’ils commencent par un exécutable, pour différencier les données de chaque niveau d’un jeu par exemple. La première version de ce code n’était pas appliquée à la SDL, mais permettait de packer des ressources à la suite d’un dépackeur, ce qui permet d’obtenir des archives auto-extractibles. Les applications sont nombreuses et favorisent une distribution aisée, mais ce concept était plus parlant pour des utilisateurs Windows ou OS X. Cette version permet, je l’espère, de rassembler un plus large public, bien que le principe soit presque identique. En effet, dans l’exemple que nous avons vu, on n’extrait pas les fichiers, on travaille directement sur le pack. Références- Bibliothèque SDL : http://www.libsdl.org/index.php
- SDL_mixer : http://www.libsdl.org/projects/SDL_mixer/
- SDL_image : http://www.libsdl.org/projects/SDL_image/
- Tutoriel SDL_RWOps/ZLib : http://www.kekkai.org/roger/sdl/rwops/rwops.html
- Game Programming Wiki : http://gpwiki.org/
- Aliens original : http://www.libsdl.org/projects/aliens/
- Sources : http://www.xgarreau.org/aide/devel/saio/





Donnez votre avis
Vous devez avoir ouvert une session pour écrire un commentaire.