|
…ou bien comment
une souris peut-elle digérer un lion ? La problématique est réelle et de plus en
plus fréquente : Nous devons souvent analyser des fichiers de grande taille,
pour des taches comme la compression, cryptage, recherche etc… Aujourd’hui, des
fichiers qui dépassent le giga ne sont pas rare, il suffit de se balader sur les
réseaux de Peer to Peer pour s’en convaincre ! Le problème est que charger en
mémoire des fichiers de plusieurs dizaines de Mo suffit à paniquer le noyau qui
gère la RAM...qui en récompense vous inflige un swap cyclonique à l’origine de
vos pulsions du doigt sur le bouton Reset….
Alors si vous voulez épater la galerie en
expliquant que vous savez traiter des fichiers de plusieurs gigaoctets avec
seulement quelques ko de buffer, lisez ce qui suit…
Bases
Pour récupérer le flux binaire stocké dans un
fichier, le principe est simple : Nous ouvrons ce fichier. Ainsi fonctionne les
logiciels comme Word, Excel, Photoshop etc.
Ouvrir le fichier « sous entend » ici le transfert
de toutes les données du fichier vers un espace mémoire précédemment alloué.
Peut-être avez-vous remarqué, mais lorsque vous ouvrez un gros fichier Word,
disons 20 Mo, la RAM disponible ne baisse pourtant pas de 20 Mo…La technique consiste à
découper le fichier en morceaux et à les charger au besoin (On retrouve dans la
VCL des techniques similaires avec les buffers de données et les grilles de
visualisation - DBGrid -).
Au sens Windows du terme, ouvrir un fichier
correspond plutôt à créer un handle (un identifiant windows) sur le fichier en
question, puis ensuite toutes les manipulations de lecture, déplacement,
écriture s’effectuent à l’aide d’appels aux API spécialisées par l’intermédiaire
de ce handle. Nous n’appelons cependant pas directement ces API car nous avons à
notre disposition des classes dédiées comme TFileStream.
TFileStream est une classe qui sait obtenir un
handle à partir d’un nom de fichier, et qui est capable de lire et écrire dans
ce fichier à l’aide des méthodes héritées de TStream : Read et Write.
Voici un diagramme de classe situant TFileStream :

TFileStream offre les interface de création du
fichier (création au sens général, c'est à dire création d'un handle sur un
fichier désigné par son chemin).
THandleStream obtient un handle sur un fichier et
manipule ce dernier à partir du handle (les API requierent ce fameux handle) :
Lecture, Deplacement et Ecriture.
TStream comporte les attributs et méthodes de base
à tous les gestionnaires de flux (size, position etc.).
Nouvelle Classe :
TProcessFileStream
Nous allons exploiter l'existant et écrire une
nouvelle classe, qui héritera de
TFileStream, et qui sera capable de mettre en mémoire un fichier, morceau
par morceau (chunk by chunk ou quignon de pain par quignon de pain comme vous
préférez :-)), et une fois que ce morceau sera en mémoire, notre objet le mettra
à disposition du développeur (sous forme de callback, mais on aurait aussi pu
choisir une approche évènementielle).
Effectuons un inventaire des futures capacités de
notre objet, avec la liste - restreinte - de ses nouvelles propriétés et
méthodes :
|
Propriétés |
EXPLICATIONS |
|
ChunkMaxSize: integer
|
Taille en octets
d'un morceau. Important car on va spécifier ici quelle taille mémoire on va
réserver au traitement de notre fichier. |
| ChunkCount:
integer |
En lecture seule,
retourne de combien de morceaux est constitué le fichier. |
|
Méthodes |
|
|
Process(aProcessor: TChunkProcess; ProcessMode: TProcessMode); |
Méthode d'entrée
pour traiter le fichier. ProcessMode indique si on veut traiter le fichier
par morceaux ou non alors que aProcessor est une méthode passée en
paramètre. Cf ci-dessous pour plus de précisions. |
Diagramme avec la nouvelle
TProcessFileStream *:

