Delphi動態事件深入分析

Delphi動態事件深入分析
2009-2-7
作者:不得閒
核心提示:本實驗證明了在類中方法的調用時候,所有的方法都隱含了一個Self參數,並且該參數作爲對象方法的第一個參數傳遞...

首先做一個空窗體,放入一Button。
在implementation下面聲明兩個方法如下:

//外部方法,只聲明一個參數,此時按照標準的對象內部事件方法TNotifyEvent聲明,此聲明中,Sender則對應爲產生該事件的對象指針。

//外部方法,聲明兩個參數,用來證明,對象在調用時候會傳遞一個Self指針,此時我們假設Frm是通過類對象傳遞過來的Self指針,而Sender爲產生該事件的對象指針

//然後在 ‘指定調用’按扭事件中寫代碼:

//很顯然運行的時候,點該按扭得到的是返回一個 消息內容爲 ‘Button1’的對話框,這是調用Form1類的對象事件觸發的方法。

//在調用 ‘調用Form類外部方法觸發事件’ Click事件中寫

//另一個按扭寫代碼如下:

運行之後
點一下 ‘調用Form類外部方法觸發事件’ ,然後在點 ‘指定調用’按扭,
showmessage(TComponent(Sender).Name);返回的值是 ‘Form1’,此時是否就已經說明了其第一個參數是否就是傳遞的一個Self指針呢。所以在調用Button.Click事件的時候傳遞過來的第一個參數爲Form1內部的Self指針,而該指針是指向Form1的。此時,我們在該函數的
Begin位置放下一個斷點,程序運行時候,此處的斷點爲非可用的,如下圖:

說明程序在Begin處根本沒有處理其他任何代碼,此時,將斷點調到
showmessage(TComponent(Sender).Name);然後點 按扭 程序運行到斷點處停下
調出CPU View窗口查看代碼如下

注意 EAX,EBX,EDX,ECX的值,首先一條是
Mov  eax,[eax+$08] //該條指令將對象的Name屬性值傳遞到Eax中
Call   ShowMessage //此函數需要一個參數,Delphi的參數傳遞規則爲EAX,EDX,ECX
如此可見,沒有任何多餘的處理,但是此時還不能證明Eax傳遞過來的就是類對象的Self指針

此時將 ‘調用Form類外部方法觸發事件’ Click事件中代碼的函數換成
ExtClick
既將   integer(@ExtClickEvent) := integer(@ExtClick1);
換成   integer(@ExtClickEvent) := integer(@ExtClick);
然後重新重複上面的步驟,在ExtClick的Begin處下斷點,程序運行到斷點處停下,則說明
程序在Begin時候有代碼執行,打開CPU View查看如下:

可見在Begin之後,ShowMessage函數之前,有兩段代碼如下:
Push ebx     //保存Ebx的值
Mov ebx,eax  //將Eax的值暫時存放到Ebx中
然後主要看下面的showmessage(TComponent(Sender).Name);一句
可見 其彙編代碼如下:
Mov  eax,[edx+$08]
Call  ShowMessage
和以前相比 Mov  eax,[eax+$08] 變成了 Mov  eax,[edx+$08]
此時,然後運行,得到結果爲TComponent(Sender).Name 的值爲Button1
而下面的代碼
if Frm is TForm then
TForm(Frm).Close;
則充分證明了EAX的值是 Form1,則說明了對象方法在調用的時候會傳遞一個隱含的Self指針,而該指針的值在EAX中.
由於Delphi中參數的傳遞爲
EAX  第一個參數
EDX  第二個參數
ECX  第三個參數
所以可知道,真正的觸發事件的按扭對象存放在EDX中.

所以我們可以得到如下結論
在 按扭的單擊事件中,
TNotifyEvent = procedure(Sender: TObject) of object;
其真正的實體爲procedure(當前聲明引起的對象Self,Sender: TObject)
所以 Button.OnClick的時候,其實傳遞方式如下
Button1.OnClick(Self,Sender);
其他事件方法等,依次類推.

