Programmation Win32 Unicode

Le problème exposé dans ce sujet a été résolu.

Bonsoir à toutes et à tous !

Je me mets actuellement à la programmation windows avec l’API win32.

Mon petit projet de la soirée: envoyé un ping avec icmpsendecho, en utilisant au maximum l’API windows. Pour cela je voudrais pouvoir tout mettre en Unicode cependant j’ai quelques soucis avec les différents types et je me perds entre les TCHAR, WHAR, PWCHAR etc.

Je viens vers vous pour que vous m’éclaireriez sur le sujet.

Ci-dessous mon code actuel:

// easy_ping.c
#include "common.h"

int easy_ping(PWSTR ip) {
    // Declare and initialize variables

    HANDLE hIcmpFile;
    IPAddr ipaddr = INADDR_NONE;
    DWORD dwRetVal = 0;
    TCHAR SendData[50] = L"Data Buffer";
    LPVOID ReplyBuffer = NULL;
    DWORD ReplySize = 0;


    InetPton(AF_INET, ip, &ipaddr);

    hIcmpFile = IcmpCreateFile();
    if (hIcmpFile == INVALID_HANDLE_VALUE) {
        printf("\tUnable to open handle.\n");
        printf("IcmpCreatefile returned error: %ld\n", GetLastError());
        return 1;
    }
    printf("%zd", sizeof(SendData));
    ReplySize = sizeof(ICMP_ECHO_REPLY) + sizeof(SendData);
    ReplyBuffer = (VOID*)malloc(ReplySize);
    if (ReplyBuffer == NULL) {
        printf("\tUnable to allocate memory\n");
        return 1;
    }


    dwRetVal = IcmpSendEcho(hIcmpFile, ipaddr, SendData, sizeof(SendData),
        NULL, ReplyBuffer, ReplySize, 1000);
    if (dwRetVal != 0) {
        PICMP_ECHO_REPLY pEchoReply = (PICMP_ECHO_REPLY)ReplyBuffer;
        IPAddr ReplyAddr;
        ReplyAddr = pEchoReply->Address;
        printf("\tSent icmp message to %S\n", ip);
        if (dwRetVal > 1) {
            printf("\tReceived %ld icmp message responses\n", dwRetVal);
            printf("\tInformation from the first response:\n");
        }
        else {
            printf("\tReceived %ld icmp message response\n", dwRetVal);
            printf("\tInformation from this response:\n");
        }
        TCHAR ipaddrreply[128] = { 0 };
        InetNtop(AF_INET, &ReplyAddr, ipaddrreply, sizeof(ipaddrreply));
        printf("\t  Received from %S\n", ipaddrreply);
        printf("\t  Status = %ld\n",
            pEchoReply->Status);
        printf("\t  Roundtrip time = %ld milliseconds\n",
            pEchoReply->RoundTripTime);
        return 0;
    }
    else {
        printf("\tCall to IcmpSendEcho failed.\n");
        printf("\tIcmpSendEcho returned error: %ld\n", GetLastError());
        return 1;
    }
}
// main.c
#include "easy_ping.h"
int __cdecl main(int argc, char** argv) {
    int i = easy_ping(L"8.8.8.8");

    return 0;
}
// common.h
#pragma once

//////////////////////////// Unicode Build Option /////////////////////////////


// Always compiler using Unicode.
#ifndef UNICODE
#define UNICODE
#endif

// When using Unicode Windows functions, use Unicode C-Runtime functions too.
#ifdef UNICODE
#ifndef _UNICODE
#define _UNICODE
#endif
#endif

#include <tchar.h>
#include <winsock2.h>
#include <iphlpapi.h>
#include <icmpapi.h>
#include <stdio.h>
#include <Ws2tcpip.h>


#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "ws2_32.lib")
// easy_ping.h
#pragma once
int easy_ping(char* ip);

Mon but final est d’avoir un code écrit de la meilleure des manières.

J’ai par exemple une question sur la taille de la variable ipaddrreply (dans easy_ping.c ligne 47). Quelle devrait être sa taille ? (sachant qu’au maximum une IP c’est 255.255.255.255 mais en unicode)

Merci !

Salut ! :)

D’une part, tu parles d’Unicode mais il existe plusieurs tailles d’Unicode comme UTF-8 (le plus connu), UTF-16, etc… De quel Unicode parles-tu donc ?

D’autre part, dans common.h (ligne 13) tu fais un test sur l’existance de UNICODE mais il est inutile puisque tu définis déjà UNICODE une ligne plus haut, donc pas besoin de le tester.

Salut,

Je vois que tu utilises des chaînes de caractères littérales larges (L"8.8.8.8" par exemple). Je t’invite à lire les deux nouveaux chapitres du tuto C sur la représentation des chaînes de caractères et sur les caractères larges pour y voir un peu plus clair à leur sujet. Sinon, je ne sais pas de quel encodage tu as besoin, mais s’il s’agit de l’UTF-8 tu peux employer la syntaxe u8"une chaîne en UTF-8" pour obtenir une chaîne de caractères littérale encodée en UTF-8 (ceci nécessite un compilateur respectant la norme C11).