(* UML permet de n'afficher que
ce que l'on désire, raison pour laquelle on ne découvre ici que les éléments les
plus importants)
Fonctionnement interne de
TProcessFileStream :
Nous venons de
modéliser notre future classe. Il faut bien comprendre comment la méthode
Process est sensée fonctionner, ensuite nous nous attaquerons à
l'implémentation.
Lorsque nous
appellons la méthode Process, nous lui passons deux paramètres :
C'est aProcessor qui
demande de l'attention car il s'agit d'un paramètre au type un peu particulier :
il est de type procédure d'objet. En effet étudions la définition de
TChunkProcess :
TChunkProcess =
procedure(buffer: pointer; Size: integer) of object;
Cette
procédure d'objet attend deux paramètres : un pointeur sur un buffer et Size, un
entier et c'est une procédure de ce type qui est passée en paramètre à la
méthode TProcessFileStream.Process. Reprenons au début, admettons que nous
appelons la méthode Process. Process s'exécute et pour chaque morceau de fichier
(en fonction de chunkMaxSize et de ProcessMode) appelle la méthode aProcessor
passée en paramètre à l'appel de Process. Ainsi pour chaque morceau du fichier
cette méthode sera appelée, accompagnée des deux informations magiques : un
pointeur sur la zone mémoire où est mis en buffer le morceau qui vient d'être
lu, suivi de la taille de cette zone mémoire (ou du morceau c'est pareil).
En
définitive, la classe TProcessFileStream ne sait pas comment vont être exploités les
morceaux du fichier qu'elle expose, elle se contente de fournir successivement
les morceaux dans un buffer à la méthode passée en paramètre. On ne connait que
le nom de cette méthode, mais pas forcément ce qu'elle fait.
L'intérêt est de pouvoir profiter du mécanisme, en passant en paramètre à
Process, n'importe quelle méthode pourvue qu'elle soit du type TChunkProcess.
Ensuite ce que cette méthode fait avec le buffer, cela importe peu, le boulot de
TProcessFileStream, qui consistait à exporter des morceaux d'un fichier vers une
méthode externe, est terminé.
Vous
lirez plus loin un petit exemple qui vous aidera à assimiler et concrétiser ces
petits concepts basés sur les callback.
implémentation
Enfin
un peu de code... Je sais que beaucoup de développeurs ont déjà les mains
plongées dans le code avant de réfléchir aux fonctionnalités réelles de son
projet, mais notre petite réflexion préalable était justifiée, si si :-)
| |
... TChunkProcess
= procedure(buffer: pointer; Size: integer) of object;
TProcessMode = (pmAll, pmByChunk);
TProcessFileStream =
class(TFileStream)
private
fBuffer: pointer;
fchunkCount: integer;
fchunkPassed: integer;
fchunkMaxSize: integer;
fLastchunkSize: integer;
fProcessMode: TProcessMode;
procedure SetchunkMaxSize(const Value: integer);
function GetchunkCount: integer;
protected
public
property chunkCount: integer read GetchunkCount;
property chunkMaxSize: integer read fchunkMaxSize write
SetchunkMaxSize;
property ProcessMode: TProcessMode read fProcessMode;
procedure Process(aProcessor: TchunkProcess; ProcessMode:
TProcessMode);
end;
implementation
{Setter pour la
propriété ChunkMaxSize. On vérifie que la taille du Chunk n'excède pas la
taille du fichier}
procedure
TProcessFileStream.SetchunkMaxSize(const Value: integer);
var
s: int64;
begin
s := Size;
if Value <= s then
fchunkMaxSize := Value
else
fchunkMaxSize := s;
end;
{Getter de la propriété
ChunkCount. Récupère le nombre de morceaux constituant un fichier, ainsi que
la taille du dernier Chunk}
function
TProcessFileStream.GetchunkCount: integer;
var
s: int64;
begin
result := 0;
if fHandle <> 0 then
begin
s := Size;
fchunkCount := s div fchunkMaxSize;
result := fchunkCount;
// Taille du dernier bloc dans l'attribut
fLastChunkSize
fLastchunkSize := s - (result * fchunkMaxSize);
end;
end;
{Méthode Process : La
pièce maîtresse !}
procedure
TProcessFileStream.Process(aProcessor:
TchunkProcess; ProcessMode: TProcessMode);
var
i: integer;
s: int64;
begin
fProcessMode := ProcessMode;
try
case ProcessMode of
pmAll:
// On met tout le fichier en mémoire
begin
s := Size;
GetMem(fBuffer, s);
Read(fBuffer^, s);
// On appelle la procedure Callback en
lui passant le buffer et la taille en param
aProcessor(fBuffer,
s);
end;
pmBychunk:
// On traite le fichier morceau par morceau
begin
GetMem(fBuffer, fchunkMaxSize);
for i := 0 to chunkCount - 1 do
begin
fchunkPassed := i +
1;
// On lit le
morceau courant
Read(fBuffer^,
fchunkMaxSize);
aProcessor(fBuffer, fchunkMaxSize);
end;
// Dernier chunk
inc(fchunkPassed);
Read(fBuffer^, fLastchunkSize);
aProcessor(fBuffer, fLastchunkSize);
end;
end;
finally
FreeMemory(fBuffer);
end;
end; |
EXPLOITATION
Nous allons essayer notre classe à travers un
petit exemple.
Exemple : Recherche des positions à
laquelle on rencontre un octet spécifié. Une méthode de recherche d'une suite
binaire complète est surement plus intéressante, mais il est plus prudent que
l'on se concentre sur la manipulation de TProcessFileStream.
Créons un nouvel objet qui va nous servir de
moteur de recherche d'un octet :
|
TFindOctet
= class
private
fByte: Byte;
fPosResult: integer;
public
property ByteToFind: byte read fByte write fByte;
procedure ChunkSearch(pBuffer:
pointer; Size: integer);
end;
|
procedure
TFindOctet.ChunkSearch(pbuffer: pointer; Size: integer);
var
i: integer;
b: byte;
pdeb: ^byte;
begin
i := 0;
pdeb := pBuffer;
repeat
inc(fPosResult); // On
incrémente la position absolue
inc(i);
b := pdeb^; // On récupère
le byte à la position courante du buffer
inc(pdeb);
until (b = fbyte)
or (i >= Size); // Si le byte est trouvé alors
if i < Size then
MessageDlg('Octet trouvé ! Position : ' +
inttostr(fPosResult),
mtInformation, [mbOK], 0);
end;
|
Dans un projet, faisons interagir nos deux objets
selon le diagramme de séquence suivant (puisque dans D7 nous avons ModelMaker,
l'excellent environnement UML, il serait dommage de ne pas s'amuser un peu...
:-)

