Utiliser la STL avec la WinAPI

Cet article concerne uniquement l’utilisation des structures de la STL dans les fonctions de l’API Windows, mais peut être généralisé à toutes les fonctions C (POSIX, etc.).
Néanmoins, le comportement pouvant être différent selon les fonctions, je peux ne pas maîtriser certaines subtilités ; C’est pour cela qu’on se concentrera uniquement sur la WinAPI.


Si vous êtes sous Windows, il y a des chances que vous deviez utiliser les fonctions de l’API Windows. Le souci, c’est que ces dernières sont en C, et donc impliquent l’utilisation de pointeurs et toutes autres joyeusetés du langage.
Sachez que si vous programmez en C++, il y a moyen d’utiliser les structures de la Standard Template Library (STL) au sein de ces fonctions.

En premier lieu, il faut qu’on définisse quelle structure utiliser pour chaque contexte :

  • pour le type LPCSTR (const char *)/LPSTR (char *), on utilisera std::string
  • pour le type LPCWSTR (const wchar_t *)/LPWSTR (wchar_t *), on utilisera std::wstring

➡ De manière générale, la documentation parlera du type LPTSTR, qui correspond à LPSTR si votre code est en ANSI, et à LPWSTR s’il est en Unicode.

  • pour le reste, on utilisera std::vector<T>, T étant le type du pointeur demandé

Vous connaissez la taille des données de retour
C’est assez simple : vous redimensionnez la structure à la taille correspondante, et vous la donnez à la fonction de l’API ; Soit en utilisant la méthode data(), soit en donnant l’adresse de la première case.

Mémo : depuis la norme C++11, string et vector sont certifiées contiguës.

Pour l’exemple, prenons la signature de la fonction GetEnvironmentVariable :

DWORD GetEnvironmentVariable(
  LPCTSTR lpName,
  LPTSTR  lpBuffer,
  DWORD   nSize
);

Voici donc la fonction getEnv de Chronis :

void CHEnv::getEnv(const std::wstring & varEnv, std::wstring & resultat)
{
	unsigned long tailleVar(GetEnvironmentVariable(varEnv.c_str(), 0, 0));

	if (tailleVar > 1)
	{
		resultat.resize(tailleVar);
		GetEnvironmentVariable(varEnv.c_str(), resultat.data(), tailleVar);

		// On retire le caractère terminal car la structure string le gère déjà
		resultat.pop_back();
	}
}

Plusieurs remarques :

➡ Mon code étant en Unicode, j’ai utilisé la structure std::wstring.

➡ Souvent, les fonctions de la WinAPI sont utilisables « à vide » et retournent la taille nécessaire pour stocker les données de retour. C’est très utile de le faire pour bien allouer le bon espace, et cela est peu coûteux en temps.

➡ Certains fonctions vont demander une chaîne de caractères en entrée pour pouvoir réaliser leur action. Si on utilise une structure string, il faut faire appel à la méthode c_str() qui aura l’avantage de garantir le caractère de fin de chaîne.

Mémo : depuis C++11, la méthode data() garantie aussi le caractère terminal dans la structure string, mais je préfère ici garder la sémantique telle que la méthode c_str() est utilisée dans un but de lecture seule.

Aussi, il faut savoir que la structure en elle-même gère ce caractère, donc dans le cas où un string est donné en sortie il faut faire attention à gérer le caractère terminateur fourni par la fonction en elle-même, et qui peut poser problème en cas de concaténation.

Vous ne connaissez pas la taille des données de retour
Dans ce cas, il faut réserver (et non redimensionner) une taille suffisamment grande pour éviter que les informations soient tronquées. La plupart du temps, la documentation de la fonction spécifie la taille maximale de retour.
C’est souvent 255 (stocké dans la macro MAX_PATH), ou l’équivalent en Unicode (32767).

Prenons cette fois-ci la signature de la fonction GetProcessImageFileName :

DWORD GetProcessImageFileName(
  HANDLE hProcess,
  LPSTR  lpImageFileName,
  DWORD  nSize
);

Voici un morceau de code de Chronis :

std::wstring cheminProcess;
cheminProcess.reserve(WMAX_PATH);

// On récupère le chemin NT du processus traité
if (!GetProcessImageFileName(this->process, &(cheminProcess[0]), WMAX_PATH))
	return GetLastError();
this->chemin = cheminProcess.c_str();

Remarques très importantes :

➡ On utilise la méthode reserve() et non resize() pour réserver uniquement l’espace et non l’allouer ; Cela permet ensuite de retrouver la vraie taille de la chaîne.

➡ Dans le cas d’un tableau classique ce n’est pas gênant, mais dans le cadre d’une chaîne qu’on souhaite manipuler il est important de rendre la chaîne valide.

En effet, utiliser la fonction reserve() ne change pas la taille du tableau ; On est face à une chaîne d’une taille inconnue et où sa méthode length() ou size() renvoie 0.

Une bonne solution est de récréer une chaîne tout en utilisant la méthode c_str(). Cela va permettre de reparcourir les caractères jusqu’au premier ‘\0’ terminateur -sa présence étant assurée par la fonction de l’API elle-même-, et donc d’avoir la bonne taille.

C++17 et string_view
Le but ici n’est pas d’expliquer le fonctionnement de la structure string_view, mais de montrer qu’elle est autant utilisable que la structure string sous certaines conditions.
Si on reprend le code en 1ère partie :

void SCEnv::getEnv(const std::wstring_view & varEnv, std::wstring & resultat)
{
	unsigned long tailleVar(GetEnvironmentVariable(varEnv.data(), 0, 0));

	if (tailleVar > 1)
	{
		resultat.resize(tailleVar);
		GetEnvironmentVariable(varEnv.data(), resultat.data(), tailleVar);

		// On retire le caractère terminal car la structure string le gère déjà
		resultat.pop_back();
	}
}

Plusieurs remarques :

➡ std::string_view (et son homologue Unicode std::wstring_view) ne sont que des vues de chaînes de caractère déjà allouées : elles ne sont donc accessibles qu’en lecture.

➡ La méthode c_str() n’existe pas, et la méthode data() ne garantie PAS le caractère terminateur : il faut donc faire attention à ce que la chaîne soit valide si on ne veut pas que la fonction de l’API plante.

Pour ce qui est des avantages et inconvénients de cette structure, je pourrais en parler dans un prochain article.
En attendant, vous pouvez voir sa page dédiée sur cppreference.

Posted in C++

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *