How to pass a method as callback to a Windows API call?

前端 未结 6 1849
萌比男神i
萌比男神i 2020-12-14 13:10

I\'d like to pass a method of a class as callback to a WinAPI function. Is this possible and if yes, how?

Example case for setting a timer:

TMyClass          


        
相关标签:
6条回答
  • 2020-12-14 13:36
    TMyClass = class
    public
      procedure DoIt;
      procedure DoOnTimerViaMethod;
    end;
    
    var MyReceiverObject: TMyClass;
    
    [...]
    
    procedure TimerProc(Wnd:HWND; uMsg:DWORD; idEvent:PDWORD; dwTime:DWORD); stdcall:
    begin
      if Assigned(MyReceiverObject) then 
        MyReceiverObject.DoOnTimerViaMethod;
    end;
    
    procedure TMyClass.DoIt;
    begin
      MyReceiverObject := Self;
      SetTimer(0, 0, 8, @TimerProc);  // <-???- that's what I want to do (last param)
    end;
    

    Not perfect. Watch for the threads, variable overwriting etc. But it does the job.

    0 讨论(0)
  • 2020-12-14 13:38

    Madshi has a MethodToProcedure procedure. It's in the "madTools.pas" which is in the "madBasic" package. If you use it, you should change the calling convention for "TimerProc" to stdcall and DoIt procedure would become,

    TMyClass = class
    private
      Timer: UINT;
      SetTimerProc: Pointer;
    [...]
    
    procedure TMyClass.DoIt;
    begin
      SetTimerProc := MethodToProcedure(Self, @TMyClass.TimerProc);
      Timer := SetTimer(0, 0, 8, SetTimerProc);
    end;
    // After "KillTimer(0, Timer)" is called call:
    // VirtualFree(SetTimerProc, 0, MEM_RELEASE);
    


    I've never tried but I think one could also try to duplicate the code in the "classses.MakeObjectInstance" for passing other procedure types than TWndMethod.

    0 讨论(0)
  • 2020-12-14 13:46

    The TimerProc procedure should be a standard procedure, not a method pointer.

    A method pointer is really a pair of pointers; the first stores the address of a method, and the second stores a reference to the object the method belongs to

    Edit

    This might be as much OOP as you are going to get it. All the nasty stuff is hidden from anyone using your TMyClass.

    unit Unit2;
    
    interface
    
    type
      TMyClass = class
      private
        FTimerID: Integer;
        FPrivateValue: Boolean;
      public
        constructor Create;
        destructor Destroy; override;
        procedure DoIt;
      end;
    
    implementation
    
    uses
      Windows, Classes;
    
    var
      ClassList: TList;
    
    constructor TMyClass.Create;
    begin
      inherited Create;
      ClassList.Add(Self);
    end;
    
    destructor TMyClass.Destroy;
    var
      I: Integer;
    begin
      I := ClassList.IndexOf(Self);
      if I <> -1 then
        ClassList.Delete(I);
      inherited;
    end;
    
    procedure TimerProc(Wnd:HWND; uMsg:DWORD; idEvent:PDWORD; dwTime:DWORD); stdcall;
    var
      I: Integer;
      myClass: TMyClass;
    begin
      for I := 0 to Pred(ClassList.Count) do
      begin
        myClass := TMyClass(ClassList[I]);
        if myClass.FTimerID = Integer(idEvent) then
          myClass.FPrivateValue := True;
      end;
    end;
    
    procedure TMyClass.DoIt;
    begin
      FTimerID := SetTimer(0, 0, 8, @TimerProc);  // <-???- that's what I want to do (last param)
    end;
    
    initialization
      ClassList := TList.Create;
    
    finalization
      ClassList.Free;
    
    end.
    

    Edit: (as mentioned by glob)

    Don't forget to add the stdcall calling convention.

    0 讨论(0)
  • 2020-12-14 13:49

    I've used MakeObjectInstance a few times to do the same. Here's an article on the subject: How to use a VCL class member-function as a Win32 callback

    0 讨论(0)
  • 2020-12-14 13:52

    Response to your second edit:

    If you want a reply that includes a pointer to a TMyClass instance, you may be out of luck. Fundamentally, the procedure Windows will call has a certain signature and is not an object method. You cannot directly work around that, not even with __closure or procedure of object magic, except as described below and in other answers. Why?

    • Windows has no knowledge of it being an object method, and wants to call a procedure with a specific signature.

    • The pointer is no longer a simple pointer - it has two halves, the object instance and the method. It needs to save the Self, as well as the method.

    By the way, I don't understand what is wrong with a short dip outside the object-oriented world. Non-OO code is not necessarily dirty if used well.

    Original, pre-your-edit answer:

    It's not possible exactly as you are trying to do it. The method that SetTimer wants must exactly follow the TIMERPROC signature - see the MSDN documentation. This is a simple, non-object procedure.

    However, the method TMyClass.DoIt is an object method. It actually has two parts: the object on which it is called, and the method itself. In Delphi, this is a "procedure of object" or a "closure" (read about procedural types here). So, the signatures are not compatible, and you cannot store the object instance, which you need in order to call an object method. (There are also calling convention problems - standard Delphi methods are implemented using the fastcall convention, whereas TIMERPROC specifies CALLBACK which, from memory, is a macro that expands to stdcall. Read more about calling conventions and especially fastcall.)

    So, what do you do? You need to map your non-object-oriented callback into object-oriented code.

    There are several ways, and the simplest is this:

    If you only have one timer ever, then you know that when your timer callback is called it is that specific timer that fired. Save a method pointer in a variable that is of type procedure of object with the appropriate signature. See the Embarcadero documentation link above for more details. It will probably look like:

    type TMyObjectProc = procedure of object;
    var pfMyProc : TMyObjectProc;
    

    Then, initialise pfMyProc to nil. In TMyClass.DoIt, set pfMyProc to @DoIt - that is, it is now pointing at the DoIt procedure in the context of that specific TMyClass instantiation. Your callback can then call that method.

    (If you're interested, class variables that are of a procedural type like this are how event handlers are stored internally. The OnFoo properties of a VCL object are pointers to object procedures.)

    Unfortunately this procedural architecture is not object-oriented, but it's how it has to be done.

    Here's what some full code might look like (I'm not at a compiler, so it may not work as written, but it should be close):

    type TMyObjectProc = procedure of object;
    var pfMyProc : TMyObjectProc;
    
    initialization
      pfMyProc = nil;
    
    procedure MyTimerCallback(hWnd : HWND; uMsg : DWORD; idEvent : PDWORD; dwTime : DWORD); stdcall;
    begin
      if Assigned(pfMyProc) then begin
        pfMyProc(); // Calls DoIt, for the object that set the timer
        pfMyProc = nil;
      end;
    end;
    
    procedure TMyClass.MyOOCallback;
    begin
      // Handle your callback here
    end;
    
    procedure TMyClass.DoIt;
    begin
      pfMyProc = @MyOOCallback;
      SetTimer(0, 0, 8, @ MyTimerCallback);
    end;
    

    Another way would be to take advantage of the fact your timer has a unique ID. Save a mapping between the timer ID and the the object. In the callback, convert from the ID to the pointer, and call the object's method.

    Edit: I've noticed a comment to another answer suggesting using the address of your object as the timer ID. This will work, but is a potentially dangerous hack if you end up having two objects at the same address at different times, and you don't call KillTimer. I've used that method but don't personally like it - I think the extra bookkeeping of keeping a (timer ID, object pointer) map is better. It really comes down to personal style, though.

    0 讨论(0)
  • 2020-12-14 13:55

    Which version of Delphi are you using?

    In recent ones you can use static class methods for this:

    TMyClass = class
    public
      class procedure TimerProc(Wnd:HWND; uMsg:DWORD; idEvent:PDWORD; dwTime:DWORD); stdcall; static;
      procedure DoIt;
    end;
    [...]
    procedure TMyClass.DoIt;
    begin
      SetTimer(0, 0, 8, @TimerProc);  
    end;
    
    0 讨论(0)
提交回复
热议问题