+0 -0

Salut.

L’idéal dans ton cas c’est de ne pas avoir à te soucier de l’encodage des chaînes (unicode ou non).

Utilise la macro TEXT(), celle-ci convertira automatiquement tes chaînes en unicode si tu compiles ton projet avec l’option UNICODE définie. Dans le cas inverse, ce sera des chaînes ASCII (voire UTF-8, tant que ton terminal les interprète correctement).

Ce n’est pas une bonne pratique que de gérer manuellement ces différences. Les développeurs de l’API Win32 sont assez malins et ont pensé à ce cas de figure. Autant se servir de tous leurs outils ! ;)

Bonjour à tous, merci pour vos réponses !

Salut ! :)

D’une part, tu parles d’Unicode mais il existe plusieurs tailles d’Unicode comme UTF-8 (le plus connu), UTF-16, etc… De quel Unicode parles-tu donc ?

D’autre part, dans common.h (ligne 13) tu fais un test sur l’existance de UNICODE mais il est inutile puisque tu définis déjà UNICODE une ligne plus haut, donc pas besoin de le tester.

Vanadiae

Windows utilise utf-16 !

Ensuite concernant la définition de UNICODE, il y a en fait d’abord la définition de UNICODEpour utiliser les fonctions functionW de l’api Win32 et ensuite il y a la définition de _UNICODE pour l’utilisation de l’unicode avec le CRT (petit lien explicatif).

Salut.

L’idéal dans ton cas c’est de ne pas avoir à te soucier de l’encodage des chaînes (unicode ou non).

Utilise la macro TEXT(), celle-ci convertira automatiquement tes chaînes en unicode si tu compiles ton projet avec l’option UNICODE définie. Dans le cas inverse, ce sera des chaînes ASCII (voire UTF-8, tant que ton terminal les interprète correctement).

Ce n’est pas une bonne pratique que de gérer manuellement ces différences. Les développeurs de l’API Win32 sont assez malins et ont pensé à ce cas de figure. Autant se servir de tous leurs outils ! ;)

Ge0

AHHHH oui je vois. Mais on est d’accord que TEXT ou _T ne s’applique qu’à une chaine de caractères et non pas à une variable ?

Voici donc les fichiers modifiés:

// easy_ping.c
#include "common.h"

int easy_ping(TCHAR* ip) {
    // Declare and initialize variables

    HANDLE hIcmpFile;
    IPAddr ipaddr = INADDR_NONE;
    DWORD dwRetVal = 0;
    TCHAR SendData[50] = TEXT("Data Buffer");
    LPVOID ReplyBuffer = NULL;
    DWORD ReplySize = 0;


    InetPton(AF_INET, ip, &ipaddr);

    hIcmpFile = IcmpCreateFile();
    if (hIcmpFile == INVALID_HANDLE_VALUE) {
        printf("\tUnable to open handle.\n");
        printf("IcmpCreatefile returned error: %ld\n", GetLastError());
        return 1;
    }
    ReplySize = sizeof(ICMP_ECHO_REPLY) + sizeof(SendData) + 8;
    ReplyBuffer = (VOID*)malloc(ReplySize);
    if (ReplyBuffer == NULL) {
        printf("\tUnable to allocate memory\n");
        return 1;
    }


    dwRetVal = IcmpSendEcho(hIcmpFile, ipaddr, SendData, sizeof(SendData),
        NULL, ReplyBuffer, ReplySize, 1000);
    if (dwRetVal != 0) {
        PICMP_ECHO_REPLY pEchoReply = (PICMP_ECHO_REPLY)ReplyBuffer;
        IPAddr ReplyAddr;
        ReplyAddr = pEchoReply->Address;
        printf("\tSent icmp message to %S\n", ip);
        if (dwRetVal > 1) {
            printf("\tReceived %ld icmp message responses\n", dwRetVal);
            printf("\tInformation from the first response:\n");
        }
        else {
            printf("\tReceived %ld icmp message response\n", dwRetVal);
            printf("\tInformation from this response:\n");
        }
        TCHAR ipaddrreply[16] = { 0 };
        InetNtop(AF_INET, &ReplyAddr, ipaddrreply, sizeof(ipaddrreply));
        printf("\t  Received from %S\n", ipaddrreply);
        printf("\t  Status = %ld\n",
            pEchoReply->Status);
        printf("\t  Roundtrip time = %ld milliseconds\n",
            pEchoReply->RoundTripTime);
        return 0;
    }
    else {
        printf("\tCall to IcmpSendEcho failed.\n");
        printf("\tIcmpSendEcho returned error: %ld\n", GetLastError());
        return 1;
    }
}
// main.c
#include "common.h"
#include "easy_ping.h"
int __cdecl main(int argc, char** argv) {
    int i = easy_ping(TEXT("8.8.8.8"));

    return 0;
}

