How can I make AllocateHwnd threadsafe?
Asked Answered
R

3

19

VCL components are designed to be used solely from the main thread of an application. For visual components this never presents me with any difficulties. However, I would sometimes like to be able to use, for example, non-visual components like TTimer from a background thread. Or indeed just create a hidden window. This is not safe because of the reliance on AllocateHwnd. Now, AllocateHwnd is not threadsafe which I understand is by design.

Is there an easy solution that allows me to use AllocateHwnd from a background thread?

Redstone answered 11/1, 2012 at 13:44 Comment(5)
With pure Windows API; the SetTimer doesn't require HWND; it's also possible to use callback function. See here for instance.Retch
@Retch You are quite right, but TTimer does use WM_TIMER and that's the target here.Redstone
I was thinking about something what's in my deleted post (pseudocode). Of course still you have to dispatch the messages to get the WM_TIMER pass through, but it looks for me less evil than AllocateHwnd for a worker thread :)Retch
You can add the (I know, now deprecated) TClientSocket to the list of component being affected by this. MakeObjectInstance isn't thread safe by itself either.Stedfast
@KenBourassa Yes, MakeObjectInstance is actually the fundamental problem. I'd like a threadsafe version of that too but it seems a little harder to achieve.Redstone
R
16

This problem can be solved like so:

  1. Obtain or implement a threadsafe version of AllocateHwnd and DeallocateHwnd.
  2. Replace the VCL's unsafe versions of these functions.

For item 1 I use Primož Gabrijelcic's code, as described on his blog article on the subject. For item 2 I simply use the very well-known trick of patching the code at runtime and replacing the beginning of the unsafe routines with unconditional JMP instructions that redirect execution to the threadsafe functions.

Putting it all together results in the following unit.

(* Makes AllocateHwnd safe to call from threads. For example this makes TTimer
   safe to use from threads.  Include this unit as early as possible in your
   .dpr file.  It must come after any memory manager, but it must be included
   immediately after that before any included unit has an opportunity to call
   Classes.AllocateHwnd. *)
unit MakeAllocateHwndThreadsafe;

interface

implementation

{$IF CompilerVersion >= 23}{$DEFINE ScopedUnitNames}{$IFEND}
uses
  {$IFDEF ScopedUnitNames}System.SysUtils{$ELSE}SysUtils{$ENDIF},
  {$IFDEF ScopedUnitNames}System.Classes{$ELSE}Classes{$ENDIF},
  {$IFDEF ScopedUnitNames}Winapi.Windows{$ELSE}Windows{$ENDIF},
  {$IFDEF ScopedUnitNames}Winapi.Messages{$ELSE}Messages{$ENDIF};

const //DSiAllocateHwnd window extra data offsets
  GWL_METHODCODE = SizeOf(pointer) * 0;
  GWL_METHODDATA = SizeOf(pointer) * 1;

  //DSiAllocateHwnd hidden window (and window class) name
  CDSiHiddenWindowName = 'DSiUtilWindow';

var
  //DSiAllocateHwnd lock
  GDSiWndHandlerCritSect: TRTLCriticalSection;
  //Count of registered windows in this instance
  GDSiWndHandlerCount: integer;

//Class message dispatcher for the DSiUtilWindow class. Fetches instance's WndProc from
//the window extra data and calls it.
function DSiClassWndProc(Window: HWND; Message: UINT; WParam: WPARAM; LParam: LPARAM): LRESULT; stdcall;

var
  instanceWndProc: TMethod;
  msg            : TMessage;
begin
  {$IFDEF CPUX64}
  instanceWndProc.Code := pointer(GetWindowLongPtr(Window, GWL_METHODCODE));
  instanceWndProc.Data := pointer(GetWindowLongPtr(Window, GWL_METHODDATA));
  {$ELSE}
  instanceWndProc.Code := pointer(GetWindowLong(Window, GWL_METHODCODE));
  instanceWndProc.Data := pointer(GetWindowLong(Window, GWL_METHODDATA));
  {$ENDIF ~CPUX64}
  if Assigned(TWndMethod(instanceWndProc)) then
  begin
    msg.msg := Message;
    msg.wParam := WParam;
    msg.lParam := LParam;
    msg.Result := 0;
    TWndMethod(instanceWndProc)(msg);
    Result := msg.Result
  end
  else
    Result := DefWindowProc(Window, Message, WParam,LParam);
end; { DSiClassWndProc }

