delphi

SendMessage(WM_COPYDATA) + Record + String


I want to send a record, that right now have only a string on it, but I will add more variables. Is the first time I work with records, so this maybe is a silly question. But, why this works:

type
  TDataPipe = record
    WindowTitle: String[255];
  end;

var
  Data: TDataPipe;
  copyDataStruct : TCopyDataStruct;
begin
  Data.WindowTitle:= String(PChar(HookedMessage.lParam));
  copyDataStruct.dwData := 0;
  copyDataStruct.cbData := SizeOf(Data);
  copyDataStruct.lpData := @Data;
  SendMessage(FindWindow('TForm1', nil), WM_COPYDATA, Integer(hInstance), Integer(@copyDataStruct));    
end;

Receiving side:

type
  TDataPipe = record
    WindowTitle: String[255];
  end;

procedure TForm1.WMCopyData(var Msg: TWMCopyData);
var
  sampleRecord : TDataPipe;
begin
  sampleRecord.WindowTitle:= TDataPipe(Msg.CopyDataStruct.lpData^).WindowTitle;
  Memo1.Lines.Add(sampleRecord.WindowTitle);
end;

Why if on the record, I use:

WindowTitle: String; //removed the fixed size

and on the sending side I use:

Data.WindowTitle:= PChar(HookedMessage.lParam); //removed String()

it simply doesn't go?

I get access violations / app freeze...

The scenario is: sending side is a DLL hooked using SetWindowsHookEx, receiving side a simple exe that loaded / called SetWindowsHookEx...


Solution

  • A String[255] is a fixed 256-byte block of memory, where the character data is stored directly in that memory. As such, it is safe to pass as-is across process boundaries without serialization.

    A String, on the other hand, is a dynamic type. It just contains a pointer to character data that is stored elsewhere in memory. As such, you can't pass a String as-is across process boundaries, all you would be passing is the pointer value, which has no meaning to the receiving process. You have to serialize String data into a flat format that can safely by passed to, and deserialized by, the receiving process. For example:

    Sending side:

    type
      PDataPipe = ^TDataPipe;
      TDataPipe = record
        WindowTitleLen: Integer;
        WindowTitleData: array[0..0] of Char;
        //WindowTitleData: array[0..WindowTitleLen-1] of Char;
      end;
    
    var
      Wnd: HWND;
      s: String;
      Data: PDataPipe;
      DataLen: Integer;
      copyDataStruct : TCopyDataStruct;
    begin
      Wnd := FindWindow('TForm1', nil);
      if Wnd = 0 then Exit;
    
      s := PChar(HookedMessage.lParam);
      DataLen := SizeOf(Integer) + (SizeOf(Char) * Length(s));
      GetMem(Data, DataLen);
      try
        Data.WindowTitleLen := Length(s);
        StrMove(Data.WindowTitleData, PChar(s), Length(s));
    
        copyDataStruct.dwData := ...; // see notes further below
        copyDataStruct.cbData := DataLen;
        copyDataStruct.lpData := Data;
        SendMessage(Wnd, WM_COPYDATA, 0, LPARAM(@copyDataStruct));    
      finally
        FreeMem(Data);
      end;
    end;
    

    Receiving side:

    type
      PDataPipe = ^TDataPipe;
      TDataPipe = record
        WindowTitleLen: Integer;
        WindowTitleData: array[0..0] of Char;
        //WindowTitleData: array[0..WindowTitleLen-1] of Char;
      end;
    
    procedure TForm1.WMCopyData(var Msg: TWMCopyData);
    var
      Data: PDataPipe;
      s: string;
    begin
      Data := PDataPipe(Msg.CopyDataStruct.lpData);
      SetString(s, Data.WindowTitleData, Data.WindowTitleLen);
      Memo1.Lines.Add(s);
    end;
    

    That being said, in either situation, you really should be assigning your own custom ID number to the copyDataStruct.dwData field. The VCL itself uses WM_COPYDATA internally, so you don't want to get those messages confused with yours, and vice versa. You can use RegisterWindowMessage() to create a unique ID to avoid conflicts with IDs used by other WM_COPYDATA users:

    var
      dwMyCopyDataID: DWORD;
    
    ...
    
    var
      ...
      copyDataStruct : TCopyDataStruct;
    begin
      ...
      copyDataStruct.dwData := dwMyCopyDataID;
      ...
    end;
    
    ...
    
    initialization
      dwMyCopyDataID := RegisterWindowMessage('MyCopyDataID');
    

    var
      dwMyCopyDataID: DWORD;
    
    ...
    
    procedure TForm1.WMCopyData(var Msg: TWMCopyData);
    var
      ...
    begin
      if Msg.CopyDataStruct.dwData = dwMyCopyDataID then
      begin
        ...
      end else
        inherited;
    end;
    
    ...
    
    initialization
      dwMyCopyDataID := RegisterWindowMessage('MyCopyDataID');
    

    Lastly, the WPARAM parameter of WM_COPYDATA is an HWND, not an HINSTANCE. If the sender does not have its own HWND, just pass 0. Do not pass your sender's HInstance variable.