首先我们需要清楚 Delphi 中“对象方法(Method)”和“函数/过程(Function/Procedure)”之间的区别。对象方法在调用时会隐藏的传递对象的 Self 指针作为第一个参数,比如我们调用 MainForm.Show(); 实际上在编译后 MainForm 这个对象是作为第一个参数传给 Show 方法了(Delphi 默认的 register 调用方式使用 EAX 寄存器传递)。
而 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 函数。
我们可以编写下面的代码:
procedure MyClick(Self: TObject; Sender: TObject);
begin
// 第一个参数是虚拟的
ShowMessage(Format('Self: %d, Sender: %s', [Integer(Self), Sender.ClassName]));
end;
procedure TForm1.FormCreate(Sender: TObject);
var
M: TMethod;
begin
M.Code := @MyClick;
M.Data := Pointer(325); // 随便取的数
btn1.OnClick := TNotifyEvent(M);
end;
这样就可以将一个普通函数赋值给对象事件属性了。
我们再来看看 TLanguages.Create 的代码:
constructor TLanguages.Create;
type
TCallbackThunk = packed record
POPEDX: Byte;
MOVEAX: Byte;
SelfPtr: Pointer;
PUSHEAX: Byte;
PUSHEDX: Byte;
JMP: Byte;
JmpOffset: Integer;
end;
var
Callback: TCallbackThunk;
begin
inherited Create;
Callback.POPEDX := $5A;
Callback.MOVEAX := $B8;
Callback.SelfPtr := Self;
Callback.PUSHEAX := $50;
Callback.PUSHEDX := $52;
Callback.JMP := $E9;
Callback.JmpOffset := Integer(@TLanguages.LocalesCallback) - Integer(@Callback.JMP) - 5;
EnumSystemLocales(TFNLocaleEnumProc(@Callback), LCID_SUPPORTED);
end;
在 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 结构变量来生成一小段动态代码。这段代码是构造在堆栈中的(局部变量),转换成汇编是:
prcoedure CallbackThunk;
asm
// 取出 lpLocaleString 参数到 EDX 寄存器
// CALLBACK EnumLocalesProc 是 stdcall 调用,参数在堆栈中
POP EDX
// 将 Self 对象传给 EAX 寄存器
MOV EAX Self
// stdcall 调用,将 Self 作为第一个参数压栈
PUSH EAX
// 将 lpLocaleString 作为第二个参数压栈
PUSH EDX
// 用相对跳转指令跳转到 TLanguages.LocalesCallback 入口地址
JMP TLanguages.LocalesCallback
end;
将 CallbackThunk 作为临时的回调函数传递给 EnumSystemLocales 是合法的。当回调被执行时,前面那小段代码动态修改了堆栈的内容,将本来只有一个参数的调用,变成了两个参数,从而实现了回调与对象方法的转换。
但是,正如 Passion 在前面提到的,由于这小块临时代码是放在堆栈中的,而 Win2003 的 DEP 限制了在堆栈中执行代码,导致事实上回调函数并没有被正确地调用。
Borland 程序员也看到了这个问题,所以在 BDS 2006 中,这部分代码的实现修改成:
var
FTempLanguages: TLanguages;
function EnumLocalesCallback(LocaleID: PChar): Integer; stdcall;
begin
Result := FTempLanguages.LocalesCallback(LocaleID);
end;
constructor TLanguages.Create;
begin
inherited Create;
FTempLanguages := Self;
EnumSystemLocales(@EnumLocalesCallback, LCID_SUPPORTED);
end;
通过声明一个临时变量和转换函数,来取代原来的方法,就不会有 DEP 冲突了。
附带说一下 Forms 单元中的 MakeObjectInstance。这个函数用来生成一块动态代码,将 Windows 的窗体消息处理过程转换为 Delphi 的对象方法调用。在 TWinControl 等需要有消息处理支持的地方用到。该函数也是采用了前面类似的方法,不过不同的是,由于这些转换调用是长期的,所以那些动态生成的代码被放到了标识为可执行的动态空间中了,所以在 Win2003 的 DEP 下仍然可以正常工作:
function MakeObjectInstance(Method: TWndMethod): Pointer;
var
...
begin
if InstFreeList = nil then
begin
Block := VirtualAlloc(nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
...
end;
刘啸
例如我们声明了一个方法 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里头来的。
由上可得到一个通用函数,用来动态设置对象事件:
procedure ReSetObjEvent(OldEventAddr: pointer;NewEventValue: pointer;ReSetObject: TObject);
begin
TMethod(OldEventAddr^).Code := NewEventValue;
TMethod(OldEventAddr^).Data := ReSetObject;
end;
参数一: 指定为 存放事件指针的内存地址值的地址指针,所以为一个指针的指针
参数二: 指定为新的事件函数地址指针
参数三: 指定为重设事件的修改者,用来隐射对象方法的隐含参数Self
调用方法:
ReSetObjEvent(@integer(@self.OnClose),@MyCloseEvent,self);
例:
procedure MyCloseEvent(ClassSend: TObject;Sender: TObject;var Action: TCloseAction );
begin
action := canone;
showmessage(TComponent(Sender).Name+'触发,不许关闭');
showmessage(TComponent(ClassSend).Name);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
ReSetObjEvent(@integer(@self.OnClose),@MyCloseEvent,self);
end;