[翻譯] Virtual method interception 虛方法攔截

原文地址:http://blog.barrkel.com/2010/09/virtual-method-interception.htmlhtml

注:基於本人英文水平,如下翻譯只是我本身的理解,如對讀者形成未知影響,一切後果自負。函數

Delphi XE在rtti.pas單元有一個新的類型TVirtualMethodInterceptor。它最初是設計用於DataSnap的認證場景(雖然我不認爲只能用在這裏)。測試

這個類作了什麼?從本質上講,它在運行時動態地建立了一種衍生metaclass,並重寫父類的虛方法,經過建立一個新的虛擬方法表和並填充到父類。用戶能夠攔截虛擬函數調用,在代碼實現中改變參數,改變返回值,攔截和中斷異常或拋出新的異常,或徹底取代方法。在概念上,它有點相似於.NET和Java中的動態代理。它就像在運行時從一個類中派生出來的新類,重寫虛方法(但不添加新的字段屬性),而後將一個實例的運行類型更改成這個新的派生類。翻譯

爲何要這麼作?兩個目的:測試和遠程處理。(「虛擬方法攔截」最初用在DataSnap認證部分)。設計

用一個例子開始:代理

 

uses SysUtils, Rtti;
{$APPTYPE console}指針

type
TFoo = class
// 修改x 的值
function Frob(var x: Integer): Integer; virtual;
end;orm

function TFoo.Frob(var x: Integer): Integer;
begin
x := x * 2;
Result := x + 10;
end;htm

procedure WorkWithFoo(Foo: TFoo);
var
a, b: Integer;
begin
a := 10;
Writeln(' a = ', a);
try
b := Foo.Frob(a);
Writeln(' result = ', b);
Writeln(' a = ', a);
except
on e: Exception do
Writeln(' 異常: ', e.ClassName);
end;
end;對象

procedure P;
var
Foo: TFoo;
vmi: TVirtualMethodInterceptor;
begin
vmi := nil;
Foo := TFoo.Create;
try
Writeln('攔截之前:');
WorkWithFoo(Foo);

vmi := TVirtualMethodInterceptor.Create(Foo.ClassType);
vmi.OnBefore := procedure(Instance: TObject; Method: TRttiMethod;
const Args: TArray<TValue>; out DoInvoke: Boolean; out Result: TValue)
var
i: Integer;
begin
Write('[以前] 調用方法', Method.Name, ' 參數: ');
for i := 0 to Length(Args) - 1 do
Write(Args[i].ToString, ' ');
Writeln;
end;

// 改變 foo 實例的 metaclass pointer
vmi.Proxify(Foo);

//全部的虛方法調用之前,都調用了OnBefore事件
Writeln('攔截之後:');
WorkWithFoo(Foo);
finally
Foo.Free;
vmi.Free;
end;
end;

begin
P;
Readln;
end.

 
如下是輸出:

image_thumb

你會發現它攔截全部的虛擬方法,包括那些所謂的銷燬過程當中,不僅是咱們本身聲明的。(析構函數自己是不包括在內。)

我能夠徹底改變方法的實現,並跳過調用方法的主體:

procedure P;
var
Foo: TFoo;
vmi: TVirtualMethodInterceptor;
ctx: TRttiContext;
m: TRttiMethod;
begin
vmi := nil;
Foo := TFoo.Create;
try
Writeln('攔截之前:');
WorkWithFoo(Foo);

vmi := TVirtualMethodInterceptor.Create(Foo.ClassType);

m := ctx.GetType(TFoo).GetMethod('Frob');
vmi.OnBefore := procedure(Instance: TObject; Method: TRttiMethod;
const Args: TArray<TValue>; out DoInvoke: Boolean; out Result: TValue)
begin
if Method = m then
begin
DoInvoke := False; //原方法的邏輯不調用了
Result := 42;
Args[0] := -Args[0].AsInteger;
end;

end;

