7

I have a Delphi 6 application that has a thread dedicated to communicating with a foreign application that uses SendMessage() and WM_COPYDATA messages to interface with external programs. Therefore, I create a hidden window with AllocateHWND() to service that need since a thread message queue won't work due to the SendMessage() function only accepting window handles, not thread IDs. What I'm not sure about is what to put in the thread Execute() method.

I assume that if I use a GetMessage() loop or a create a loop with a WaitFor*() function call in it that the thread will block and therefore the thread's WndProc() will never process the SendMessage() messages from the foreign program right? If so, what is the correct code to put in an Execute() loop that will not consume CPU cycles unnecessarily but will exit once a WM_QUIT message is received? I can always do a loop with a Sleep() if necessary but I'm wondering if there is a better way.

Robert Oschler
  • 14,153
  • 18
  • 94
  • 227

2 Answers2

15

AllocateHWnd() (more specifically, MakeObjectInstance()) is not thread-safe, so you have to be careful with it. Better to use CreatWindow/Ex() directly instead (or a thread-safe version of AllocateHWnd(), like DSiAllocateHwnd().

In any case, an HWND is tied to the thread context that creates it, so you have to create and destroy the HWND inside your Execute() method, not in the thread's constructor/destructor. Also, even though SendMessage() is being used to send the messages to you, they are coming from another process, so they will not be processed by your HWND until its owning thread performs message retrieval operations, so the thread needs its own message loop.

Your Execute() method should look something like this:

procedure TMyThread.Execute;
var
  Message: TMsg;
begin
  FWnd := ...; // create the HWND and tie it to WndProc()...
  try
    while not Terminated do
    begin
      if MsgWaitForMultipleObjects(0, nil^, False, 1000, QS_ALLINPUT) = WAIT_OBJECT_0 then
      begin
        while PeekMessage(Message, 0, 0, 0, PM_REMOVE) do
        begin
          TranslateMessage(Message);
          DispatchMessage(Message);
        end;
      end;
    end;
  finally
    // destroy FWnd...
  end;
end;

procedure TMyThread.WndProc(var Message: TMessage);
begin
  if Message.Msg = WM_COPYDATA then
  begin
    ...
    Message.Result := ...;
  end else
    Message.Result := DefWindowProc(FWnd, Message.Msg, Message.WParam, Message.LParam);
end;

Alternatively:

// In Delphi XE2, a virtual TerminatedSet() method was added to TThread,
// which is called when TThread.Terminate() is called.  In earlier versions,
// use a custom method instead...

type
  TMyThread = class(TThread)
  private
    procedure Execute; override;
    {$IF RTLVersion >= 23}
    procedure TerminatedSet; override;
    {$IFEND}
  public
    {$IF RTLVersion < 23}
    procedure Terminate; reintroduce;
    {$IFEND}
  end;

procedure TMyThread.Execute;
var
  Message: TMsg;
begin
  FWnd := ...; // create the HWND and tie it to WndProc()...
  try
    while not Terminated do
    begin
      if WaitMessage then
      begin
        while PeekMessage(Message, 0, 0, 0, PM_REMOVE) do
        begin
          if Message.Msg = WM_QUIT then Break;
          TranslateMessage(Message);
          DispatchMessage(Message);
        end;
      end;
    end;
  finally
    // destroy FWnd...
  end;
end;

{$IF RTLVersion < 23}
procedure TMyThread.Terminate;
begin
  inherited Terminate;
  PostThreadMessage(ThreadID, WM_QUIT, 0, 0);
end;
{$ELSE}
procedure TMyThread.TerminatedSet;
begin
  PostThreadMessage(ThreadID, WM_QUIT, 0, 0);
end;
{$IFEND}
Community
  • 1
  • 1
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Thanks @Remy Lebeau. The MsgWaitForMultipleObjects() was the key ingredient I was missing. – Robert Oschler Oct 09 '11 at 02:51
  • 2
    You should use DSiAllocateHwnd instead of AllocateHwnd. http://www.thedelphigeek.com/2007/06/allocatehwnd-is-not-thread-safe.html – gabr Oct 09 '11 at 08:23
  • @David: `WaitMessage()` does not return until a new message arrives, blocking the calling thread. `MsgWaitForMultipleObjects()` has a timeout, so the thread can wake up to do other things while the message queue is idle, like checking the `Terminated` property. You can's do that `WaitMessage()` unless you post a message yourself. – Remy Lebeau Oct 09 '11 at 15:57
  • But I think that posting message is better. Don't want to have to wait for the timeout. And if you do it that way then you can be properly idle. Your code will wake up every second no matter what. – David Heffernan Oct 09 '11 at 16:08
  • @RemyLebeau - it's better to use MsgWaitForMultipleObjects with INFINITE timeout, not 1000, and use an event in case if we need to end the thread quickly. Instead of an event, we may post a message to the thread after calling Terminate - so it will check its Terminated property and exit. Using INFINITE will significantly decrease the Page Faults column in the task manager, and will save resources ;-)) – Maxim Masiutin May 10 '17 at 17:11
  • 1
    @MaximMasiutin: yes, I'm aware of all of that. My earlier comments were written years ago. In modern Delphi versions, `TThread` has a virtual `TerminatedSet()` method that can be overridden to do such a post/signal when `Terminate()` is called. But the original question was for Delphi 6, which doesn't have `TerminatedSet()`, so a custom method would be needed instead. I have updated my answer. – Remy Lebeau May 10 '17 at 18:04
  • @RemyLebeau - Thank you for the commend and for updating the answer. I have also posted a code - it is a similar solution but doesn't rely on any Delphi version, since it is mostly on pure Win32. – Maxim Masiutin May 10 '17 at 18:48
  • @RemyLebeau If I used `AllocateHWnd()` and `DeallocateHWnd()` in the context of the main thread (the constructor and distructor of the main form) and used it to send messages to Main thread, would that be safe? – Nasreddine Galfout Nov 26 '17 at 10:59
  • 1
    @NasreddineAbdelillahGalfout yes – Remy Lebeau Nov 26 '17 at 17:51
  • @RemyLebeau By the way the link in your answer to DSiAllocateHwnd is dead please replace it with the one in gabr's comment. – Nasreddine Galfout Nov 26 '17 at 23:10
0

Here is a loop that doesn't require Classes.pas and relies solely on System.pas for some auxiliary functions, Windows.pas for Win32 API functions and Messages.pas for the WM_ constants.

Please note that the window handle here is created and destroyed from the worker thread, but the main thread waits until the worker thread completes the initialization. You can postpone this wait until a later moment, when you actually need the window handle, so the main thread may do some work in the meanwhile, while the worker thread sets itself up.

unit WorkerThread;

interface

implementation

uses
  Messages,
  Windows;

var
  ExitEvent, ThreadReadyEvent: THandle;
  ThreadId: TThreadID;
  ThreadHandle: THandle;
  WindowHandle: HWND;

function HandleCopyData(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT;
begin
  Result := 0; // handle it
end;

function HandleWmUser(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT;
// you may handle other messages as well - just an example of the WM_USER handling
begin
  Result := 0; // handle it
end;

function MyWindowProc(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
begin
  if Msg = WM_COPYDATA then
  begin
    Result := HandleCopyData(hWnd, Msg, wParam, lParam);
  end else
  if Msg = WM_USER then
  begin
    // you may handle other messages as well - just an example of the WM_USER handling
    // if you have more than 2 differnt messag types, use the "case" switch
    Result := HandleWmUser(hWnd, Msg, wParam, lParam);
  end else
  begin
    Result := DefWindowProc(hWnd, Msg, wParam, lParam);
  end;
end;

const
  WindowClassName = 'MsgHelperWndClass';
  WindowClass: TWndClass = (
    style: 0;
    lpfnWndProc: @MyWindowProc;
    cbClsExtra: 0;
    cbWndExtra: 0;
    hInstance: 0;
    hIcon: 0;
    hCursor: 0;
    hbrBackground: 0;
    lpszMenuName: nil;
    lpszClassName: WindowClassName);

procedure CreateWindowFromThread;
var
  A: ATOM;
begin
  A := RegisterClass(WindowClass);
  WindowHandle := CreateWindowEx(WS_EX_TOOLWINDOW, WindowClassName, 'Message Helper Window', WS_POPUP, 0, 0, 0, 0, 0, 0, hInstance, nil);
end;

procedure FreeWindowFromThread;
var
  H: HWND;
begin
  H := WindowHandle;
  WindowHandle := 0;
  DestroyWindow(H);
  UnregisterClass(WindowClassName, hInstance);
end;

function ThreadFunc(P: Pointer): Integer;  //The worker thread main loop, windows handle initialization and finalization
const
  EventCount = 1;
var
  EventArray: array[0..EventCount-1] of THandle;
  R: Cardinal;
  M: TMsg;
begin
  Result := 0;
  CreateWindowFromThread;
  try
    EventArray[0] := ExitEvent; // you may add other events if you need - just enlarge the Events array
    SetEvent(ThreadReadyEvent);
    repeat
      R := MsgWaitForMultipleObjects(EventCount, EventArray, False, INFINITE, QS_ALLINPUT);
      if R = WAIT_OBJECT_0 + EventCount then
      begin
        while PeekMessage(M, WindowHandle, 0, 0, PM_REMOVE) do
        begin
          case M.Message of
             WM_QUIT:
               Break;
             else
                begin
                  TranslateMessage(M);
                  DispatchMessage(M);
                end;
          end;
        end;
        if M.Message = WM_QUIT then
          Break;
      end else
      if R = WAIT_OBJECT_0 then
      begin
        // we have the ExitEvent signaled - so the thread have to quit
        Break;
      end else
      if R = WAIT_TIMEOUT then
      begin
        // do nothing, the timeout should not have happened since we have the INFINITE timeout
      end else
      begin
        // some errror happened, or the wait was abandoned with WAIT_ABANDONED_0 to (WAIT_ABANDONED_0 + nCount– 1)
        // just exit the thread
        Break;
      end;
    until False;
  finally
    FreeWindowFromThread;
  end;
end;

procedure InitializeFromMainThread;
begin
  ExitEvent := CreateEvent(nil, False, False, nil);
  ThreadReadyEvent := CreateEvent(nil, False, False, nil);
  ThreadHandle := BeginThread(nil, 0, @ThreadFunc, nil, 0, ThreadId);
end;

procedure WaitUntilHelperThreadIsReady;
begin
  WaitForSingleObject(ThreadReadyEvent, INFINITE); // wait until the worker thread start running and initialize the main window
  CloseHandle(ThreadReadyEvent); // we won't need it any more
  ThreadReadyEvent := 0;
end;

procedure FinalizeFromMainThread;
begin
  SetEvent(ExitEvent); // we should call it AFTER terminate for the Terminated property would already be True when the tread exits from MsgWaitForMultipleObjects
  WaitForSingleObject(ThreadHandle, INFINITE);
  CloseHandle(ThreadHandle); ThreadHandle := 0;
  CloseHandle(ExitEvent); ExitEvent := 0;
end;

initialization
  InitializeFromMainThread;

  WaitUntilHelperThreadIsReady; // we can call it later, just before we need the window handle
finalization
  FinalizeFromMainThread;
end.
Maxim Masiutin
  • 3,991
  • 4
  • 55
  • 72
  • 1
    if I used `Halt` in my program the finalization section will not be executed. is this okey. – Nasreddine Galfout Nov 26 '17 at 10:47
  • 2
    @NasreddineAbdelillahGalfout do not use `Halt`. There is rarely a good reason to use it except in extreme conditions – Remy Lebeau Nov 26 '17 at 17:53
  • 1
    @RemyLebeau thank you for both responses. I been reading the documentation about `AllocateHWnd()` and other alternatives. the finalization section came up, and when I read about it I found out about `Halt`. I don't use it but it is good to know. Thank you again. – Nasreddine Galfout Nov 26 '17 at 20:06