//Thread-safe AllocateHwnd.
//  @author  gabr [based on http://fidoforum.ru/pages/new46s35o217746.ru.delphi and
//                 TIcsWndHandler.AllocateHWnd from ICS v6 (http://www.overbyte.be)]
//  @since   2007-05-30
function DSiAllocateHWnd(wndProcMethod: TWndMethod): HWND;
var
  alreadyRegistered: boolean;
  tempClass        : TWndClass;
  utilWindowClass  : TWndClass;
begin
  Result := 0;
  FillChar(utilWindowClass, SizeOf(utilWindowClass), 0);
  EnterCriticalSection(GDSiWndHandlerCritSect);
  try
    alreadyRegistered := GetClassInfo(HInstance, CDSiHiddenWindowName, tempClass);
    if (not alreadyRegistered) or (tempClass.lpfnWndProc <> @DSiClassWndProc) then begin
      if alreadyRegistered then
        {$IFDEF ScopedUnitNames}Winapi.{$ENDIF}Windows.UnregisterClass(CDSiHiddenWindowName, HInstance);
      utilWindowClass.lpszClassName := CDSiHiddenWindowName;
      utilWindowClass.hInstance := HInstance;
      utilWindowClass.lpfnWndProc := @DSiClassWndProc;
      utilWindowClass.cbWndExtra := SizeOf(TMethod);
      if {$IFDEF ScopedUnitNames}Winapi.{$ENDIF}Windows.RegisterClass(utilWindowClass) = 0 then
        raise Exception.CreateFmt('Unable to register DSiWin32 hidden window class. %s',
          [SysErrorMessage(GetLastError)]);
    end;
    Result := CreateWindowEx(WS_EX_TOOLWINDOW, CDSiHiddenWindowName, '', WS_POPUP,
      0, 0, 0, 0, 0, 0, HInstance, nil);
    if Result = 0 then
      raise Exception.CreateFmt('Unable to create DSiWin32 hidden window. %s',
              [SysErrorMessage(GetLastError)]);
    {$IFDEF CPUX64}
    SetWindowLongPtr(Result, GWL_METHODDATA, NativeInt(TMethod(wndProcMethod).Data));
    SetWindowLongPtr(Result, GWL_METHODCODE, NativeInt(TMethod(wndProcMethod).Code));
    {$ELSE}
    SetWindowLong(Result, GWL_METHODDATA, cardinal(TMethod(wndProcMethod).Data));
    SetWindowLong(Result, GWL_METHODCODE, cardinal(TMethod(wndProcMethod).Code));
    {$ENDIF ~CPUX64}
    Inc(GDSiWndHandlerCount);
  finally LeaveCriticalSection(GDSiWndHandlerCritSect); end;
end; { DSiAllocateHWnd }

//Thread-safe DeallocateHwnd.
//  @author  gabr [based on http://fidoforum.ru/pages/new46s35o217746.ru.delphi and
//                 TIcsWndHandler.AllocateHWnd from ICS v6 (http://www.overbyte.be)]
//  @since   2007-05-30
procedure DSiDeallocateHWnd(wnd: HWND);
begin
  if wnd = 0 then
    Exit;
  DestroyWindow(wnd);
  EnterCriticalSection(GDSiWndHandlerCritSect);
  try
    Dec(GDSiWndHandlerCount);
    if GDSiWndHandlerCount <= 0 then
      {$IFDEF ScopedUnitNames}Winapi.{$ENDIF}Windows.UnregisterClass(CDSiHiddenWindowName, HInstance);
  finally LeaveCriticalSection(GDSiWndHandlerCritSect); end;
end; { DSiDeallocateHWnd }

procedure PatchCode(Address: Pointer; const NewCode; Size: Integer);
var
  OldProtect: DWORD;
begin
  if VirtualProtect(Address, Size, PAGE_EXECUTE_READWRITE, OldProtect) then begin
    Move(NewCode, Address^, Size);
    FlushInstructionCache(GetCurrentProcess, Address, Size);
    VirtualProtect(Address, Size, OldProtect, @OldProtect);
  end;
end;

type
  PInstruction = ^TInstruction;
  TInstruction = packed record
    Opcode: Byte;
    Offset: Integer;
  end;

procedure RedirectProcedure(OldAddress, NewAddress: Pointer);
var
  NewCode: TInstruction;
