How can I make AllocateHwnd threadsafe?

后端 未结 3 1219
情话喂你
情话喂你 2020-12-01 05:59

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 so

3条回答
  •  一整个雨季
    2020-12-01 06:27

    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, WParam, LParam: longint): longint; 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.

提交回复
热议问题