aujourd’hui on va voir plusieurs notions fondamentales du développement Win32, et surtout des notions que beaucoup de gens utilisent sans réellement les comprendre : les handles, l’héritage de handles, les types Win32 et la question Unicode / ANSI.
C’est un bloc de base, mais il ne faut surtout pas le prendre à la légère. En pratique, une énorme partie de l’API Win32 repose dessus. Si on comprend mal ce qu’est un HANDLE, comment il vit, comment il se transmet, ce que veut dire LPVOID, pourquoi il existe des versions A et W, ou pourquoi BOOL n’est pas exactement bool, alors on code en Win32 un peu à l’aveugle, avec beaucoup de confusions, et on finit vite par faire des erreurs pénibles.
Pourquoi ce sujet est fondamental
Comprendre correctement ces notions permet de mieux voir :
- ce qu’une API Win32 te retourne réellement
- pourquoi un HANDLE n’est pas une adresse mémoire
- comment Windows contrôle l’accès aux objets noyau
- comment un handle peut etre hérité ou dupliqué
- pourquoi il existe autant de typedefs dans l’API
- ce que veulent dire des noms comme DWORD, LPVOID, LPCWSTR ou HANDLE
- pourquoi les API A et W coexistent encore
- pourquoi il vaut mieux raisonner directement en Unicode dans le code moderne
Ce qu’est réellement un HANDLE
Un HANDLE est une référence opaque vers un objet géré par Windows. Le mot important ici, c’est opaque. Cela signifie que le programme utilisateur ne doit pas essayer de donner lui-meme une interprétation interne à cette valeur.
Un handle :
- n’est pas un pointeur utilisable comme une adresse mémoire ordinaire
- n’est pas censé etre déréférencé
- n’a de sens que dans le contexte du processus auquel il appartient
- sert de clé d’accès vers une ressource ou un objet noyau
- des fichiers
- des processus
- des threads
- des events
- des mutex
- des sémaphores
- des jobs
- des tokens
- des pipes
- des objets de mapping
Pourquoi Windows utilise des handles
Windows sépare fortement le monde user-mode et le monde kernel-mode. Le noyau ne remet pas simplement au programme un pointeur brut vers ses structures internes. Ce serait dangereux, instable, et totalement incompatible avec le modèle de sécurité du système.
Le schéma général est plutot celui-ci :
- le noyau crée ou référence l’objet réel
- il inscrit une entrée correspondante dans la table de handles du processus
- le processus reçoit une valeur de type HANDLE qui permet de réutiliser cette entrée
C’est pour cela qu’un handle n’a pas de sens en dehors du processus qui le possède, sauf s’il est transmis ou dupliqué explicitement.
Table de handles et portée du handle
Chaque processus possède sa propre table de handles. C’est une idée très importante.
Cela signifie notamment :
- qu’un meme nombre de handle peut exister dans deux processus différents sans désigner le meme objet
- qu’un processus ne peut pas réutiliser directement un handle appartenant à un autre
- qu’un handle n’est valable que dans l’espace logique du processus qui l’a obtenu, sauf mécanisme spécial
Cycle de vie d’un handle
Le cycle de vie d’un handle suit généralement une logique simple :
- une API crée ou ouvre un objet
- elle retourne un handle
- le programme utilise ce handle avec d’autres API
- le programme ferme le handle quand il n’en a plus besoin
Code: Select all
HANDLE hFile = CreateFileW(
L"test.txt",
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hFile == INVALID_HANDLE_VALUE)
return 1;
CloseHandle(hFile);
CloseHandle n’est pas universel
Une erreur classique consiste à croire que tout ce qui “ressemble à un handle” se ferme avec CloseHandle. Ce n’est pas vrai.
Par exemple :
- un HANDLE classique de fichier, processus, thread, event, job, token, mapping se ferme avec CloseHandle
- un SC_HANDLE du SCM se ferme avec CloseServiceHandle
- un HKEY de registre se ferme avec RegCloseKey
- un SOCKET se ferme avec closesocket
- un handle de recherche FindFirstFile se ferme avec FindClose
- une bibliothèque chargée via LoadLibrary se libère avec FreeLibrary
Pseudo-handles
Certaines fonctions ne renvoient pas de vrais handles au sens classique, mais des pseudo-handles.
Les exemples les plus connus sont :
- GetCurrentProcess
- GetCurrentThread
Code: Select all
HANDLE hProc = GetCurrentProcess();
HANDLE hThread = GetCurrentThread();
Il faut retenir plusieurs choses :
- un pseudo-handle ne doit pas etre fermé avec CloseHandle
- il est valable dans le contexte du processus courant
- si on veut un vrai handle duplicable ou transmissible, il faut le dupliquer ou ouvrir explicitement l’objet
- GetCurrentProcessId
- GetCurrentThreadId
- DuplicateHandle
Toutes les API Win32 ne signalent pas l’échec de la meme manière.
Certaines renvoient NULL en cas d’échec. D’autres renvoient INVALID_HANDLE_VALUE.
Exemples :
- CreateFileW échoue avec INVALID_HANDLE_VALUE
- CreateEventW échoue avec NULL
- CreateMutexW échoue avec NULL
- OpenProcess échoue avec NULL
Exemple correct avec CreateFileW :
Code: Select all
HANDLE hFile = CreateFileW(...);
if (hFile == INVALID_HANDLE_VALUE)
{
DWORD err = GetLastError();
return 1;
}
Code: Select all
HANDLE hEvent = CreateEventW(NULL, TRUE, FALSE, NULL);
if (!hEvent)
{
DWORD err = GetLastError();
return 1;
}
Par défaut, un processus enfant n’hérite pas automatiquement de tous les handles du parent. L’héritage est explicite, ce qui est une bonne chose du point de vue sécurité et architecture.
Pour qu’un handle puisse etre hérité, il faut généralement combiner deux conditions :
- le handle doit etre marqué comme héritable
- CreateProcessW doit etre appelé avec bInheritHandles à TRUE
SECURITY_ATTRIBUTES et création d’un handle héritable
La structure la plus classique pour rendre un handle héritable à la création est :
- SECURITY_ATTRIBUTES
Code: Select all
SECURITY_ATTRIBUTES sa = { 0 };
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
Avec un pipe :
Code: Select all
HANDLE hRead = NULL;
HANDLE hWrite = NULL;
if (!CreatePipe(&hRead, &hWrite, &sa, 0))
return 1;
CreateProcessW et bInheritHandles
Lors du lancement du processus enfant, le paramètre concerné est :
- bInheritHandles
Code: Select all
BOOL ok = CreateProcessW(
NULL,
cmdLine,
NULL,
NULL,
TRUE,
0,
NULL,
NULL,
&si,
&pi
);
Mais il faut bien comprendre ce que cela signifie :
- ce n’est pas “tout devient automatiquement transmis”
- seuls les handles héritables sont concernés
- le processus enfant reçoit ses propres entrées de table de handles vers les objets hérités
Il existe aussi une API très importante pour modifier certains attributs d’un handle existant :
- SetHandleInformation
- HANDLE_FLAG_INHERIT
Code: Select all
if (!SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0))
return 1;
API voisine à connaitre :
- GetHandleInformation
Quand on veut transmettre explicitement un handle à un autre processus, ou obtenir un vrai handle à partir d’un pseudo-handle, l’API centrale est :
- DuplicateHandle
Code: Select all
HANDLE hNewHandle = NULL;
if (!DuplicateHandle(
hSrcProcess,
hHandle,
hDstProcess,
&hNewHandle,
0,
FALSE,
DUPLICATE_SAME_ACCESS))
{
return 1;
}
- transmettre un handle à un autre processus
- obtenir un vrai handle à partir d’un pseudo-handle
- contrôler les droits du handle dupliqué
- fermer la source au moment de la duplication si nécessaire
- DUPLICATE_SAME_ACCESS
- DUPLICATE_CLOSE_SOURCE
Types Win32 : pourquoi autant de noms
Beaucoup de développeurs débutants trouvent les types Win32 “verbeux” ou “anciens” : DWORD, LPVOID, LPCWSTR, ULONG_PTR, HANDLE, BOOL, etc. En réalité, ces typedefs ne sont pas là par hasard.
Ils servent à plusieurs choses :
- exprimer l’intention
- masquer certains détails historiques ou d’architecture
- améliorer la lisibilité des signatures
- permettre une meilleure portabilité entre variantes de plateforme
- indiquer des conventions d’usage sur les pointeurs, la constance, les tailles ou les handles
Quelques types entiers fondamentaux
Parmi les types entiers Win32 les plus importants :
- BYTE : 8 bits non signé
- WORD : 16 bits non signé
- DWORD : 32 bits non signé
- LONG : 32 bits signé
- ULONG : 32 bits non signé
- ULONGLONG : 64 bits non signé
- LONGLONG : 64 bits signé
- UINT : entier non signé de style C
- INT : entier signé de style C
Code: Select all
DWORD pid = GetCurrentProcessId();
- SIZE_T
- SSIZE_T
- ULONG_PTR
- DWORD_PTR
- UINT_PTR
- LONG_PTR
- INT_PTR
SIZE_T, ULONG_PTR et les types dépendants de l’architecture
En code 32 bits contre 64 bits, ces types prennent toute leur importance.
Exemples :
- SIZE_T représente une taille mémoire adaptée à l’architecture
- ULONG_PTR représente un entier non signé suffisamment grand pour contenir un pointeur
- LONG_PTR représente la version signée
Code: Select all
LPVOID mem = VirtualAlloc(
NULL,
(SIZE_T)4096,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE
);
Types pointeurs Win32
Le style Win32 classique contient beaucoup de types pointeurs préfixés par LP ou P.
Exemples importants :
- LPVOID : pointeur générique modifiable
- LPCVOID : pointeur générique constant
- LPBYTE : pointeur vers BYTE
- PBYTE : pointeur vers BYTE
- LPDWORD : pointeur vers DWORD
- PDWORD : pointeur vers DWORD
- LPWSTR : pointeur vers chaine wide modifiable
- LPCWSTR : pointeur vers chaine wide constante
- LPSTR : pointeur vers chaine ANSI modifiable
- LPCSTR : pointeur vers chaine ANSI constante
Code: Select all
LPVOID buffer = VirtualAlloc(...);
Const, lecture des signatures et intention
Lire correctement une signature Win32, c’est aussi savoir repérer ce qui est modifiable ou non.
Par exemple :
- LPCWSTR = pointeur constant vers wide string
- LPWSTR = pointeur wide modifiable
- LPCVOID = pointeur générique constant
- LPVOID = pointeur générique modifiable
Types handles spécialisés
Beaucoup de types Win32 “spécialisés” sont en réalité des handles ou quasi-handles vers une catégorie d’objet précise.
Exemples importants :
- HANDLE
- HMODULE
- HINSTANCE
- HWND
- HDC
- HKEY
- SC_HANDLE
- SOCKET
Il faut résister à une mauvaise habitude : croire que parce que “ça ressemble à une valeur opaque”, tout est interchangeable. Ce n’est pas le cas.
Par exemple :
- un HWND n’est pas un HANDLE de fichier
- un HKEY ne se ferme pas comme un HANDLE noyau
- un SOCKET ne se ferme pas avec CloseHandle
- un HMODULE ne se manipule pas comme un HWND
BOOL contre bool
En Win32, BOOL n’est pas le bool natif du C++.
BOOL est historiquement un entier, généralement de 32 bits, avec les constantes :
- TRUE
- FALSE
Code: Select all
BOOL ok = ReadFile(...);
if (!ok)
return 1;
Il faut donc éviter les raisonnements trop automatiques. BOOL n’est pas “cassé”, c’est juste un type historique de l’API.
Autres types très fréquents à connaitre
Parmi les types Win32 que l’on croise partout :
- CHAR
- WCHAR
- TCHAR
- ATOM
- HRESULT
- NTSTATUS
- LCID
- LANGID
- FILETIME
- SYSTEMTIME
- LARGE_INTEGER
- ULARGE_INTEGER
Conventions d’appel : WINAPI, CALLBACK, APIENTRY
En Win32, les types ne sont pas la seule chose à regarder. Les conventions d’appel comptent aussi.
Les macros les plus fréquentes :
- WINAPI
- CALLBACK
- APIENTRY
Exemple :
Code: Select all
DWORD WINAPI ThreadFunc(LPVOID param);
Unicode contre ANSI : le vrai modèle Windows moderne
Windows moderne est fondamentalement orienté Unicode. Cela veut dire que pour les chaines système, la bonne version d’API à privilégier est presque toujours la version wide, suffixée W.
Exemples :
- CreateFileA : version ANSI
- CreateFileW : version Unicode
- MessageBoxA : version ANSI
- MessageBoxW : version Unicode
- CreateProcessA : version ANSI
- CreateProcessW : version Unicode
Pourquoi les versions A existent encore
Les versions ANSI existent pour compatibilité historique avec du vieux code ou certains scénarios anciens. Elles ne représentent pas le meilleur modèle mental pour le développement moderne.
Le problème n’est pas seulement philosophique. Utiliser les versions A peut compliquer :
- la gestion des caractères non ASCII
- les chemins de fichiers contenant des caractères internationaux
- la cohérence avec le reste de l’écosystème Windows
- les conversions de code pages
- privilégier les API W
- raisonner en wchar_t et en chaines wide quand on parle aux API Windows
Win32 fournit aussi des macros génériques. Par exemple, selon que UNICODE soit défini ou non :
- CreateFile devient CreateFileW
- ou CreateFile devient CreateFileA
- TCHAR devient wchar_t si UNICODE est défini
- TCHAR devient char sinon
- LPTSTR
- LPCTSTR
- TEXT("abc")
- _T("abc")
Bonnes pratiques Unicode en Win32 moderne
Une approche propre et moderne consiste généralement à :
- utiliser directement les fonctions W
- utiliser wchar_t pour les chaines système
- utiliser des littéraux L"..."
- éviter de dépendre du vieux modèle TCHAR si ce n’est pas nécessaire
- convertir explicitement lorsqu’on doit passer entre UTF-8 et UTF-16
Code: Select all
HANDLE hFile = CreateFileW(
L"test.txt",
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
Quand il faut convertir entre UTF-8, ANSI et UTF-16, deux API sont particulièrement importantes :
- MultiByteToWideChar
- WideCharToMultiByte
- de convertir une chaine UTF-8 vers wchar_t
- de convertir une chaine wide vers un encodage multi-octets
Exemples de types et structures qu’il faut reconnaitre
Pour avoir une bonne base, il faut reconnaitre rapidement au moins :
- HANDLE
- DWORD
- WORD
- BYTE
- BOOL
- LPVOID
- LPCVOID
- LPWSTR
- LPCWSTR
- LPSTR
- LPCSTR
- SIZE_T
- ULONG_PTR
- LONG_PTR
- LARGE_INTEGER
- FILETIME
- SYSTEMTIME
- SECURITY_ATTRIBUTES
- STARTUPINFOW
- PROCESS_INFORMATION
Les API à connaitre autour des handles et de l’héritage
Pour la gestion générale des handles :
- CloseHandle
- GetHandleInformation
- SetHandleInformation
- DuplicateHandle
- GetCurrentProcess
- GetCurrentThread
- CreateProcessW
- CreatePipe
- CreateEventW
- CreateMutexW
- OpenProcess
- MultiByteToWideChar
- WideCharToMultiByte
- l’ensemble des API A / W comme CreateFileA / CreateFileW
Comme souvent en Win32, les plus gros problèmes viennent d’une mauvaise compréhension des bases.
Parmi les erreurs fréquentes :
- confondre handle et pointeur
- déréférencer ou caster un handle comme si c’était une adresse utile
- fermer un pseudo-handle
- oublier de fermer un vrai handle
- tester NULL alors que l’API échoue avec INVALID_HANDLE_VALUE
- croire qu’un handle a un sens universel dans tous les processus
- mélanger les familles de fermeture
- mal gérer l’héritage de handles avec CreateProcessW
- mélanger versions A et W dans un meme code sans logique claire
- utiliser char partout alors que l’API Windows moderne attend du wide
Un handle est une référence opaque vers un objet géré par Windows. Ce n’est pas un pointeur, ni une adresse à déréférencer, ni un identifiant global absolu. Il n’a de sens que dans le processus qui le possède, sauf transmission ou duplication explicite.
Les idées essentielles à garder sont les suivantes :
- un vrai handle doit en général etre fermé correctement
- un pseudo-handle ne doit pas etre fermé
- l’héritage de handles est explicite
- DuplicateHandle sert à transmettre ou reproduire un handle dans un autre contexte
- les types Win32 expriment une intention réelle
- les types dépendants de l’architecture comme SIZE_T ou ULONG_PTR sont importants
- Windows moderne doit etre abordé en Unicode, donc plutot avec les API W
- les versions A existent surtout pour compatibilité historique
Comprendre les handles, l’héritage, les types Win32 et Unicode, c’est sortir d’un Win32 “copié-collé” pour entrer dans un Win32 réellement compris. C’est aussi ce qui permet de lire les signatures système avec beaucoup plus de clarté, de choisir les bons types, d’éviter des bugs de fermeture ou d’héritage, et de construire un code plus propre, plus lisible, et plus robuste.
Si tu maitrises déjà correctement :
- HANDLE, INVALID_HANDLE_VALUE et CloseHandle
- la différence entre vrai handle et pseudo-handle
- SECURITY_ATTRIBUTES, SetHandleInformation et DuplicateHandle
- les types comme DWORD, LPVOID, SIZE_T, LPCWSTR
- BOOL contre bool
- CreateFileW, CreateProcessW et les API W en général
- MultiByteToWideChar et WideCharToMultiByte
A bientot pour le prochain cours