begin
  NewCode.Opcode := $E9;//jump relative
  NewCode.Offset := NativeInt(NewAddress)-NativeInt(OldAddress)-SizeOf(NewCode);
  PatchCode(OldAddress, NewCode, SizeOf(NewCode));
end;

initialization
  InitializeCriticalSection(GDSiWndHandlerCritSect);
  RedirectProcedure(@AllocateHWnd, @DSiAllocateHWnd);
  RedirectProcedure(@DeallocateHWnd, @DSiDeallocateHWnd);

finalization
  DeleteCriticalSection(GDSiWndHandlerCritSect);

end.

This unit must be included very early in the .dpr file's list of units. Clearly it cannot appear before any custom memory manager, but it should appear immediately after that. The reason being that the replacement routines must be installed before any calls to AllocateHwnd are made.

Update I have merged in the very latest version of Primož's code which he kindly sent to me.

Redstone answered 11/1, 2012 at 13:44 Comment(27)
If anyone wonders why I asked and answered my own question, this is in response to a request for this code made to me by somebody on Twitter.Redstone
I don't really understand the question. If I had to control some GUI TTimer from some non-GUI thread, I would just PostMessage the interval and set the timer in the message-handler, (perhaps using '-1' to mean 'disable'). I could always post the TTimer instance in the other PostMessage parameter if there is more than one.Duvalier
@MartinJames: David is not trying to control a GUIThread bound TTimer from a background thread, but to work with a TTimer entirely from a background thread.Splutter
@MartinJames Marjan is right, I need the WM_TIMER messages to arrive in a message queue owned by the worker thread.Redstone
@David, why on earth would you want a Timer in a background thread? Can you expand on this subject?Eris
@Eris For example, to keep a network connection from timing out when the application goes idle. Start a timer in a background thread that pokes the connection periodically to keep it alive.Redstone
@Eris Well, the TTimer component is unchanged. When you include this unit is becomes safe to create a TTimer on a background thread. If you use the vanilla VCL then doing so results in a race condition.Redstone
@David: I see what you mean. I use signals to solve this problem. Each signal has a Timestamp associated, the workerthread keeps track of all signals and executes the signal when the timestamp is equal or less than the current time.Eris
the worker will check the whole time for signals. I can set a speed property, which is fact nothing more than a sleep.Eris
let us continue this discussion in chatEris
@DavidHeffernan So if i understand correctly. A visual control will also be safe to create in a background thread with this?Jaeger
@Eric Not necessarily. This just avoids the race in MakeObjectInstance. You cannot run a VCL control this way. Non-visual timer is simple enough for this to be enough.Redstone
@DavidHeffernan I get this error when compiling it. I have included the unit before anything in .DPR. Exception class Exception with message 'Unable to register DSiWin32 hidden window class. Class already exists'. It happens here EventMonitor := TOmniEventMonitor.Create(nil);Jaeger
@Eric That's a runtime error. The code compiles, and if included as the first unit in your .dpr file, works fine.Redstone
@DavidHeffernan Not really. Because TOmniEventMonitor.Create also calls emMessageWindow := DSiAllocateHWnd(WndProc);Jaeger
@EricSantos My code does not include a type named TOmniEventMonitor. As I said, the code in the answer compiles and runs fine.Redstone
@DavidHeffernan Put a TOmniEventMonitor on a Form and include your code and it will not work.Jaeger
@DavidHeffernan To be compatible with OmniThreadLibrary one must change CDSiHiddenWindowName = 'DSiUtilWindow';. Please mention this in your answer. Thank you.Jaeger
@EricSantos Then I guess you need to fix it. Change my code to use a different class name.Redstone
@DanielMaurić Not so. You can call DSiAllocateHWnd multiple times.Redstone
@DavidHeffernan Thanks, I figured it out and deleted the comment before I saw your reply. I do still have some problems with this but that may be another question.Terrazas
+1 @DavidHeffernan what would happen if I use Halt in my programme.(the finalization part will not execute). and is that jump instruction also used for 64 bit or that is another story.Alephnull
+Nasreddine if you call Halt then you get a normal termination. The patch works just as well in 64 bit windows.Redstone
The signature of the DSiClassWndProc function needs correction for x64, otherwise in some cases CreateWindowEx returns 0 and the error "Unable to create DSiWin32 hidden window" occurs. Correction: {$IFDEF CPUX64} function DSiClassWndProc(Window: HWND; Message, WParam, LParam: NativeInt): NativeInt; stdcall; {$ELSE} function DSiClassWndProc(Window: HWND; Message, WParam, LParam: longint): longint; stdcall; {$ENDIF}Judd
@Judd this is a wiki, you should just edit the answer to correct thatRedstone
@Judd thanks. Actually your edit contained a mistake. The Message arg is UINT. In fact my subsequent edit shows how to do it without using a conditional.Redstone
@DavidHeffernan Yes, you are right. Your code is betterJudd
K
6

