on continue avec un autre gros morceau du développement Win32 : le système de fichiers et les I/O.
C’est un sujet qu’on sous-estime souvent au début, parce que lire un fichier avec ReadFile parait simple. En réalité, dès qu’on sort du cas trivial, on tombe très vite sur des notions importantes : handles, partage, mode d’ouverture, pointeur de fichier, I/O synchrones ou asynchrones, cache système, attributs, tailles, chemins, énumération de répertoires, informations détaillées, notifications, verrous, et parfois meme I/O sur des objets qui ne sont pas de simples fichiers.
Pourquoi les I/O sont un sujet important
Quand on comprend mal les entrées/sorties, on écrit souvent du code qui fonctionne “sur sa machine”, puis qui devient mauvais ou fragile dans un vrai contexte. Typiquement, cela donne :
- un programme lent parce qu’il fait trop d’I/O inutiles ou mal structurées
- un thread principal qui se bloque
- des erreurs aléatoires sur certains fichiers ou certains chemins
- des comportements différents selon le disque, le réseau, le système de fichiers ou la charge de la machine
- des handles oubliés qui deviennent des fuites de ressources
Tout passe par des handles
Sous Windows, une très grande partie du modèle d’I/O repose sur les handles. Un handle n’est pas un pointeur vers une structure user-mode. C’est une référence opaque gérée par le noyau. Quand ton programme ouvre un fichier, un pipe, un event, un mutex, un device, ou bien d’autres objets encore, il reçoit le plus souvent un HANDLE.
Il faut bien comprendre ce point, parce qu’il conditionne toute la suite. Le HANDLE représente une ressource ouverte, sur laquelle on va effectuer des opérations, puis qu’il faudra libérer correctement.
En pratique, le schéma général est presque toujours le meme :
- ouvrir ou créer une ressource
- récupérer un HANDLE
- utiliser ce HANDLE avec d’autres API
- fermer la ressource quand on a terminé
Ouvrir un fichier avec CreateFile
L’API centrale pour ouvrir un fichier en Win32 est CreateFileW. Malgré son nom, elle ne sert pas seulement à “créer” un fichier. Elle sert surtout à ouvrir ou créer un objet fichier selon le mode demandé.
Exemple :
Code: Select all
HANDLE hFile = CreateFileW(
L"test.txt",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hFile == INVALID_HANDLE_VALUE)
{
DWORD err = GetLastError();
return 1;
}
- le chemin de l’objet
- les droits demandés, par exemple GENERIC_READ ou GENERIC_WRITE
- le mode de partage, via FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SHARE_DELETE
- les attributs de sécurité éventuels
- le mode de création, par exemple OPEN_EXISTING, CREATE_ALWAYS, OPEN_ALWAYS
- les flags et attributs du fichier
- un éventuel handle de modèle
- CREATE_NEW : crée seulement si le fichier n’existe pas
- CREATE_ALWAYS : recrée et écrase si nécessaire
- OPEN_EXISTING : ouvre seulement si le fichier existe
- OPEN_ALWAYS : ouvre s’il existe, sinon crée
- TRUNCATE_EXISTING : vide un fichier existant
- FILE_ATTRIBUTE_NORMAL
- FILE_ATTRIBUTE_HIDDEN
- FILE_ATTRIBUTE_READONLY
- FILE_FLAG_OVERLAPPED
- FILE_FLAG_SEQUENTIAL_SCAN
- FILE_FLAG_RANDOM_ACCESS
- FILE_FLAG_WRITE_THROUGH
- FILE_FLAG_NO_BUFFERING
- FILE_FLAG_BACKUP_SEMANTICS
- FILE_FLAG_DELETE_ON_CLOSE
Les droits, le partage et les erreurs d’ouverture
Beaucoup d’erreurs lors d’un CreateFile ne viennent pas d’un “fichier introuvable”, mais d’un conflit de droits ou de partage. Si un autre processus a ouvert le fichier sans permettre certains partages, ton ouverture peut échouer.
Il faut donc distinguer deux choses :
- les accès que ton programme demande
- les accès qu’il autorise aux autres pendant que le handle reste ouvert
C’est une distinction fondamentale pour comprendre les erreurs de type sharing violation.
Lire et écrire : ReadFile et WriteFile
Une fois le handle obtenu, les fonctions de base sont ReadFile et WriteFile.
Exemple de lecture :
Code: Select all
DWORD bytesRead = 0;
char buffer[128] = { 0 };
BOOL ok = ReadFile(
hFile,
buffer,
sizeof(buffer) - 1,
&bytesRead,
NULL
);
if (!ok)
{
DWORD err = GetLastError();
return 1;
}
Code: Select all
DWORD bytesWritten = 0;
const char* msg = "Hello from Win32\r\n";
BOOL ok = WriteFile(
hFile,
msg,
(DWORD)strlen(msg),
&bytesWritten,
NULL
);
if (!ok)
{
DWORD err = GetLastError();
return 1;
}
Premièrement, il faut toujours vérifier le retour BOOL. Une opération d’I/O peut échouer, etre partielle, ou demander un traitement particulier.
Deuxièmement, bytesRead et bytesWritten indiquent ce qui a réellement été traité. Il ne faut jamais supposer qu’une lecture ou une écriture a concerné exactement la taille demandée sans vérifier.
Le pointeur de fichier
Lorsqu’on travaille avec un fichier classique ouvert de manière synchrone, le handle possède une position courante, souvent appelée file pointer. Chaque lecture ou écriture avance cette position.
Pour la déplacer explicitement, on utilise en général SetFilePointerEx :
Code: Select all
LARGE_INTEGER off;
off.QuadPart = 0;
if (!SetFilePointerEx(hFile, off, NULL, FILE_BEGIN))
return 1;
- FILE_BEGIN
- FILE_CURRENT
- FILE_END
Code: Select all
LARGE_INTEGER size;
if (!GetFileSizeEx(hFile, &size))
return 1;
Tronquer, étendre et positionner correctement un fichier
Autour du pointeur de fichier, quelques API sont bonnes à connaitre :
- SetEndOfFile, pour tronquer ou étendre le fichier à la position courante
- SetFilePointerEx, pour se placer précisément
- GetFileSizeEx, pour connaitre la taille totale
Code: Select all
LARGE_INTEGER off;
off.QuadPart = 100;
if (!SetFilePointerEx(hFile, off, NULL, FILE_BEGIN))
return 1;
if (!SetEndOfFile(hFile))
return 1;
I/O synchrones : le modèle par défaut
Par défaut, ReadFile et WriteFile sont synchrones. Cela signifie que le thread appelant reste bloqué jusqu’à ce que l’opération soit terminée, ou qu’elle échoue.
Ce modèle est simple à comprendre, mais il a une conséquence immédiate : si tu effectues une I/O lente dans le mauvais thread, par exemple le thread principal d’une interface ou d’une boucle réactive, ton programme peut se figer.
C’est pour cela qu’en pratique, il faut toujours réfléchir à la question suivante : est-ce que je peux me permettre de bloquer ce thread ?
I/O overlapped : le modèle asynchrone Win32
Pour ne pas bloquer le thread appelant, Windows propose les I/O asynchrones dites overlapped. Le principe repose sur deux éléments :
- ouvrir l’objet avec FILE_FLAG_OVERLAPPED
- fournir une structure OVERLAPPED
Code: Select all
HANDLE hFile2 = CreateFileW(
L"test.txt",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL
);
if (hFile2 == INVALID_HANDLE_VALUE)
return 1;
Code: Select all
OVERLAPPED ov = { 0 };
BOOL ok = WriteFile(
hFile2,
msg,
(DWORD)strlen(msg),
NULL,
&ov
);
if (!ok)
{
DWORD err = GetLastError();
if (err != ERROR_IO_PENDING)
return 1;
}
Le résultat se récupère ensuite via GetOverlappedResult, ou bien en attendant un event associé à la structure OVERLAPPED.
La structure OVERLAPPED
OVERLAPPED est une structure qu’il faut connaitre dès qu’on fait des I/O asynchrones.
Les champs les plus importants sont :
- Offset
- OffsetHigh
- hEvent
Exemple :
Code: Select all
OVERLAPPED ov = { 0 };
ov.Offset = 0;
ov.OffsetHigh = 0;
ov.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
Code: Select all
DWORD transferred = 0;
if (!GetOverlappedResult(hFile2, &ov, &transferred, TRUE))
return 1;
Annuler une I/O asynchrone
Quand on travaille avec des I/O asynchrones, il faut aussi connaitre les API d’annulation :
- CancelIo
- CancelIoEx
- CancelSynchronousIo
Attendre une I/O et raisonner en termes d’événements
Une I/O overlapped s’intègre naturellement au modèle de synchronisation Win32. Si ov.hEvent est renseigné, on peut attendre la fin de l’opération avec WaitForSingleObject ou WaitForMultipleObjects.
C’est un point important, parce qu’il relie directement le modèle I/O au modèle de synchronisation vu dans le cours précédent.
Verrouillage de zones dans un fichier
Il existe aussi des API pour verrouiller une région d’un fichier :
- LockFile
- LockFileEx
- UnlockFile
- UnlockFileEx
Le cache système et FlushFileBuffers
Sous Windows, les I/O sur fichiers passent souvent par le cache système. Cela veut dire qu’un WriteFile réussi ne signifie pas forcément que les données ont été physiquement écrites sur le disque à cet instant précis.
Pour forcer le flush des buffers du fichier, on utilise :
Code: Select all
if (!FlushFileBuffers(hFile))
return 1;
Les flags CreateFile comme FILE_FLAG_WRITE_THROUGH et FILE_FLAG_NO_BUFFERING influencent aussi fortement le comportement, mais ils doivent etre utilisés avec discernement.
Le mode non buffered et ses pièges
FILE_FLAG_NO_BUFFERING permet de contourner une partie du cache système, mais il impose des contraintes importantes sur les tailles, les alignements et les offsets. Ce n’est pas un mode à utiliser “pour aller plus vite” sans comprendre précisément les règles du système de stockage.
De meme, FILE_FLAG_WRITE_THROUGH peut changer le compromis entre performance et persistance. Ce sont des options de niveau plus avancé, utiles, mais à manier proprement.
Lister des fichiers et parcourir un répertoire
Pour lister les entrées d’un dossier, on utilise généralement FindFirstFileW et FindNextFileW, avec la structure WIN32_FIND_DATAW.
Exemple :
Code: Select all
WIN32_FIND_DATAW data;
HANDLE hFind = FindFirstFileW(L"*.*", &data);
if (hFind != INVALID_HANDLE_VALUE)
{
do
{
// data.cFileName
} while (FindNextFileW(hFind, &data));
FindClose(hFind);
}
- cFileName
- dwFileAttributes
- nFileSizeHigh
- nFileSizeLow
- ftCreationTime
- ftLastAccessTime
- ftLastWriteTime
Autour des répertoires et fichiers, il faut aussi connaitre :
- CreateDirectoryW
- CreateDirectoryExW
- RemoveDirectoryW
- DeleteFileW
- MoveFileW
- MoveFileExW
- CopyFileW
- CopyFileExW
- ReplaceFileW
Le sujet des chemins sous Windows est plus subtil qu’il n’en a l’air. Il faut connaitre au moins :
- MAX_PATH et ses limites historiques
- le préfixe \\?\ pour les chemins étendus
- la différence entre chemins relatifs et absolus
- les problèmes de normalisation
- les chemins UNC pour le réseau
- GetFullPathNameW
- GetFinalPathNameByHandleW
- GetTempPathW
- GetTempFileNameW
- SetCurrentDirectoryW
- GetCurrentDirectoryW
Attributs et informations générales
Pour récupérer les attributs d’un chemin :
Code: Select all
DWORD attr = GetFileAttributesW(L"test.txt");
if (attr == INVALID_FILE_ATTRIBUTES)
return 1;
- SetFileAttributesW
- GetFileAttributesW
Code: Select all
BY_HANDLE_FILE_INFORMATION info;
if (!GetFileInformationByHandle(hFile, &info))
return 1;
- dwFileAttributes
- ftCreationTime
- ftLastAccessTime
- ftLastWriteTime
- dwVolumeSerialNumber
- nFileSizeHigh
- nFileSizeLow
- nNumberOfLinks
- nFileIndexHigh
- nFileIndexLow
GetFileInformationByHandleEx et les structures modernes
Au-delà de BY_HANDLE_FILE_INFORMATION, il est important de connaitre GetFileInformationByHandleEx, qui permet d’obtenir des informations plus ciblées selon une classe demandée.
Parmi les structures utiles de cette famille :
- FILE_BASIC_INFO
- FILE_STANDARD_INFO
- FILE_NAME_INFO
- FILE_ATTRIBUTE_TAG_INFO
- FILE_ID_INFO
- FILE_COMPRESSION_INFO
- FILE_ALIGNMENT_INFO
- FILE_STORAGE_INFO
- CreationTime
- LastAccessTime
- LastWriteTime
- ChangeTime
- FileAttributes
- AllocationSize
- EndOfFile
- NumberOfLinks
- DeletePending
- Directory
Timestamps et structure FILETIME
Dès qu’on manipule les dates de fichiers, il faut connaitre FILETIME. Cette structure encode un temps sous forme de deux DWORD représentant un entier 64 bits comptant des intervalles de 100 nanosecondes depuis une époque définie par Windows.
On la rencontre dans beaucoup de structures, par exemple :
- WIN32_FIND_DATAW
- BY_HANDLE_FILE_INFORMATION
- FILE_BASIC_INFO
- FileTimeToSystemTime
- SystemTimeToFileTime
- CompareFileTime
- GetSystemTimeAsFileTime
- SetFileTime
Tous les HANDLE ne représentent pas la meme chose. Il peut donc etre utile de connaitre :
- GetFileType
Les valeurs classiques sont :
- FILE_TYPE_DISK
- FILE_TYPE_CHAR
- FILE_TYPE_PIPE
- FILE_TYPE_UNKNOWN
DeviceIoControl : au-delà des simples fichiers
Le modèle I/O de Windows ne se limite pas aux fichiers de données. Beaucoup d’objets du système sont exposés via des handles et des requetes d’I/O plus générales. C’est là qu’intervient DeviceIoControl.
Exemple schématique :
Code: Select all
DWORD returned = 0;
BYTE outBuffer[256] = { 0 };
BOOL ok = DeviceIoControl(
hDevice,
ioControlCode,
NULL,
0,
outBuffer,
sizeof(outBuffer),
&returned,
NULL
);
- de communication avec des drivers
- d’accès à des périphériques
- de certaines opérations spécialisées sur volumes ou systèmes de fichiers
Notifications de changement dans les répertoires
Pour surveiller les changements dans un répertoire, une API très utile est ReadDirectoryChangesW.
Elle permet de recevoir des notifications quand des fichiers sont ajoutés, supprimés, renommés ou modifiés dans un dossier donné.
La structure centrale ici est FILE_NOTIFY_INFORMATION.
On la rencontre quand on veut construire :
- un watcher de dossier
- un outil de synchronisation
- un indexeur
- une surveillance temps réel
Même si ce cours est centré sur le filesystem, il faut garder à l’esprit que ReadFile et WriteFile ne servent pas uniquement aux fichiers disque. On les utilise aussi avec :
- des pipes anonymes
- des named pipes
- certaines consoles
- divers objets exposant un modèle d’I/O compatible
Lecture et écriture positionnées
Avec les I/O overlapped, Offset et OffsetHigh permettent d’effectuer des opérations à une position précise sans dépendre du pointeur de fichier partagé par le handle dans le modèle synchrone classique. C’est un point important pour le multithreading et pour les I/O parallèles.
Autrement dit, la structure OVERLAPPED peut aussi servir à exprimer “où” lire ou écrire.
Les structures qu’il faut connaitre
Pour avoir déjà une base propre, il faut au minimum connaitre les structures suivantes :
- OVERLAPPED
- WIN32_FIND_DATAW
- BY_HANDLE_FILE_INFORMATION
- FILETIME
- FILE_BASIC_INFO
- FILE_STANDARD_INFO
- FILE_NAME_INFO
- FILE_ID_INFO
- FILE_NOTIFY_INFORMATION
- SECURITY_ATTRIBUTES
Les API à connaitre absolument
Pour l’ouverture, la fermeture et les opérations de base :
- CreateFileW
- ReadFile
- WriteFile
- CloseHandle
- FlushFileBuffers
- GetLastError
- SetFilePointerEx
- GetFileSizeEx
- SetEndOfFile
- GetOverlappedResult
- CancelIo
- CancelIoEx
- CancelSynchronousIo
- FindFirstFileW
- FindNextFileW
- FindClose
- CreateDirectoryW
- RemoveDirectoryW
- DeleteFileW
- MoveFileExW
- CopyFileExW
- ReplaceFileW
- GetFileAttributesW
- SetFileAttributesW
- GetFileInformationByHandle
- GetFileInformationByHandleEx
- SetFileTime
- GetFinalPathNameByHandleW
- LockFileEx
- UnlockFileEx
- ReadDirectoryChangesW
- DeviceIoControl
- GetFileType
Comme souvent en Win32, les erreurs les plus pénibles ne viennent pas d’une API exotique, mais d’un mauvais usage des bases.
Parmi les fautes très fréquentes :
- oublier CloseHandle
- ne pas vérifier les codes de retour
- ignorer GetLastError quand une API échoue
- bloquer un thread important avec des I/O synchrones
- supposer que bytesRead ou bytesWritten valent toujours la taille demandée
- mal gérer les chemins, les partages ou les modes d’ouverture
- confondre objet fichier, handle de fichier et chemin
- utiliser FILE_FLAG_NO_BUFFERING sans respecter les contraintes d’alignement
Le système de fichiers et les I/O sous Windows reposent sur quelques idées qu’il faut vraiment maitriser : presque tout passe par des handles, CreateFile est l’API d’entrée majeure, ReadFile et WriteFile forment la base des opérations synchrones, OVERLAPPED ouvre la porte aux I/O asynchrones, et tout le reste gravite autour de la bonne gestion du modèle de partage, des chemins, des métadonnées et du cycle de vie des ressources.
Si tu maitrises déjà correctement :
- CreateFileW
- ReadFile et WriteFile
- SetFilePointerEx et GetFileSizeEx
- OVERLAPPED et GetOverlappedResult
- WIN32_FIND_DATAW et BY_HANDLE_FILE_INFORMATION
- FlushFileBuffers, LockFileEx et ReadDirectoryChangesW
- GetFileInformationByHandleEx
- DeviceIoControl
Et surtout, tu commences à voir que le “filesystem” sous Windows n’est pas juste un ensemble de fonctions pour lire des octets, mais un vrai modèle d’objets, d’handles, d’opérations synchrones ou asynchrones, et d’abstractions noyau que l’on retrouve partout dans l’architecture système.
A bientot pour le prochain cours