1. Nous créons un objet de type TFindOctet
que nous nommerons aFindOctet et nous initialisons le byte à rechercher
aFindOctet := TFindOctet.Create;
aFindOctet.ByteToFind := $D8;
// $D8 est une valeur prise au hazard
2. Nous créons un objet TProcessFileStream,
en indiquant un fichier existant en paramètre puis nous initialisons la taille
des morceaux (chunk) de traitement.
aProcessFileStream := TProcessFileStream.Create(aFile, fmOpenReadWrite,
fmShareDenyWrite);
aProcessFileStream.ChunckMaxSize :=
3000 * 1024;
// 3 Mo de buffer
3. Nous appelons Process en passant en
paramètre le callback vers l'objet aFindOctet
aProcessFileStream.Process(aFindOctet.ChunkSearch, pmByChunk);
4. Dès à présent, pour chaque morceau
traité, la méthode ChunkSearch va être appelée (rappel du callback) avec en
paramètres un buffer contenant le nouveau morceau du fichier à traiter, ainsi
que la taille de ce morceau (qui peut différer si c'est le dernier morceau). La
méthode callback ChunkSearch n'a plus qu'à faire son travail.
Conclusion
A été présenté ici une méthode pour traiter
des fichiers volumineux par lots. Nul doute que d'autres méthodes existent,
d'ailleurs comme je le précisais plus haut, au lieu d'appeler un callback,
j'aurais pu appeler un gestionnaire d'évènement. Ce sont des choix
discutables, un peu hors sujet ici. Mais nous pouvons citer les MemoryMappedFiles,
à manipuler via les API windows ou bien ce qui fera plaisir aux nostaliques
du TurboPascal : BlockRead et BlockWrite. A vous de choisir une ou l'autre de
ces techniques si celle décrite ici vous rend morose :-)
Avec le principe du Callback, on peut concevoir
que n'importe quelle méthode est donc susceptible de traiter ces paquets
successifs de données, pourvu qu'elle interface le type TChunkProcess défini
dans notre analyse.
Les schémas UML de cet article n'étaient pas
indispensables, mais ils étaient intéressants sur le plan documentaire visuel,
en vous incitant aussi à utiliser le maximum des possibilités offertes par
Delphi 7 Studio. Les développeurs ne possédant pas cette version pourront
cependant compiler le code ci-dessus, et pour la partie UML, profiter des
schémas de ce papier pour mieux comprendre.
Comme d'habitude, si vous avez
des questions, vous savez que la rédaction de Developpez.com est là pour vous
soutenir au travers des tutoriaux, articles et forums.
Téléchargement des sources
Sylvain James
TeamB-Fr
Rédaction Developpez.com, Responsable Delphi
|