Don't use TTimer in a thread, it will never be safe. Have the thread either:

1) use SetTimer() with a manual message loop. You don't need an HWND if you use a callback function, but you do still have to dispatch messages.

2) use CreateWaitableTimer() and then call WaitForSingleObject() in a loop until the timer is signalled.

3) use timeSetEvent(), which is a multi-threaded timer. Just be careful because its callback is called in its own thread so make sure your callback function is thread-safe, and there are restrictions to what you are allowed to call inside that thread. Best to have it set a signal that your real thread waits on an then does its work outside of the timer.

Koph answered 12/1, 2012 at 0:27 Comment(8)
I must say I find it strange that you appear to deny the viability or possibility of creating windows that have affinity with non-UI threads. Is that really what you mean?Redstone
The Win32 API allows windows to be created in the context of worker threads, even UI windows, as long as the threads run their own message loops. But VCL-based windows are not safe to use outside the context of the main thread because the VCL internally does certain things, and uses certain resources, that are not protected from concurrent access. So the rule of thumb is to NEVER use VCL-based UIs in the context of worker threads, period.Koph
It is very unlikely that AllocateHWnd() will ever be "fixed" to allow use in worker threads.Koph
AllocateHwnd is fixed perfectly by my code and TTimer is safe to use from a worker thread after that fix. I agree that it can only work if you have full control over the WndProc, or, as is the case with TTimer, the WndProc is sufficiently benign.Redstone
I was referring to Embarcadero making the VCL's native AllocateHWnd() implementation thread-safe.Koph
Remy, I asked David for the run-time patch to make AllocateHWnd thread-safe, though I may not be source of this question. It is related to a much bigger question I researched and not found an answer. I want to have a timer in an Indy thread. Specifically, the thread created for a (dbx) DataSnap user session. Indy's TCP server creates a thread for the connection. I'm certain there is a message loop in there to handle the WinSock calls. But I can't find a way to hook into the WndProc for this thread to even use SetTimer. I'll post another StackOverflow question for this specific question.Witmer
Indy uses blocking sockets, which do not use window messages. As such, Indy connection threads do not have a message queue or a message loop by default. If you need one, you have to provide it yourself. I would not recomnend it, as it does not fit into Indy's threading model (unless you absolutely need it, such as for apartment-threaded COM objects). There are other ways to do thread-based timers without using messages.Koph
Remy, question asked at #9010781. I'd greatly appreciate suggestions for other ways to do thread-based timers. Thanks!Witmer
M
2

Since you have already written code that operates in a dedicated thread, I would assume you don't expect any code to run while this code waits for something. In that case you could just call Sleep either with a specific number of milliseconds, or with a small amount of milliseconds and use this in a loop to check Now or GetTickCount to see if a certain time has elapsed. Using Sleep will also keep CPU-usage down, since the operating system is signaled that you don't require the thread to keep running for that time.

Mathison answered 11/1, 2012 at 16:34 Comment(6)
Once you start running a message loop, the message retrieval function GetMessage blocks when the queue is empty.Redstone
Oh did I forget to mention? I'm suggesting to forget about TTimer and messaging altogether.Mathison
I'm providing code for somebody that wants to use TTimer. Or for some reason needs to create a window handle with a window proc that is the method of an object. Each to their own.Redstone
Threads are used for many, many things. Consider a server where multiple users connect simultaneously. From basic TCP server to DataSnap server, each connection is typically a separate thread that lasts the lifetime of the connection. Based on a request from a user, you want something to happen after an elapsed time. Such as caching data for 5 minutes one request is made in anticipation of another request. If no more requests after 5 minutes, clear the cache. The cache lives in that thread's context. I do not want to involve the main thread in any way. Suggestions without using a timer?Witmer
This is an entire new question in a comment on an answer to another question. Please use the 'ask question' button at the top left of this page.Mathison
I just now noticed Stijn's response. I wasn't asking a question to get an answer but rather asking a rhetorical question to give another example of where David's code would be useful. My apologies for the confusion.Witmer

© 2022 - 2024 — McMap. All rights reserved.