Developpez.com - Rubrique Pascal

Le Club des Développeurs et IT Pro

Envoyer des chaînes ou des structures en asynchrone par PostMessage

En Delphi, par Andnotor

Le 2012-12-06 12:50:56, par Andnotor, Rédacteur/Modérateur
Bonjour à tous !

Je vous propose aujourd'hui un nouveau tutoriel sur l'envoi de chaînes ou de structures par PostMessage en utilisant la table d'atomes.

N'hésitez pas à faire part de vos réactions

Bonne lecture !
  Discussion forum
10 commentaires
  • Andnotor
    Rédacteur/Modérateur
    Tu peux envoyer tout ce que tu veux, entre threads ou entre processus

    Voici deux exemples.

    Rediriger la sortie d'une application console.
    Admettons que cycliquement, tu lances en tâche de fond une application console invisible pour de la maintenance (backup, mise à jour...). Tu aimerais cependant que l'utilisateur soit informé de l'avancement ou d'une éventuelle erreur. Tu pourrais bien sûr :
    • utiliser CreateProcess et rediriger la sortie sur un Pipe. Un travail conséquent ;
    • lancer ShellExecuteEx depuis un thread et attendre la fin pour contrôler le code de sortie (ExitCode), mais pas de notification d'avancement ;
    • envoyer WM_COPYDATA mais SendMessage est bloquant et dû à la structure de taille fixe, le texte risque d'être tronqué (ou alors il faut surdimentionner le buffer).

    Ici, rien de tout cela ! Il suffit de redéfinir WriteLn et d'envoyer PostTextMessage
    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    program Project1;
    
    {$APPTYPE CONSOLE}
    
    {$R *.res}
    
    uses
      SysUtils, Windows, Messages, MessagesEx;
    
    procedure WriteLn(aText :string);
    begin
      PostTextMessage(FindWindow('ClassName', 'WindowName'), WM_USER, aText);
    end;
    
    begin
      ClearTableOnExit := FALSE;
    
      try
        WriteLn('BlaBla');
        WriteLn('BlaBla');
    
      except
        on E: Exception do
          WriteLn(E.Message);
      end;
    end.

    Hook.
    Tu aimerais intercepter le retour de traitement d'un message par une fenêtre quelconque. Tu vas pour cela injecter une DLL dans les processus par SetWindowsHookEx. Mais un hook peut être très pénalisant pour le système et le ralentir considérablement si le traitement à effectuer est long.
    Jusqu'à Windows XP, on privilégiait le passage d'info par fichier mappé. Les lectures et écritures devant bien sûr être synchronisées par mutex. Depuis Vista et l'UAC, C'est fini ! La DLL injectée dans une application de niveau d'intégrité inférieur (par exemple Internet Explorer en mode protégé) n'aura pas accès au fichier mappé. Une violation est générée et c'est le deadlock assuré de l'application cible.
    On pourrait à nouveau utiliser WM_COPYDATA, mais il faudra que notre application soit très réactive. Imagine un hook souris tellement pénalisant que le curseur avance par saccade !
    Router simplement les informations par PostMessage ? Bien sûr si peu d'informations nous intéressent et tiennent dans WParam/LParam. Mais il ne faut pas espérer en récupérer plus par la suite par AttachThreadInput (UAC quand tu nous tiens...).

    Intercepter le retour d'un message se fait en plaçant un hook de type WH_CALLWNDPROCRET. Un pointeur est passé dans LParam correspondant à la structure suivante :
    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    type
      TCwpRetStruct = record
        lResult :LRESULT;
        lParam  :LPARAM;
        wParam  :WPARAM;
        message :UINT;
        hwnd    :HWND;
      end;
    Nous n'aurons donc plus qu'à faire :

    Code :
    1
    2
    3
    4
    5
    6
    7
    function CWPRetProc(aCode: integer; aWParam: WPARAM; aLParam: LPARAM):LRESULT; stdcall;
    begin
      if (aCode = HC_ACTION) then
        PostBufferMessage(FindWindow('ClassName', 'WindowName'), WM_USER, aLParam, SizeOf(TCwpRetStruct));
    
      Result := CallNextHookEx(0, aCode, aWParam, aLParam);
    end;
    Est-ce plus clair ?
  • Paul TOTH
    Expert éminent sénior
    Bonjour,

    Personnellement j'utilise un autre approche que je trouve bcp plus simple

    Code :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    type
      TMessageData = class
        UnEntier: Integer;
        UneChaine: string;
        UnStream: TStream;
        NImporteQuoi: TNImportQuoi;
      end;
    
      TDataMessage = record
        Msg: Integer;
        Null: Integer;
        Data: TMessageData;
       Result : Integer;
      end;
    
    procedure SendInfos(HWnd, Msg: Integer; UnEntier, UneChaine...);
    var
      data: TMessageData;
    begin
      data := TMessageData.Create;
      data.UnEntier := UnEntier;
      data.UneChaine := UneChaine;
      ...
      PostMessage(HWnd, Msg, 0, Integer(@data));
    end;
    le destinataire du message est responsable de la suppression de "data", mais il est possible d'ajouter ces objets à une ThreadList globale qu'on purge le cas échéant quand c'est nécessaire.
  • ShaiLeTroll
    Expert éminent sénior
    Envoyé par Andnotor 
    envoyer WM_COPYDATA mais SendMessage est bloquant et dû à la structure de taille fixe, le texte risque d'être tronqué (ou alors il faut surdimentionner le buffer).

    WM_COPYDATA est bloquant, cela a un avantage car son retour permet de vérifier la bonne reception\compréhension du message par le receveur

    Sinon WM_COPYDATA utilise une structure COPYDATASTRUCT de taille fixe mais permet justement de gérer un buffer de longueur variable via se membres cbData et lpData
    On peut donc strictement définir un buffer de la taille exacte de la donnée devant être envoyé

    Sinon, pourquoi ne pas avoir utiliser BinToHex qui aurait fait le travail de conversion en Hexa bien plus rapidement que les nombreuses réallocation\concaténation de Data.Bytes même si FastMM le gère bien mieux que l'ancien gestionnaire de mémoire

    Tient, j'ai fait un petit dev hier que l'on m'a réclamé pour simplifier la vie de l'utilisateur
    un programme A doit lancer un programme B
    A étant déjà loggué, B est lancé en mode "AutoLogin" pour que l'utilisateur n'est pas retapé son Login+PW
    Dans d'autres programmes qui font la même chose, le login est passé sur la ligne de commande, en clair pour certaines et d'autres en chiffré
    Cela reste une jolie faille de sécurité puisque cela permet un login sans PW
    Mon prédecesseur n'avait pas du tout conscience de la sécurité !

    Je passe un HWND qui servirait ensuite pour un échange entre A et B d'un GUID servant d'ID de session qui aboutit à l'authentification, c'est déjà plus difficile à choper (un hook)

    je viens d'en extraire une petite classe plus générique, on voit tout de suite qu'avec un peu d'effort que l'on pourrait hériter d'un TStream et utiliser un TBinaryReader\TBinaryWriter dessus

    la Version C++, si j'ai le temps, je la tenterais en Delphi

    Code c++ :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    //--------------------------------------------------------------------------- 
    typedef bool __fastcall (__closure *TShaiCopyDataMessagerReadEvent)(TObject* Sender, HWND DataSender, void *Data, int DataLen); 
      
    //--------------------------------------------------------------------------- 
    //                          TShaiCopyDataMessager                           - 
    //--------------------------------------------------------------------------- 
    class TShaiCopyDataMessager : public TObject 
    { 
    private: 
      // Membres Privés 
      HWND FSenderHandle; 
      HWND FRecipientHandle; 
      COPYDATASTRUCT FRawData; 
      TShaiCopyDataMessagerReadEvent FOnRead; 
      
      // Accesseurs 
      HWND __fastcall GetSenderHandle(); 
      
      // Méthodes Privées 
      void __fastcall WndProc(Messages::TMessage &Message); 
      bool DoRead(HWND DataSender, void *Data, int DataLen); 
      
    public: 
      // Constructeurs Publiques 
      /*constructor*/__fastcall TShaiCopyDataMessager(); 
      /*destructor*/virtual __fastcall ~TShaiCopyDataMessager(); 
      
      // Méthodes Publiques 
      void BeginWrite(); 
      void WriteInteger(int Value); 
      void WriteString(String Value); 
      void WriteBuffer(void *Data, int DataLen); 
      bool EndWrite(); 
      void CancelWrite(); 
      
      int ReadInteger(void *Data, /*out*/int &Value); 
      int ReadString(void *Data, /*out*/String &Value); 
      
      // Propriétés Publiques 
      __property HWND SenderHandle = {read=GetSenderHandle}; 
      __property HWND RecipientHandle = {read=FRecipientHandle, write=FRecipientHandle}; 
      __property TShaiCopyDataMessagerReadEvent OnRead = {read=FOnRead, write=FOnRead}; 
    };
    Code c++ :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    //--------------------------------------------------------------------------- 
    //                          TShaiCopyDataMessager                           - 
    //--------------------------------------------------------------------------- 
    /*constructor*/__fastcall TShaiCopyDataMessager::TShaiCopyDataMessager() 
    { 
      
    } 
      
    //--------------------------------------------------------------------------- 
    /*destructor*/__fastcall TShaiCopyDataMessager::~TShaiCopyDataMessager() 
    { 
      CancelWrite(); 
      
      if (FSenderHandle != 0) 
      { 
        DeallocateHWnd(FSenderHandle); 
        FSenderHandle = 0; 
      } 
    } 
      
    //--------------------------------------------------------------------------- 
    HWND __fastcall TShaiCopyDataMessager::GetSenderHandle() 
    { 
      if ( ! FSenderHandle) 
        FSenderHandle = AllocateHWnd(WndProc); 
      
      return FSenderHandle; 
    } 
      
    //--------------------------------------------------------------------------- 
    void TShaiCopyDataMessager::BeginWrite() 
    { 
      CancelWrite(); 
    } 
      
    //--------------------------------------------------------------------------- 
    void TShaiCopyDataMessager::WriteInteger(int Value) 
    { 
      WriteBuffer(&Value, sizeof(Value)); 
    } 
      
    //--------------------------------------------------------------------------- 
    void TShaiCopyDataMessager::WriteString(String Value) 
    { 
      int Len = Value.Length(); 
      WriteInteger(Len); 
      
      if (Len > 0) 
      { 
        int Size = Len * sizeof(Char); // gère que la table BMP de UTF-16 
        WriteBuffer(Value.c_str(), Size); 
      } 
    } 
      
    //--------------------------------------------------------------------------- 
    void TShaiCopyDataMessager::WriteBuffer(void *Data, int DataLen) 
    { 
      int Position = FRawData.cbData; 
      FRawData.cbData += DataLen; 
      if (Position > 0) 
        ReallocMemory(FRawData.lpData, FRawData.cbData); 
      else 
        FRawData.lpData = GetMemory(FRawData.cbData); 
      CopyMemory((Byte*)FRawData.lpData + Position, Data, DataLen); 
    } 
      
    //--------------------------------------------------------------------------- 
    bool TShaiCopyDataMessager::EndWrite() 
    { 
      bool Result = SendMessage(FRecipientHandle, WM_COPYDATA, (WPARAM)FSenderHandle, (LPARAM)&FRawData); 
      CancelWrite(); 
      return Result; 
    } 
      
    //--------------------------------------------------------------------------- 
    void TShaiCopyDataMessager::CancelWrite() 
    { 
      FreeMemory(FRawData.lpData); 
      ZeroMemory(&FRawData, sizeof(FRawData)); 
    } 
      
    //--------------------------------------------------------------------------- 
    int TShaiCopyDataMessager::ReadInteger(void *Data, /*out*/int &Value) 
    { 
      int Result = -1; 
      if ( ! IsBadReadPtr(Data, sizeof(Value))) 
      { 
        Value = *(int*)Data; 
        Result = sizeof(Value); 
      } 
      return Result; 
    } 
      
    //--------------------------------------------------------------------------- 
    int TShaiCopyDataMessager::ReadString(void *Data, /*out*/String &Value) 
    { 
      int Result = -1; 
      int Len = 0; 
      int TmpResult = ReadInteger(Data, Len); 
      if (TmpResult > 0) 
      { 
        Result = TmpResult; 
        if (Len > 0) 
        { 
          Value = UnicodeString((WideChar*)((Byte*)Data + TmpResult), Len); 
          int Size = Len * sizeof(Char); // gère que la table BMP de UTF-16 
          Result += Size; 
        } 
        else 
          Value = ""; 
      } 
      return Result; 
    } 
      
    //--------------------------------------------------------------------------- 
    void __fastcall TShaiCopyDataMessager::WndProc(Messages::TMessage &Message) 
    { 
      if (Message.Msg == WM_COPYDATA) 
      { 
        PCOPYDATASTRUCT PRawData = (PCOPYDATASTRUCT)Message.LParam; 
        if (PRawData && PRawData->cbData) 
          Message.Result = DoRead((HWND)Message.WParam, PRawData->lpData, PRawData->cbData); 
        else 
          Message.Result = false; 
      } 
      else 
        Message.Result = DefWindowProc(FSenderHandle, Message.Msg, Message.WParam, Message.LParam); 
    } 
      
    //--------------------------------------------------------------------------- 
    bool TShaiCopyDataMessager::DoRead(HWND DataSender, void *Data, int DataLen) 
    { 
      bool Result = false; 
      if (FOnRead) 
        Result = FOnRead(this, DataSender, Data, DataLen); 
      
      return Result; 
    }

    //---------------------------------------------------------------------------

    Code c++ :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    void __fastcall TSystemManipForm::BtnCopyDataWhoIAmClick(TObject *Sender) 
    { 
      if ( ! FCopyDataMessager) 
      { 
        FCopyDataMessager = new TShaiCopyDataMessager(); 
        FCopyDataMessager->OnRead = CopyDataMessagerReadEventHandler; 
      } 
      
      EditCopyDataWhoIAm->Text = UIntToStr((unsigned)FCopyDataMessager->SenderHandle); 
    } 
      
    //--------------------------------------------------------------------------- 
    bool __fastcall TSystemManipForm::CopyDataMessagerReadEventHandler(TObject *Sender, HWND DataSender, void *Data, int DataLen) 
    { 
      bool Result = false; 
      
      TShaiCopyDataMessager* Messager = dynamic_cast<TShaiCopyDataMessager*>(Sender); 
      if (Messager) 
      { 
        String S1, S2, S3; 
      
        int OffSet = Messager->ReadString(Data, S1); 
        Data = (Byte*)Data + OffSet; 
        OffSet = Messager->ReadString(Data, S2); 
        Data = (Byte*)Data + OffSet; 
        OffSet = Messager->ReadString(Data, S3); 
      
        MemoCopyDataRecv->Lines->Add(S1 + S2 + S3); 
        Result = Pos("bad", S2) <= 0; 
      } 
      
      return Result; 
    } 
      
    //--------------------------------------------------------------------------- 
    void __fastcall TSystemManipForm::BtnCopyDataSendToClick(TObject *Sender) 
    { 
      if ( ! FCopyDataMessager) 
      { 
        FCopyDataMessager = new TShaiCopyDataMessager(); 
        FCopyDataMessager->OnRead = CopyDataMessagerReadEventHandler; 
      } 
      
      FCopyDataMessager->RecipientHandle = (HWND)StrToInt(EditCopyDataSendTo->Text); 
      FCopyDataMessager->BeginWrite(); 
      FCopyDataMessager->WriteString("La Chaine est : \""); 
      FCopyDataMessager->WriteString(EditCopyDataSendToText->Text); 
      FCopyDataMessager->WriteString("\" !"); 
      if ( ! FCopyDataMessager->EndWrite()) 
        MemoCopyDataRecv->Lines->Add("bad est refusé"); 
    } 
    //---------------------------------------------------------------------------
    Code dfm :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    object BtnCopyDataWhoIAm: TButton 
      Left = 3 
      Top = 3 
      Width = 75 
      Height = 25 
      Caption = 'Who I Am ?' 
      TabOrder = 0 
      OnClick = BtnCopyDataWhoIAmClick 
    end 
    object EditCopyDataWhoIAm: TEdit 
      Left = 84 
      Top = 5 
      Width = 121 
      Height = 21 
      Alignment = taRightJustify 
      NumbersOnly = True 
      ReadOnly = True 
      TabOrder = 1 
      Text = '?' 
    end 
    object BtnCopyDataSendTo: TButton 
      Left = 3 
      Top = 34 
      Width = 75 
      Height = 25 
      Caption = 'Send To' 
      TabOrder = 2 
      OnClick = BtnCopyDataSendToClick 
    end 
    object EditCopyDataSendTo: TEdit 
      Left = 84 
      Top = 36 
      Width = 121 
      Height = 21 
      Alignment = taRightJustify 
      NumbersOnly = True 
      TabOrder = 3 
      Text = '?' 
    end 
    object EditCopyDataSendToText: TEdit 
      Left = 211 
      Top = 36 
      Width = 121 
      Height = 21 
      TabOrder = 4 
      Text = 'Bonjour !' 
    end 
    object MemoCopyDataRecv: TMemo 
      Left = 3 
      Top = 65 
      Width = 329 
      Height = 274 
      Lines.Strings = ( 
        'Receive :') 
      ReadOnly = True 
      ScrollBars = ssBoth 
      TabOrder = 5 
    end
  • Andnotor
    Rédacteur/Modérateur
    C'est possible en effet

    Par record, il faudra prendre soin de faire appel à GetMem/FreeMem (New/Dispose) et pas simplement déclarer une variable locale à la fonction !
    Mon approche se veut cependant plus universelle et autorise l'envoi entre processus
  • ShaiLeTroll
    Expert éminent sénior
    J'ai jamais eu le temps de m'occuper de [QR] Comment passer une string dans un message Windows ? qui utilise WM_COPYDATA qui fonctionnee en inter-processus
  • Andnotor
    Rédacteur/Modérateur
    ...mais qui est obligatoirement synchrone par SendMessage
  • BuzzLeclaire
    Membre éprouvé
    @AndNotY,

    Salut, j'ai parcouru tout ton article, et à la fin je me suis demandé à quoi cela peut servir en cas réel ? (je préfère passer pour un idiot, mais je pose quand même la question...)

    Est-ce un moyen de transmettre des données (tableau, chaîne, nombre) entre 2 applications ?

  • Andnotor
    Rédacteur/Modérateur
    Envoyé par ShaiLeTroll
    WM_COPYDATA est bloquant, cela a un avantage car son retour permet de vérifier la bonne reception\compréhension du message par le receveur
    C'est clair qu'à partir du moment où un dialogue est nécessaire, cette technique n'est pas applicable. Ce n'est pas pour rien que j'ai pris l'exemple de WH_CALLWNDPROCRET dont la donnée ne peut être modifiée

    Maintenant, mon but était vraiment de faire de l'asynchrone...

    Envoyé par ShaiLeTroll
    Sinon, pourquoi ne pas avoir utiliser BinToHex qui aurait fait le travail de conversion en Hexa bien plus rapidement que les nombreuses réallocation\concaténation de Data.Bytes
    J'aurais pu Comme j'aurais pu aussi définir une chaîne de 2x la taille de la donnée et travailler sur l'index du caractère.
    Mais je suis un pervers et au départ je comptais convertir un octet sur 10 bits (actuellement 16 bits) pour utiliser au mieux les 255 octets à disposition. Puisqu'une des contraintes est : pas de byte à "0" son codage aurait été 1000010000 (en ajoutant toujours "1". Mais ça devenait un peu compliqué et le tuto n'aurait plus été "Confirmé", mais "Expert"

    Mais j'aurais dû plus simplifier !

    Envoyé par ShaiLeTroll
    Je passe un HWND qui servirait ensuite pour un échange entre A et B d'un GUID servant d'ID de session qui aboutit à l'authentification, c'est déjà plus difficile à choper (un hook)
    J'ai aussi utiliser ce genre de technique en envoyant un message que l'application ne sait pas gérer (RegisterWindowMessage, donc inconnu par la cible)
    mais récupéré par hook pour exécuter du code dans le processus cible
  • MIWAN
    Membre habitué
    c'est Noël
    Dans l'endroit et au bon moment que ce tutoriel car je passais des heures pour apprendre quelques choses sur les HOOK(s).

    Merci pour ce tutoriel et les interventions des chères membres
  • leonick12
    Membre à l'essai
    salut, j'ai vu le tuto, mais j'avoue c'est quand il faut envoyer ou recevoir je ne sais pas quoi mettre dans la commande : PostTextMessage(aWnd :hWnd; aMessage :cardinal; aText :string) :integer;

    si quelqu'un connais bien cette commande, je suis preneur car ca corresponds bien a ce que je voudrais faire, j'ai bien récuperer le fichier MessagesEx je l'ai intégrer a mon application, mais pas plus.