Handles, héritage, types Win32 et Unicode

Développement système natif en c/c++ avec win32 ...

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 46
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Handles, héritage, types Win32 et Unicode

Post by Hydraxx »

Salut, :D

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
Autrement dit, ce cours touche aux fondations memes du style Win32.

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
Il permet par exemple de manipuler :
  • des fichiers
  • des processus
  • des threads
  • des events
  • des mutex
  • des sémaphores
  • des jobs
  • des tokens
  • des pipes
  • des objets de mapping
Le point à retenir est simple : l’objet réel vit coté système, et l’application reçoit une référence contrôlée vers cet objet.

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
Le handle devient donc une sorte de clé d’accès contrôlée. Quand une autre API reçoit ce handle, Windows vérifie qu’il correspond bien à une entrée valide du processus appelant, puis retrouve l’objet cible et applique la logique de droits ou d’opération demandé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
Autrement dit, le HANDLE n’est pas l’objet, ni un identifiant global absolu. C’est une référence valable dans un contexte de processus donné.

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
Exemple classique :

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);
Si on oublie la fermeture, on fuit une ressource. Cela ne produit pas toujours un crash immédiat, mais cela reste une fuite, et dans un programme long ou un service, cela finit par devenir un vrai problème.

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
Donc il ne faut jamais raisonner uniquement sur le nom du type, mais sur la famille d’API à laquelle l’objet appartient.

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
Exemple :

Code: Select all

HANDLE hProc = GetCurrentProcess();
HANDLE hThread = GetCurrentThread();
Ces valeurs représentent respectivement le processus courant et le thread courant, mais sous une forme spéciale fournie par le système.

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
API utiles autour de ça :
  • GetCurrentProcessId
  • GetCurrentThreadId
  • DuplicateHandle
INVALID_HANDLE_VALUE et NULL : attention à la différence

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
Il faut donc absolument connaitre le contrat précis de l’API utilisée, sinon on peut tester la mauvaise valeur et introduire un bug subtil.

Exemple correct avec CreateFileW :

Code: Select all

HANDLE hFile = CreateFileW(...);
if (hFile == INVALID_HANDLE_VALUE)
{
    DWORD err = GetLastError();
    return 1;
}
Exemple correct avec CreateEventW :

Code: Select all

HANDLE hEvent = CreateEventW(NULL, TRUE, FALSE, NULL);
if (!hEvent)
{
    DWORD err = GetLastError();
    return 1;
}
Héritage de handles

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
C’est un sujet très important dans les scénarios de redirection standard, de pipes, de services, ou d’IPC local.

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
Exemple :

Code: Select all

SECURITY_ATTRIBUTES sa = { 0 };
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
Cette structure peut ensuite etre passée à des API de création d’objet, par exemple pour certains pipes, fichiers, events, mutex, etc., selon ce que l’API accepte.

Avec un pipe :

Code: Select all

HANDLE hRead = NULL;
HANDLE hWrite = NULL;

if (!CreatePipe(&hRead, &hWrite, &sa, 0))
    return 1;
Ici, les handles créés peuvent etre hérités par un enfant si le CreateProcessW est configuré en conséquence.

CreateProcessW et bInheritHandles

Lors du lancement du processus enfant, le paramètre concerné est :
  • bInheritHandles
Exemple :

Code: Select all

BOOL ok = CreateProcessW(
    NULL,
    cmdLine,
    NULL,
    NULL,
    TRUE,
    0,
    NULL,
    NULL,
    &si,
    &pi
);
Le TRUE ici autorise l’héritage des handles marqués comme héritables.

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
Contrôler l’héritage après création : SetHandleInformation

Il existe aussi une API très importante pour modifier certains attributs d’un handle existant :
  • SetHandleInformation
Le flag le plus connu dans ce contexte est :
  • HANDLE_FLAG_INHERIT
Exemple :

Code: Select all

if (!SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0))
    return 1;
Cela permet par exemple de créer deux handles héritables, puis de retirer l’héritage sur l’un des deux avant le CreateProcess. C’est très pratique pour les scénarios de redirection précise.

API voisine à connaitre :
  • GetHandleInformation
Dupliquer un handle

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
Exemple :

Code: Select all

HANDLE hNewHandle = NULL;

if (!DuplicateHandle(
    hSrcProcess,
    hHandle,
    hDstProcess,
    &hNewHandle,
    0,
    FALSE,
    DUPLICATE_SAME_ACCESS))
{
    return 1;
}
Cette API est très importante. Elle sert notamment à :
  • 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
Flags importants :
  • DUPLICATE_SAME_ACCESS
  • DUPLICATE_CLOSE_SOURCE