然後根據該結論,則我們可以不在受
爲Form中的某個控件對象指定事件方法的時候受到 Of Object 那個東西的限制,可以將事件方法指定到任何地方了。只要注意,該方法對應的參數要比其事件方法(Of Object)指定的方法多一個參數聲明,則可
比如,此時,我們拿窗體關閉事件做文章:
新建一個按扭,寫代碼

窗體關閉的事件方法爲
TCloseEvent = procedure(Sender: TObject;Var action: TCloseAction) of Object;

從上面結論我們知道可以聲明一個外部函數,該外部函數的參數要比TCloseEvent的參數多一個Self指針的,所以我們聲明如下:
procedure MyCloseEvent(Frm: TForm;Sender: TObject;var Action: TCloseAction);
Frm則是外部在窗體關閉的時候,傳遞的隱含指針Self

該函數整體代碼如下:

點一下,新建的按扭之後,看看是否還可以關閉窗體!!

通過彙編來處理

//由於前面我們已經證明了,在類之中的方法,其傳遞的時候,都會有一個隱含的參數Self,所以,該段彙編代碼中我們就知道了Event參數對應應該是Edx寄存器,而不是Eax寄存器了。然後,後面有[ebx+$2d8]這樣的內容,這個是窗體 OnClose事件所在位置的地址。可以通過CpuView窗口查看得到,暫時沒有想到如何通過指定一個 事件名稱來得到該事件在內存中的地址。如果這樣的話,那麼則可以寫一個函數
ReSetObjEvent(EventName: string;EventValue: pointer);
先通過EventName找到事件地址,然後再通過上面的則可以寫出一個簡單通俗易懂的公用函數了。
否則只能通過傳遞地址,根據改變地址中的值來修改事件函數的指向了。如下:
寫一個專門用來重設置事件方法的函數如下:

其實也就是 改變存放事件方法指針的內存塊的數據值,使其變成另一個值。
注意,參數一指定爲存放舊事件方法指針的內存地址,所以他應該是一個指針的指針了。
      參數二指定爲事件方法指針值。
調用方法如下:
比如,指定窗體的 OnClose事件方法指針爲窗體類外部定義的函數。
  ReSetObjEvent(@(integer(@Form1.onClose)),@MyCloseEvent)
例如:

續言:
  以上在Delphi7下測試通過,至於2007下,我測試,也傳遞了一個隱含參數,但是該隱含參數不是Self

再論:
經過Cnpack的劉嘯提醒之後,發現了Delphi7下測試通過,而2007下不通過的原因是在於D7下如下聲明:

此時2007下該段程序運行不能通過而D7編譯運行可以通過,實在確實是一個巧合了。
通過提示得知,TCloseEvent在Delphi中被稱爲對象方法,而對象方法
在 Delphi 中用 procedure(Sender: TObject) of object; 這種格式聲明的 事件(Event) 類型實際上是同時包含有對象和函數的記錄。我們可以把一個 TNotifyEvent 的變量強制轉換成 TMethod:
TMethod = record
  Code, Data: Pointer;
end;

例如我們聲明瞭一個方法 MainForm.BtnClick 並將它賦值給 btn1.OnClick 事件,實際上是將 MainForm 對象和 BtnClick 方法地址分別作爲 TMethod 結構的 Data 和 Code 成員賦值給 btn1.OnClick 事件屬性。當 btn1 按鈕調用這個 BtnClick 事件時,實際上是將 TMethod 結構的 Data 作爲第一個參數去調用 Code 函數。

我們可以編寫下面的代碼:

這樣就可以將一個普通函數賦值給對象事件屬性了。

我們再來看看 TLanguages.Create 的代碼:

在 Win32 SDK 中可以查到 EnumSystemLocales 要求的回調格式是:
BOOL CALLBACK EnumLocalesProc(
    LPTSTR lpLocaleString         // pointer to locale identifier string
);

而 SysUtils 中的方法聲明:
  TLanguages = class
    ...
    function LocalesCallback(LocaleID: PChar): Integer; stdcall;
    ...
  end;