在這裏,中斷了方法的調用並把返回結果賦值爲42,同時修改第一個參數的值:
攔截前:
  before: a = 10
  Result = 30
  after:  a = 20
攔截後:
  before: a = 10
  Result = 42
  after:  a = –10
 
能夠經過一個異常來中斷方法的調用:
vmi.OnBefore := procedure(Instance: TObject; Method: TRttiMethod;
      const Args: TArray; out DoInvoke: Boolean; out Result: TValue)
    begin
      if Method = m then
        raise Exception.Create('Aborting');
    end;
輸出:
攔截前:
  before: a = 10
  Result = 30
  after:  a = 20
攔截後:
  before: a = 10
  Exception: Exception
 
不侷限於在方法調用前攔截,同時也能夠在方法調用後攔截,並能夠修改參數和返回值:
m := ctx.GetType(TFoo).GetMethod('Frob');
    vmi.OnAfter := procedure(Instance: TObject; Method: TRttiMethod;
      const Args: TArray; var Result: TValue)
    begin
      if Method = m then
        Result := Result.AsInteger + 1000000;
    end;
如下是輸出:
攔截前:
  before: a = 10
  Result = 30
  after:  a = 20
攔截後:
  before: a = 10
  Result = 1000030
  after:  a = 20
 
若是虛擬方法中拋出了一個異常,能夠屏蔽掉這個異常:
function TFoo.Frob(var x: Integer): Integer;
begin
  raise Exception.Create('Abort');
end;

// ...
    m := ctx.GetType(TFoo).GetMethod('Frob');
    vmi.OnException := procedure(Instance: TObject; Method: TRttiMethod;
      const Args: TArray; out RaiseException: Boolean;
      TheException: Exception; out Result: TValue)
    begin
      if Method = m then
      begin
        RaiseException := False;  //屏蔽掉原方法中的異常
        Args[0] := Args[0].AsInteger * 2;
        Result := Args[0].AsInteger + 10;
      end;
    end;
輸出:
Before hackery:
  before: a = 10
  Exception: Exception
After interception:
  before: a = 10
  Result = 30
  after:  a = 20
 
有件事要知道,類TVirtualMethodInterceptor 是沒有的, 它經過攔截對象工做,攔截須要一些內存開銷,但這是不多的:
PPointer(foo)^ := vmi.OriginalClass;
 
另外一個指針: 類的繼承鏈是被掛鉤過程改變了。這能夠很容易地顯示:
//...
    Writeln('After interception:');
    WorkWithFoo(foo);
    
    Writeln('Inheritance chain while intercepted:');
    cls := foo.ClassType;
    while cls <> nil do
    begin
      Writeln(Format('  %s (%p)', [cls.ClassName, Pointer(cls)]));
      cls := cls.ClassParent;
    end;
    
    PPointer(foo)^ := vmi.OriginalClass;
    
    Writeln('After unhooking:');
    WorkWithFoo(foo);
    
    Writeln('Inheritance chain after unhooking:');
    cls := foo.ClassType;
    while cls <> nil do
    begin
      Writeln(Format('  %s (%p)', [cls.ClassName, Pointer(cls)]));
      cls := cls.ClassParent;
    end;
// ...
 
輸出:
Before hackery:
  before: a = 10
  Exception: Exception
After interception:
  before: a = 10
  Result = 30
  after:  a = 20
Inheritance chain while intercepted:
  TFoo (01F34DA8)      //運行時增長的類
  TFoo (0048BD84)      //vmi.OriginalClass
  TObject (004014F0)
After unhooking:
  before: a = 10
  Exception: Exception
Inheritance chain after unhooking:
  TFoo (0048BD84)
  TObject (004014F0)
 

該功能主要是前期庫的基礎修補,但但願你能夠看到,這不是太難。(提供一種打補丁的方式)

兩個問題:

1:這種方法跟寫Helper的區別?

2:當類是多層繼承時,攔截的是哪一個類的虛擬方法?

相關文章
相關標籤/搜索