Ce pendant il me reste 2 warnings:

  • "Using uninitialized memory 'SendData’."
  • "Buffer overrun while writing to 'ipaddrreply’: the writable size is '32' bytes, but '64' bytes might be written.". Je pense que je me suis embrouillé tout seul je pense. Une adresse IP contient au maximum 15 caractères + \0. Donc sa taille doit être de 16. Par contre je ne comprends pas pourquoi mon compilateur dit qu’il risque d’y avoir un dépassement de tampon…

Suis-je maintenant complètement générique et très windows-esque ? J’aimerai atteindre un certain niveau de "beauté" de code dans l’esprit windows avec l’API Win32.

+0 -0

AHHHH oui je vois. Mais on est d’accord que TEXT ou _T ne s’applique qu’à une chaine de caractères et non pas à une variable ?

Tout à fait. :)

"Buffer overrun while writing to 'ipaddrreply’: the writable size is '32' bytes, but '64' bytes might be written.". Je pense que je me suis embrouillé tout seul je pense. Une adresse IP contient au maximum 15 caractères + \0. Donc sa taille doit être de 16. Par contre je ne comprends pas pourquoi mon compilateur dit qu’il risque d’y avoir un dépassement de tampon…

Car tu as un tableau de caractères ASCII. Or il te faut une zone qui soit doublement plus grande si tu travailles avec des caractères unicode.

Pour rester souple, je te recommande d’utiliser SysAllocString. Tu manipules en interne des chaînes unicode, mais « qui peut le plus peut le moins ».

Salut,

Pour la petite explication sur les types TCHAR, WCHAR, PWCHAR; etc. en fait c’est assez simple une fois qu’on connaît les abréviations utilisées:

  • C = const
  • P = pointer
  • W = wide
  • LP = long pointer (c’est sans doute une réminiscence de l’époque 16 bits)

ET dans tout ça, le T (pour text) désigne wchar_t si on compile en unicode, char sinon. Windows utilise aussi volontiers STR.

En d’autres termes,

  • TCHAR = wchar_t ou char selon la compilation
  • PWCHAR = wchar_t* potentiellement modifiée par la fonction, et toujours wchar_t* même si tu ne compiles pas en unicode
  • LPTCSTR = long pointeur constant sur une chaîne de texte => const char* ou const wchar_t*; c’est le type souvent utilisé pour les textes dans les contrôles, les messages et les notifications.
  • LPCWSTR = long pointeur constant sur une chaîne large => const wchar_t* que tu compiles en unicode ou pas

L’abréviation LP est utilisée partout dans l’API win32 et signifie la même chose, long pointer. Par exemple LPVOID, LPDWORD, …

Tiens par contre pour SysAllocString, je ne savais pas… je croyais que c’était uniquement utilisé dans le cadre de composants COM ou de DDE.

+0 -0

Tiens par contre pour SysAllocString, je ne savais pas… je croyais que c’était uniquement utilisé dans le cadre de composants COM ou de DDE.

Après réflexion, je pense que le PO a mieux fait de se référer à ton message qu’aux miens. Car il s’avère que tu as raison.

Ne pas tenir compte de mon message précédent, donc.

AHHHH oui je vois. Mais on est d’accord que TEXT ou _T ne s’applique qu’à une chaine de caractères et non pas à une variable ?

Tout à fait. :)

"Buffer overrun while writing to 'ipaddrreply’: the writable size is '32' bytes, but '64' bytes might be written.". Je pense que je me suis embrouillé tout seul je pense. Une adresse IP contient au maximum 15 caractères + \0. Donc sa taille doit être de 16. Par contre je ne comprends pas pourquoi mon compilateur dit qu’il risque d’y avoir un dépassement de tampon…

Car tu as un tableau de caractères ASCII. Or il te faut une zone qui soit doublement plus grande si tu travailles avec des caractères unicode.

Pour rester souple, je te recommande d’utiliser SysAllocString. Tu manipules en interne des chaînes unicode, mais « qui peut le plus peut le moins ».

Ge0

Je ne comprends pas. Mon tableau est de type TCHAR, lors de la compilation cela fera comme si c’était wchar_t. Donc ce n’est pas un tableau de caractères ASCII ? De plus si je double la taille, une nouveau warning apparait disant qu’il me faut le double encore…

Je chipote peut-être car le programme fonctionne très bien mais je n’aime pas avoir de warnings haha :D

Et merci @QuentinC pour cette explication bien détaillée !

Je ne comprends pas. Mon tableau est de type TCHAR, lors de la compilation cela fera comme si c’était wchar_t. Donc ce n’est pas un tableau de caractères ASCII ? De plus si je double la taille, une nouveau warning apparait disant qu’il me faut le double encore…

Balèze ton compilateur pour te sortir ce warning… c’est lui qui a raison.

D’après la doc de la fonction: https://docs.microsoft.com/en-us/windows/desktop/api/ws2tcpip/nf-ws2tcpip-inetntopw

Pour le dernier paramètre, il est dit ceci: On input, the length, in characters, of the buffer pointed to by the pStringBuf parameter.

C’est un coup classique: tu dois indiquer la taille en caractères et non en bytes.

Pour être full proof quelque soient les options de compilation, il faut donc écrire: sizeof(ipaddrreply)/sizeof(TCHAR).

+1 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte