VCL Delphi Windows消息 窗口函數 注冊 MakeObject (轉自網絡)
MakeObjectInstance函數是Delphi函數,這個函數的申明部分是這樣的:
function MakeObjectInstance(Method: TWndMethod): Pointer;
和它相對應的另一個函數叫做FreeObjectInstance,它的申明部分是這樣的:
procedure FreeObjectInstance(ObjectInstance: Pointer);
MakeObjectInstance函數在Delphi的幫助文件中并沒有,但是它還是在Interface中申明了,
也就是說我們可以使用它。
要完全解釋清楚這個函數的作用不是三言兩語的事情,
現貼上一篇文章:
關鍵字:
VCL Delphi Windows消息 窗口函數 注冊 MakeObject 匯編 貼文時間
2001-7-9 17:16:31 文章類型:
原作 給貼子投票
投票
cheka 原作 出處:
VCL HardCore ——VCL窗口函數注冊機制研究手記,兼與MFC比較
By cheka cheka@yeah.net (轉載請保留此信息)
這個名字起的有些聳人聽聞,無他意,只為吸引眼球而已,如果您對下列關鍵詞有興趣,希望不要錯過本文:
1. VCL可視組件在內存中的分頁式管理;
2. 讓系統回調類的成員方法
3. Delphi 中匯編指令的使用
4. Hardcore
5. 第4條是騙你的
我們知道Windows平臺上的GUI程序都必須遵循Windows的消息響應機制,可以簡單概括如下,所有的窗口控件都向系統注冊自身的窗口函數,運行期間消息可被指派至特定窗口控件的窗口函數處理。對消息相應機制做這樣的概括有失嚴密,請各位見諒,我想趕緊轉向本文重點,即在利用Object Pascali或是C++這樣的面向對象語言編程中,如何把一個類的成員方法向系統注冊以供回調。
在注冊窗口類即調用RegisterClass函數時,我們向系統傳遞的是一個WindowProc 類型的函數指針
WindowProc 的定義如下
LRESULT CALLBACK WindowProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
如果我們有一個控件類,它擁有看似具有相同定義的成員方法TMyControl.WindowProc,可是卻不能夠將它的首地址作為lpfnWndProc參數傳給RegisterClass,道理很簡單,因為Delphi中所有類成員方法都有一個隱含的參數,也就是Self,因此無法符合標準 WindowProc 的定義。
那么,在VCL中,控件向系統注冊時究竟傳遞了一個什么樣的窗口指針,同時通過這個指針又是如何調到各個類的事件響應方法呢?我先賣個關子,先看看MFC是怎么做的。
在調查MFC代碼之前,我作過兩種猜想:
一,作注冊用的函數指針指向的是一個類的靜態方法,
靜態方法同樣不需要隱含參數 this (對應 Delphi中的 Self ,不過Object Pascal不支持靜態方法)
二,作注冊用的函數指針指向的是一個全局函數,這當然最傳統,沒什么好說的。
經過簡單的跟蹤,我發現MFC中,全局函數AfxWndProc是整個MFC程序處理消息的“根節點”,也就是說,所有的消息都由它指派給不同控件的消息響應函數,也就是說,所有的窗口控件向系統注冊的窗口函數很可能就是 AfxWndProc (抱歉沒做深入研究,如果不對請指正)。而AfxWndProc 是如何調用各個窗口類的WndProc呢?
哈哈,MFC用了一種很樸素的機制,相比它那么多稀奇古怪的宏來說,這種機制相當好理解:使用一個全局的Map數據結構來維護所有的窗口對象和Handle(其中Handle為鍵值),然后AfxWndProc根據Handle來找出唯一對應的窗口對象(使用靜態函數CWnd::FromHandlePermanent(HWND hWnd) ),然后調用其WndProc,注意WndProc可是虛擬方法,因此消息能夠正確到達所指定窗口類的消息響應函數并被處理。
于是我們有理由猜想VCL也可能采用相同的機制,畢竟這種方式實現起來很簡單。我確實是這么猜的,不過結論是我錯了......
開場秀結束,好戲正式上演。
在Form1上放一個Button(缺省名為Button1),在其OnClick事件中寫些代碼,加上斷點,F9運行,當停留在斷點上時,打開Call Stack窗口(View->Debug Window->Call Stack, 或者按Ctrl-Alt-S )可看到調用順序如下(從底往上看,stack嘛)
( 如果你看到的 Stack 和這個不一致,請打開DCU 調試開關 Project->Options->Compiler->Use Debug DCUs, 這個開關如果不打開,是沒法調試VCL源碼的 )
TForm1.Button1Click(???)
TControl.Click
TButton.Click
TButton.CNCommand ((48401, 3880, 0, 3880, 0))
TControl.WndProc ((48401, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TWinControl.WndProc ((48401, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TButtonControl.WndProc ((48401, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TControl.Perform (48401,3880,3880)
DoControlMsg (3880,(no value))
TWinControl.WMComman d((273, 3880, 0, 3880, 0))
TCustomForm.WMCommand ((273, 3880, 0, 3880, 0))
TControl.WndProc ((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TWinControl.WndProc((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TCustomForm.WndProc ((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TWinControl.MainWndProc ((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
StdWndProc (3792,273,3880,3880)
可見 StdWndProc 看上去象是扮演了MFC中 AfxWndProc 的角色,不過我們先不談它,如果你抑制不住好奇心,可以提前去看它的源碼,在Forms.pas中,看到了么? 是不是特~~~~別有趣阿。
實際上,VCL在RegisterClass時傳遞的窗口函數指針并非指向StdWndProc。那是什么呢?
我跟,我跟,我跟跟跟,終于在Controls.pas的TWindowControl的實現代碼中
(procedure TWinControl.CreateWnd;) 看到了RegisterClass的調用,hoho,終于找到組織了......別忙,發現了沒,這時候注冊的窗口函數是InitWndProc,看看它的定義,嗯,符合標準,再去瞧瞧代碼都干了些什么。
發現這句:
SetWindowLong(HWindow, GWL_WNDPROC,Longint(CreationControl.FObjectInstance));
我Faint,搞了半天InitWndProc初次被調用(對每一個Wincontrol來說)就把自個兒給換了,新上崗的是FObjectInstance。下面還有一小段匯編,是緊接著調用FObjectInstance的,調用的理由不奇怪,因為以后調用FObjectInstace都由系統CallBack了,但現在還得勞InitWndProc的大駕去call。調用的方式有些講究,不過留給您看完這篇文章后自個兒琢磨去吧。
接下來只能繼續看FObjectInstance是什么東東,它定義在 TWinControl 的 Private 段,是個Pointer也就是個普通指針,當什么使都行,你跟Windows說它就是 WndProc 型指針 Windows 拿你也沒轍。
FObjectInstance究竟指向何處呢,鏡頭移向 TWincontrol 的構造函數,這是FObjectInstance初次被賦值的地方。 多余的代碼不用看,焦點放在這句上
FObjectInstance := MakeObjectInstance(MainWndProc);
可以先告訴您,MakeObjectInstance是本主題最精彩之處,但是您現在只需知道FObjectInstance“指向了”MainWndProc,也就是說通過某種途徑VCL把每個MainWndProc作為窗口函數注冊了,先證明容易的,即 MainWndProc 具備窗口函數的功能,來看代碼:
( 省去異常處理 )
procedure TWinControl.MainWndProc(var Message: TMessage);
begin
WindowProc(Message);
FreeDeviceContexts;
FreeMemoryContexts;
end;
FreeDeviceContexts; 和 FreeMemoryContexts 是保證VCL線程安全的,不在本文討論之列,只看WindowProc(Message); 原來 MainWndProc 把消息委托給了方法 WindowProc處理,注意到 MainWndProc 不是虛擬方法,而 WindowProc 則是虛擬的,了解 Design Pattern 的朋友應該點頭了,嗯,是個 Template Method , 很自然也很經典的用法,這樣一來所有的消息都能準確到達目的地,也就是說從功能上看 MainWndProc 確實可以充作窗口函數。您現在可以回顧一下MFC的 AfxWindowProc 的做法,同樣是利用對象的多態性,但是兩種方式有所區別。
是不是有點亂了呢,讓我們總結一下,VCL 注冊窗口函數分三步:
1. [ TWinControl.Create ]
FObjectInstance 指向了 MainWndProc
2. [ TWinControl.CreateWnd ]
WindowClass.lpfnWndProc 值為 @InitWndProc;
調用Windows.RegisterClass(WindowClass)向系統注冊
3. [ InitWndProc 初次被Callback時 ]
SetWindowLong(HWindow, GWL_WNDPROC, Longint(CreationControl.FObjectInstance))
窗口函數被偷梁換柱,從此 InitWndProc 退隱江湖
(注意是對每個TWinControl控件來說,InitWndProc 只被調用一次)
前面說過,非靜態的類方法是不能注冊成為窗口函數的,特別是Delphi中根本沒有靜態類方法,那么MainWndProc 也不能有特權(當然寶蘭可以為此在編譯器上動點手腳,如果他們不怕成為嘔像的話)。
那么,那么,您應該意識到了,在幕后操縱一切的,正是......
背景打出字幕
超級巨星:麥克奧布吉特因斯坦斯
(MakeObjectInstance)
天空出現閃電,哦耶,主角才剛剛亮相。
廢話不說,代碼伺候:
( 原始碼在 Form.pas 中,“{}”中是原始的注釋,而“ file://”/ 后的是我所加,您可以直接就注釋代碼,也可以先看我下面的評論,再回頭啃code )
// 共占 13 Bytes
type
PObjectInstance = ^TObjectInstance;
TObjectInstance = packed record
Code: Byte; // 1 Byte
Offset: Integer; // 4 Byte
case Integer of
0: (Next: PObjectInstance); // 4 Byte
1: (Method: TWndMethod); // 8 Byte
// TWndMethod 是一個指向對象方法的指針,
// 事實上是一個指針對,包含方法指針以
// 及一個對象的指針(即Self )
end;
// 313是滿足整個TInstanceBlock的大小不超過4096的最大值
InstanceCount = 313;
// 共占 4079 Bytes
type
PInstanceBlock = ^TInstanceBlock;
TInstanceBlock = packed record
Next: PInstanceBlock; // 4 Bytes
Code: array[1..2] of Byte; // 2 Bytes
WndProcPtr: Pointer; // 4 Bytes
Instances: array[0..InstanceCount] of TObjectInstance; 313 * 13 = 4069
end;
function CalcJmpOffset(Src, Dest: Pointer): Longint;
begin
Result := Longint(Dest) - (Longint(Src) + 5);
end;
function MakeObjectInstance(Method: TWndMethod): Pointer;
const
BlockCode: array[1..2] of Byte = (
$59, { POP ECX }
$E9); { JMP StdWndProc } // 實際上只有一個JMP
PageSize = 4096;
var
Block: PInstanceBlock;
Instance: PObjectInstance;
begin
// InstFreeList = nil 表明一個Instance block已被占滿,于是需要為一個新
// Instance block分配空間,一個個Instance block通過PinstanceBlock中的
// Next 指針相連,形成一個鏈表,其頭指針為InstBlockList
if InstFreeList = nil then
begin
// 為Instance block分配虛擬內存,并指定這塊內存為可讀寫并可執行
// PageSize 為4096。
Block := VirtualAlloc(nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
Block^.Next := InstBlockList;
Move(BlockCode, Block^.Code, SizeOf(BlockCode));
Block^.WndProcPtr := Pointer(CalcJmpOffset(@Block^.Code[2], @StdWndProc));
// 以下代碼建立一個Instance的鏈表
Instance := @Block^.Instances;
repeat
Instance^.Code := $E8; { CALL NEAR PTR Offset }
file://算/出相對 jmp StdWndProc指令的偏移量,放在$E8的后面
Instance^.Offset := CalcJmpOffset(Instance, @Block^.Code);
Instance^.Next := InstFreeList;
InstFreeList := Instance;
// 必須有這步,讓Instance指針移至當前instance子塊的底部
Inc(Longint(Instance), SizeOf(TObjectInstance));
// 判斷一個Instance block是否已被構造完畢
until Longint(Instance) - Longint(Block) >= SizeOf(TInstanceBlock);
InstBlockList := Block;
end;
Result := InstFreeList;
Instance := InstFreeList;
InstFreeList := Instance^.Next;
Instance^.Method := Method;
end;
不要小看這區區幾十行代碼的能量,就是它們對 VCL 的可視組件進行了分頁式管理,(代碼中對兩個鏈表進行操作,InstanceBlock 中有 ObjectInstance 的鏈表,而一個個InstanceBlock 又構成一個鏈表 )一個 InstanceBlock 為一頁,有4096 字節,雖然 InstanceBlock 實際使用的只有 4079 字節,不過為了 Alignment ,就加了些 padding 湊滿 4096 。從代碼可見每一頁中可容納 313 個所謂的ObjectInstance,如果望文生義很容易將這個 ObjectInstance 誤解為對象實例,其實不然,每個ObjectInstance 其實是一小段可執行代碼,而這些可執行代碼不是編譯期間生成的,也不是象虛擬函數那樣滯后聯編,而根本就是MakeObjectInstance 在運行期間“創作”的(天哪)! 也就是說,MakeObjectInstance 將所有的可視VCL組件 改造成了一頁頁的可執行代碼區域,是不是很了不起呢。
不明白ObjectInstance所對應的代碼是做什么的么?沒關系,一起來看
call - - - - - - - - - - - > pop ECX // 在call 之前,下一個指令地址會被壓棧
@MainWndProc // 緊接著執行pop ECX, 為何這么做呢?
@Object(即Self) // 前面注釋中提過
答案在 StdWndProc 的代碼中,要命哦,全是匯編,可是無限風光在險峰,硬著頭皮闖一回吧。
果不其然,我們發現其中用到了ECX
function StdWndProc(Window: HWND; Message, WParam: Longint;
LParam: Longint): Longint; stdcall; assembler;
asm
XOR EAX,EAX
PUSH EAX
PUSH LParam
PUSH WParam
PUSH Message
MOV EDX,ESP
MOV EAX,[ECX].Longint[4] // 相當于 MOV EAX, [ECX+4] ( [ECX+4] 是什么?就是Self )
CALL [ECX].Pointer // 相當于 CALL [ECX] , 也就是調用 MainWndProc
ADD ESP,12
POP EAX
end;
這段匯編中在調用MainWndProc前作了些參數傳遞的工作,由于MainWndProc 的定義如下
procedure TwinControl..MainWndProc(var Message: TMessage);
根據Delphi 的約定,這種情況下隱函數Self 作為第一個參數,放入EAX 中,TMessage 結構的指針作為第二個參數,放入EDX中,而Message的指針從哪兒來呢?我們看到在連續幾個 Push 之后,程序已經在堆棧中構造了一個TMessage 結構,而這時的ESP 當然就是這個結構的指針,于是將它賦給EDX 。如果您不熟悉這方面的約定,可以參考Delphi 的幫助Object Pascal Refrence -> Program Control。
現在真相大白,Windows 消息百轉千折終于傳進MainWndProc , 不過這一路也可謂相當精彩,MakeObject這一函數自然是居功至偉, StdWndProc 也同樣是幕后英雄,讓我們把 MakeObjectInstance 作出的代碼和StdWndProc 連接起來,哦,堪稱鬼斧神工。
( 圖片無法顯示,請下載全文)
就此在總結一下, FobjectInstance 被VCL 注冊為窗口函數,而實際上 FObjectInstance 并不實際指向某個函數,而是指向一個ObjectInstance, 而后者我們已經知道是一系列相接的可執行代碼段當中的一塊,當系統需要將 FObjectInstance 當做窗口函數作為回調時,實際進入了ObjectInstance 所在的代碼段,然后幾番跳躍騰挪(一個call 加一個 jump )來到StdWndProc ,StdWndProc 的主要功用在于將Self 指針壓棧,并把Windows的消息包裝成Delphi的TMessage 結構,如此才能成功調用到TWinControl類的成員方法 MainWndProc, 消息一旦進入MainWndProc 便可以輕車熟路一路高唱小曲來到各個對象轉屬的WndProc , 從此功德圓滿。
后記:
個人感覺在這一技術上VCL 要比MFC 效率高出不少,后者每次根據窗口句柄來檢索相對應的窗口對象指針頗為費時,同時MakeObject 的代碼也相當具有參考價值,有沒有想過讓你自己的程序在內存中再開一堆可執行代碼?
所有的代碼是基于Delphi5的,可能與其余版本有所出入,但相信不會很大。
整個星期六和星期天我都花在寫作此文上了(連調試帶寫字), 不過水平所限,難免有所錯誤與表達不周,但愿不至以己昏昏令人昏昏,歡迎來信探討指教 cheka@yeah.net , thanx
function MakeObjectInstance(Method: TWndMethod): Pointer;
和它相對應的另一個函數叫做FreeObjectInstance,它的申明部分是這樣的:
procedure FreeObjectInstance(ObjectInstance: Pointer);
MakeObjectInstance函數在Delphi的幫助文件中并沒有,但是它還是在Interface中申明了,
也就是說我們可以使用它。
要完全解釋清楚這個函數的作用不是三言兩語的事情,
現貼上一篇文章:
關鍵字:
VCL Delphi Windows消息 窗口函數 注冊 MakeObject 匯編 貼文時間
2001-7-9 17:16:31 文章類型:
原作 給貼子投票
投票
cheka 原作 出處:
VCL HardCore ——VCL窗口函數注冊機制研究手記,兼與MFC比較
By cheka cheka@yeah.net (轉載請保留此信息)
這個名字起的有些聳人聽聞,無他意,只為吸引眼球而已,如果您對下列關鍵詞有興趣,希望不要錯過本文:
1. VCL可視組件在內存中的分頁式管理;
2. 讓系統回調類的成員方法
3. Delphi 中匯編指令的使用
4. Hardcore
5. 第4條是騙你的
我們知道Windows平臺上的GUI程序都必須遵循Windows的消息響應機制,可以簡單概括如下,所有的窗口控件都向系統注冊自身的窗口函數,運行期間消息可被指派至特定窗口控件的窗口函數處理。對消息相應機制做這樣的概括有失嚴密,請各位見諒,我想趕緊轉向本文重點,即在利用Object Pascali或是C++這樣的面向對象語言編程中,如何把一個類的成員方法向系統注冊以供回調。
在注冊窗口類即調用RegisterClass函數時,我們向系統傳遞的是一個WindowProc 類型的函數指針
WindowProc 的定義如下
LRESULT CALLBACK WindowProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
如果我們有一個控件類,它擁有看似具有相同定義的成員方法TMyControl.WindowProc,可是卻不能夠將它的首地址作為lpfnWndProc參數傳給RegisterClass,道理很簡單,因為Delphi中所有類成員方法都有一個隱含的參數,也就是Self,因此無法符合標準 WindowProc 的定義。
那么,在VCL中,控件向系統注冊時究竟傳遞了一個什么樣的窗口指針,同時通過這個指針又是如何調到各個類的事件響應方法呢?我先賣個關子,先看看MFC是怎么做的。
在調查MFC代碼之前,我作過兩種猜想:
一,作注冊用的函數指針指向的是一個類的靜態方法,
靜態方法同樣不需要隱含參數 this (對應 Delphi中的 Self ,不過Object Pascal不支持靜態方法)
二,作注冊用的函數指針指向的是一個全局函數,這當然最傳統,沒什么好說的。
經過簡單的跟蹤,我發現MFC中,全局函數AfxWndProc是整個MFC程序處理消息的“根節點”,也就是說,所有的消息都由它指派給不同控件的消息響應函數,也就是說,所有的窗口控件向系統注冊的窗口函數很可能就是 AfxWndProc (抱歉沒做深入研究,如果不對請指正)。而AfxWndProc 是如何調用各個窗口類的WndProc呢?
哈哈,MFC用了一種很樸素的機制,相比它那么多稀奇古怪的宏來說,這種機制相當好理解:使用一個全局的Map數據結構來維護所有的窗口對象和Handle(其中Handle為鍵值),然后AfxWndProc根據Handle來找出唯一對應的窗口對象(使用靜態函數CWnd::FromHandlePermanent(HWND hWnd) ),然后調用其WndProc,注意WndProc可是虛擬方法,因此消息能夠正確到達所指定窗口類的消息響應函數并被處理。
于是我們有理由猜想VCL也可能采用相同的機制,畢竟這種方式實現起來很簡單。我確實是這么猜的,不過結論是我錯了......
開場秀結束,好戲正式上演。
在Form1上放一個Button(缺省名為Button1),在其OnClick事件中寫些代碼,加上斷點,F9運行,當停留在斷點上時,打開Call Stack窗口(View->Debug Window->Call Stack, 或者按Ctrl-Alt-S )可看到調用順序如下(從底往上看,stack嘛)
( 如果你看到的 Stack 和這個不一致,請打開DCU 調試開關 Project->Options->Compiler->Use Debug DCUs, 這個開關如果不打開,是沒法調試VCL源碼的 )
TForm1.Button1Click(???)
TControl.Click
TButton.Click
TButton.CNCommand ((48401, 3880, 0, 3880, 0))
TControl.WndProc ((48401, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TWinControl.WndProc ((48401, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TButtonControl.WndProc ((48401, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TControl.Perform (48401,3880,3880)
DoControlMsg (3880,(no value))
TWinControl.WMComman d((273, 3880, 0, 3880, 0))
TCustomForm.WMCommand ((273, 3880, 0, 3880, 0))
TControl.WndProc ((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TWinControl.WndProc((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TCustomForm.WndProc ((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
TWinControl.MainWndProc ((273, 3880, 3880, 0, 3880, 0, 3880, 0, 0, 0))
StdWndProc (3792,273,3880,3880)
可見 StdWndProc 看上去象是扮演了MFC中 AfxWndProc 的角色,不過我們先不談它,如果你抑制不住好奇心,可以提前去看它的源碼,在Forms.pas中,看到了么? 是不是特~~~~別有趣阿。
實際上,VCL在RegisterClass時傳遞的窗口函數指針并非指向StdWndProc。那是什么呢?
我跟,我跟,我跟跟跟,終于在Controls.pas的TWindowControl的實現代碼中
(procedure TWinControl.CreateWnd;) 看到了RegisterClass的調用,hoho,終于找到組織了......別忙,發現了沒,這時候注冊的窗口函數是InitWndProc,看看它的定義,嗯,符合標準,再去瞧瞧代碼都干了些什么。
發現這句:
SetWindowLong(HWindow, GWL_WNDPROC,Longint(CreationControl.FObjectInstance));
我Faint,搞了半天InitWndProc初次被調用(對每一個Wincontrol來說)就把自個兒給換了,新上崗的是FObjectInstance。下面還有一小段匯編,是緊接著調用FObjectInstance的,調用的理由不奇怪,因為以后調用FObjectInstace都由系統CallBack了,但現在還得勞InitWndProc的大駕去call。調用的方式有些講究,不過留給您看完這篇文章后自個兒琢磨去吧。
接下來只能繼續看FObjectInstance是什么東東,它定義在 TWinControl 的 Private 段,是個Pointer也就是個普通指針,當什么使都行,你跟Windows說它就是 WndProc 型指針 Windows 拿你也沒轍。
FObjectInstance究竟指向何處呢,鏡頭移向 TWincontrol 的構造函數,這是FObjectInstance初次被賦值的地方。 多余的代碼不用看,焦點放在這句上
FObjectInstance := MakeObjectInstance(MainWndProc);
可以先告訴您,MakeObjectInstance是本主題最精彩之處,但是您現在只需知道FObjectInstance“指向了”MainWndProc,也就是說通過某種途徑VCL把每個MainWndProc作為窗口函數注冊了,先證明容易的,即 MainWndProc 具備窗口函數的功能,來看代碼:
( 省去異常處理 )
procedure TWinControl.MainWndProc(var Message: TMessage);
begin
WindowProc(Message);
FreeDeviceContexts;
FreeMemoryContexts;
end;
FreeDeviceContexts; 和 FreeMemoryContexts 是保證VCL線程安全的,不在本文討論之列,只看WindowProc(Message); 原來 MainWndProc 把消息委托給了方法 WindowProc處理,注意到 MainWndProc 不是虛擬方法,而 WindowProc 則是虛擬的,了解 Design Pattern 的朋友應該點頭了,嗯,是個 Template Method , 很自然也很經典的用法,這樣一來所有的消息都能準確到達目的地,也就是說從功能上看 MainWndProc 確實可以充作窗口函數。您現在可以回顧一下MFC的 AfxWindowProc 的做法,同樣是利用對象的多態性,但是兩種方式有所區別。
是不是有點亂了呢,讓我們總結一下,VCL 注冊窗口函數分三步:
1. [ TWinControl.Create ]
FObjectInstance 指向了 MainWndProc
2. [ TWinControl.CreateWnd ]
WindowClass.lpfnWndProc 值為 @InitWndProc;
調用Windows.RegisterClass(WindowClass)向系統注冊
3. [ InitWndProc 初次被Callback時 ]
SetWindowLong(HWindow, GWL_WNDPROC, Longint(CreationControl.FObjectInstance))
窗口函數被偷梁換柱,從此 InitWndProc 退隱江湖
(注意是對每個TWinControl控件來說,InitWndProc 只被調用一次)
前面說過,非靜態的類方法是不能注冊成為窗口函數的,特別是Delphi中根本沒有靜態類方法,那么MainWndProc 也不能有特權(當然寶蘭可以為此在編譯器上動點手腳,如果他們不怕成為嘔像的話)。
那么,那么,您應該意識到了,在幕后操縱一切的,正是......
背景打出字幕
超級巨星:麥克奧布吉特因斯坦斯
(MakeObjectInstance)
天空出現閃電,哦耶,主角才剛剛亮相。
廢話不說,代碼伺候:
( 原始碼在 Form.pas 中,“{}”中是原始的注釋,而“ file://”/ 后的是我所加,您可以直接就注釋代碼,也可以先看我下面的評論,再回頭啃code )
// 共占 13 Bytes
type
PObjectInstance = ^TObjectInstance;
TObjectInstance = packed record
Code: Byte; // 1 Byte
Offset: Integer; // 4 Byte
case Integer of
0: (Next: PObjectInstance); // 4 Byte
1: (Method: TWndMethod); // 8 Byte
// TWndMethod 是一個指向對象方法的指針,
// 事實上是一個指針對,包含方法指針以
// 及一個對象的指針(即Self )
end;
// 313是滿足整個TInstanceBlock的大小不超過4096的最大值
InstanceCount = 313;
// 共占 4079 Bytes
type
PInstanceBlock = ^TInstanceBlock;
TInstanceBlock = packed record
Next: PInstanceBlock; // 4 Bytes
Code: array[1..2] of Byte; // 2 Bytes
WndProcPtr: Pointer; // 4 Bytes
Instances: array[0..InstanceCount] of TObjectInstance; 313 * 13 = 4069
end;
function CalcJmpOffset(Src, Dest: Pointer): Longint;
begin
Result := Longint(Dest) - (Longint(Src) + 5);
end;
function MakeObjectInstance(Method: TWndMethod): Pointer;
const
BlockCode: array[1..2] of Byte = (
$59, { POP ECX }
$E9); { JMP StdWndProc } // 實際上只有一個JMP
PageSize = 4096;
var
Block: PInstanceBlock;
Instance: PObjectInstance;
begin
// InstFreeList = nil 表明一個Instance block已被占滿,于是需要為一個新
// Instance block分配空間,一個個Instance block通過PinstanceBlock中的
// Next 指針相連,形成一個鏈表,其頭指針為InstBlockList
if InstFreeList = nil then
begin
// 為Instance block分配虛擬內存,并指定這塊內存為可讀寫并可執行
// PageSize 為4096。
Block := VirtualAlloc(nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
Block^.Next := InstBlockList;
Move(BlockCode, Block^.Code, SizeOf(BlockCode));
Block^.WndProcPtr := Pointer(CalcJmpOffset(@Block^.Code[2], @StdWndProc));
// 以下代碼建立一個Instance的鏈表
Instance := @Block^.Instances;
repeat
Instance^.Code := $E8; { CALL NEAR PTR Offset }
file://算/出相對 jmp StdWndProc指令的偏移量,放在$E8的后面
Instance^.Offset := CalcJmpOffset(Instance, @Block^.Code);
Instance^.Next := InstFreeList;
InstFreeList := Instance;
// 必須有這步,讓Instance指針移至當前instance子塊的底部
Inc(Longint(Instance), SizeOf(TObjectInstance));
// 判斷一個Instance block是否已被構造完畢
until Longint(Instance) - Longint(Block) >= SizeOf(TInstanceBlock);
InstBlockList := Block;
end;
Result := InstFreeList;
Instance := InstFreeList;
InstFreeList := Instance^.Next;
Instance^.Method := Method;
end;
不要小看這區區幾十行代碼的能量,就是它們對 VCL 的可視組件進行了分頁式管理,(代碼中對兩個鏈表進行操作,InstanceBlock 中有 ObjectInstance 的鏈表,而一個個InstanceBlock 又構成一個鏈表 )一個 InstanceBlock 為一頁,有4096 字節,雖然 InstanceBlock 實際使用的只有 4079 字節,不過為了 Alignment ,就加了些 padding 湊滿 4096 。從代碼可見每一頁中可容納 313 個所謂的ObjectInstance,如果望文生義很容易將這個 ObjectInstance 誤解為對象實例,其實不然,每個ObjectInstance 其實是一小段可執行代碼,而這些可執行代碼不是編譯期間生成的,也不是象虛擬函數那樣滯后聯編,而根本就是MakeObjectInstance 在運行期間“創作”的(天哪)! 也就是說,MakeObjectInstance 將所有的可視VCL組件 改造成了一頁頁的可執行代碼區域,是不是很了不起呢。
不明白ObjectInstance所對應的代碼是做什么的么?沒關系,一起來看
call - - - - - - - - - - - > pop ECX // 在call 之前,下一個指令地址會被壓棧
@MainWndProc // 緊接著執行pop ECX, 為何這么做呢?
@Object(即Self) // 前面注釋中提過
答案在 StdWndProc 的代碼中,要命哦,全是匯編,可是無限風光在險峰,硬著頭皮闖一回吧。
果不其然,我們發現其中用到了ECX
function StdWndProc(Window: HWND; Message, WParam: Longint;
LParam: Longint): Longint; stdcall; assembler;
asm
XOR EAX,EAX
PUSH EAX
PUSH LParam
PUSH WParam
PUSH Message
MOV EDX,ESP
MOV EAX,[ECX].Longint[4] // 相當于 MOV EAX, [ECX+4] ( [ECX+4] 是什么?就是Self )
CALL [ECX].Pointer // 相當于 CALL [ECX] , 也就是調用 MainWndProc
ADD ESP,12
POP EAX
end;
這段匯編中在調用MainWndProc前作了些參數傳遞的工作,由于MainWndProc 的定義如下
procedure TwinControl..MainWndProc(var Message: TMessage);
根據Delphi 的約定,這種情況下隱函數Self 作為第一個參數,放入EAX 中,TMessage 結構的指針作為第二個參數,放入EDX中,而Message的指針從哪兒來呢?我們看到在連續幾個 Push 之后,程序已經在堆棧中構造了一個TMessage 結構,而這時的ESP 當然就是這個結構的指針,于是將它賦給EDX 。如果您不熟悉這方面的約定,可以參考Delphi 的幫助Object Pascal Refrence -> Program Control。
現在真相大白,Windows 消息百轉千折終于傳進MainWndProc , 不過這一路也可謂相當精彩,MakeObject這一函數自然是居功至偉, StdWndProc 也同樣是幕后英雄,讓我們把 MakeObjectInstance 作出的代碼和StdWndProc 連接起來,哦,堪稱鬼斧神工。
( 圖片無法顯示,請下載全文)
就此在總結一下, FobjectInstance 被VCL 注冊為窗口函數,而實際上 FObjectInstance 并不實際指向某個函數,而是指向一個ObjectInstance, 而后者我們已經知道是一系列相接的可執行代碼段當中的一塊,當系統需要將 FObjectInstance 當做窗口函數作為回調時,實際進入了ObjectInstance 所在的代碼段,然后幾番跳躍騰挪(一個call 加一個 jump )來到StdWndProc ,StdWndProc 的主要功用在于將Self 指針壓棧,并把Windows的消息包裝成Delphi的TMessage 結構,如此才能成功調用到TWinControl類的成員方法 MainWndProc, 消息一旦進入MainWndProc 便可以輕車熟路一路高唱小曲來到各個對象轉屬的WndProc , 從此功德圓滿。
后記:
個人感覺在這一技術上VCL 要比MFC 效率高出不少,后者每次根據窗口句柄來檢索相對應的窗口對象指針頗為費時,同時MakeObject 的代碼也相當具有參考價值,有沒有想過讓你自己的程序在內存中再開一堆可執行代碼?
所有的代碼是基于Delphi5的,可能與其余版本有所出入,但相信不會很大。
整個星期六和星期天我都花在寫作此文上了(連調試帶寫字), 不過水平所限,難免有所錯誤與表達不周,但愿不至以己昏昏令人昏昏,歡迎來信探討指教 cheka@yeah.net , thanx