C’est une brique de base des mécanismes IPC ou de certains superviseurs.

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
Autrement dit, ces types ne sont pas juste des décorations stylistiques.

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
Exemple :

Code: Select all

DWORD pid = GetCurrentProcessId();
Il faut aussi connaitre les types entiers dépendants de l’architecture, très importants en code bas niveau moderne :
  • SIZE_T
  • SSIZE_T
  • ULONG_PTR
  • DWORD_PTR
  • UINT_PTR
  • LONG_PTR
  • INT_PTR
Ces types existent précisément pour représenter des tailles ou valeurs liées à la largeur des pointeurs sur la plateforme.

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
C’est pour cela qu’une API comme VirtualAlloc utilise SIZE_T pour la taille :

Code: Select all

LPVOID mem = VirtualAlloc(
    NULL,
    (SIZE_T)4096,
    MEM_RESERVE | MEM_COMMIT,
    PAGE_READWRITE
);
Utiliser un DWORD à la place d’un SIZE_T dans des scénarios 64 bits peut parfois etre conceptuellement faux, meme si ça “semble marcher” sur de petits cas.

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
Exemple :

Code: Select all

LPVOID buffer = VirtualAlloc(...);
Historiquement, LP voulait dire Long Pointer, mais aujourd’hui il faut surtout le lire comme une convention de nommage héritée de l’API Win32.

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
Rien que ce détail change la manière d’appeler l’API et d’interpréter le contrat de la fonction.

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
Tous ne sont pas strictement identiques du point de vue implémentation ou fermeture, mais ils servent tous à exprimer une catégorie logique différente.

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
Les types spécialisés servent précisément à éviter une partie de ces confusions.

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
Exemple :

Code: Select all

BOOL ok = ReadFile(...);
if (!ok)
    return 1;
En C++, bool est un vrai type booléen du langage. Les deux peuvent coexister, mais dans les signatures Win32 classiques, c’est souvent BOOL qui apparait.

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
Tous ne relèvent pas exactement du meme domaine, mais les reconnaitre visuellement aide énormément à lire l’API.

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
Elles indiquent généralement une convention d’appel spécifique, historiquement stdcall dans beaucoup de cas Win32 classiques.

Exemple :

Code: Select all

DWORD WINAPI ThreadFunc(LPVOID param);
Il faut respecter ces conventions dans les callbacks ou points d’entrée attendus par l’API, sinon on peut avoir des bugs graves d’appel de fonction.

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
En pratique, dans du code Win32 moderne, il vaut beaucoup mieux utiliser directement les versions W.

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
Donc la règle raisonnable aujourd’hui est simple :
  • privilégier les API W
  • raisonner en wchar_t et en chaines wide quand on parle aux API Windows
Les macros génériques : CreateFile, TEXT, TCHAR

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
De meme :
  • TCHAR devient wchar_t si UNICODE est défini
  • TCHAR devient char sinon
Autres types liés :
  • LPTSTR
  • LPCTSTR
Et pour les littéraux :
  • TEXT("abc")
  • _T("abc")
Historiquement, cette couche servait à écrire du code portable entre ANSI et Unicode. Aujourd’hui, dans du code neuf, beaucoup de développeurs préfèrent etre explicites et utiliser directement les types wide, les fonctions W et les littéraux L"...".

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
Exemple :

Code: Select all

HANDLE hFile = CreateFileW(
    L"test.txt",
    GENERIC_READ,
    FILE_SHARE_READ,
    NULL,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    NULL
);
Conversions de chaines : API utiles

Quand il faut convertir entre UTF-8, ANSI et UTF-16, deux API sont particulièrement importantes :
  • MultiByteToWideChar
  • WideCharToMultiByte
Elles permettent par exemple :
  • de convertir une chaine UTF-8 vers wchar_t
  • de convertir une chaine wide vers un encodage multi-octets
Ce sont des fonctions très importantes dès qu’on mélange Win32 et formats texte externes.

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
Ce ne sont pas les seuls types utiles, mais ils couvrent déjà énormément de signatures Win32 courantes.

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
Pour l’héritage et la création de processus :
  • CreateProcessW
  • CreatePipe
  • CreateEventW
  • CreateMutexW
  • OpenProcess
Pour les conversions et Unicode :
  • MultiByteToWideChar
  • WideCharToMultiByte
  • l’ensemble des API A / W comme CreateFileA / CreateFileW
Erreurs classiques

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
Ce qu’il faut retenir

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
Conclusion

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
alors tu possèdes déjà une très bonne base pour écrire du Win32 propre et sur.

A bientot pour le prochain cours :D

Who is online

Users browsing this forum: No registered users and 1 guest