顯然,我們是無法將 LocalesCallback 這個方法直接傳遞給 EnumSystemLocales 的,因爲 LocalesCallback 的函數形式聲明實際上是:
function LocalesCallback(Self: TLanguages; LocaleID: PChar): Integer; stdcall;
比 EnumLocalesProc 多出來一個參數。

所以在 TLanguages.Create 中,使用了 Callback 結構變量來生成一小段動態代碼。這段代碼是構造在堆棧中的(局部變量),轉換成彙編是:

將 CallbackThunk 作爲臨時的回調函數傳遞給 EnumSystemLocales 是合法的。當回調被執行時,前面那小段代碼動態修改了堆棧的內容,將本來只有一個參數的調用,變成了兩個參數,從而實現了回調與對象方法的轉換。

但是,正如 Passion 在前面提到的,由於這小塊臨時代碼是放在堆棧中的,而 Win2003 的 DEP 限制了在堆棧中執行代碼,導致事實上回調函數並沒有被正確地調用。

Borland 程序員也看到了這個問題,所以在 BDS 2006 中,這部分代碼的實現修改成:

通過聲明一個臨時變量和轉換函數,來取代原來的方法,就不會有 DEP 衝突了。

附帶說一下 Forms 單元中的 MakeObjectInstance。這個函數用來生成一塊動態代碼,將 Windows 的窗體消息處理過程轉換爲 Delphi 的對象方法調用。在 TWinControl 等需要有消息處理支持的地方用到。該函數也是採用了前面類似的方法,不過不同的是,由於這些轉換調用是長期的,所以那些動態生成的代碼被放到了標識爲可執行的動態空間中了,所以在 Win2003 的 DEP 下仍然可以正常工作:

劉嘯
例如我們聲明瞭一個方法 MainForm.BtnClick 並將它賦值給 btn1.OnClick 事件,實際上是將 MainForm 對象和 BtnClick 方法地址分別作爲 TMethod 結構的 Data 和 Code 成員賦值給 btn1.OnClick 事件屬性。“當 btn1 按鈕調用這個 BtnClick 事件時,實際上是將 TMethod 結構的 Data 作爲第一個參數去調用 Code 函數。”

這裏關於調用的似乎值得討論一下。記得這個事件OnClick在被調用時是這麼寫的:

if Assigned(FOnClick) then
  FOnClick(Self);

第一個參數是調用時傳入的是Button自身,也就是Button的Self,而不是原本這個Method裏頭的Data吧?
我的理解是,Method的Data只是用來說明這個方法屬於哪個對象實例,但被調的時候似乎沒發揮作用。所以自行捏造一個TMethod的data部分,然後給OnClick等賦值再調用也能成功。

 

周勁羽
if Assigned(FOnClick) then
  FOnClick(Self);
這裏傳入的 Self 是 TNotifyEvent 中的 Sender: TObject 參數,而作爲對象方法的 OnClick,實際上需要兩個參數,第一個隱藏的 Self 是 OnClick 方法所從屬的對象,第二個纔是 Sender。

比如 Button 調用 FOnClick 時,這個 FOnClick 指向的方法可能是從屬於某個 Form 的 OnBtnClick。類自己是不保存對象實例的,直接調用 Form.OnBtnClick 時 Self 是 Form 這個實例,而通過 Button.FOnClick 調用到 Form.OnBtnClick 方法時,OnBtnClick 的 Self 從哪裏來?當然就是用 TMethod.Data 傳過去的嘍。而這個 TMethod.Data 則是在賦值 Button.OnClick := Form.OnBtnClick 時的 Form 對象。
FOnClick時傳入的Self是作爲Sender的,而BtnOnClick方法裏頭所引用的Self是Form實例,後者的Self應該是從Data裏頭來的。

由上可得到一個通用函數,用來動態設置對象事件:

參數一: 指定爲 存放事件指針的內存地址值的地址指針,所以爲一個指針的指針
參數二: 指定爲新的事件函數地址指針
參數三: 指定爲重設事件的修改者,用來隱射對象方法的隱含參數Self

調用方法:
  ReSetObjEvent(@integer(@self.OnClose),@MyCloseEvent,self);

例:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章