Wednesday, September 01, 2010

Virtual method interception

Delphi XE has a new type in Rtti.pas called TVirtualMethodInterceptor. It was originally designed for use in DataSnap authentication scenarios (though I don't think it's currently being used there), but of course, making it only work for that would have been quite a limitation.

What does it do? Essentially, it creates a derived metaclass dynamically at runtime that overrides every virtual method in the ancestor, by creating a new virtual method table and populating it with stubs that intercepts calls and arguments. When the metaclass reference for any instance of the "ancestor" is replaced with this new metaclass, the user can then intercept virtual function calls, change arguments on the fly, change the return value, intercept and suppress exceptions or raise new exceptions, or entirely replace calling the underlying method. In concept, it's somewhat similar to dynamic proxies from .NET and Java. It's like being able to derive from a class at runtime, override methods (but not add new instance fields), and then change the runtime type of an instance to this new derived class.

Why would you want to do this? Two obvious purposes spring to mind: testing and remoting. Mock objects have been in vogue in the testing space in other languages for some time. By intercepting method calls, one may more easily verify that a particular subsystem is calling all the right methods, with the correct arguments, in the expected order; similarly, the subsystem can proceed with the return values from these method calls, without necessarily having to hit the database, the network, etc. for what should be a unit test. Remoting on the basis of method calls is somewhat less useful, especially when an unreliable and latency-prone network gets into the stack, but that's not the only usage point. The virtual method interceptor logic was originally implemented to be used as part of DataSnap authentication, so that a call that comes in from the network can still be checked as its code flow spreads throughout the graph of objects.

Anyhow, here's a simple example to get started:

uses SysUtils, Rtti;
{$apptype console}
type
  TFoo = class
    // Frob doubles x and returns the new x + 10
    function Frob(var x: Integer): Integer; virtual;
  end;

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

procedure WorkWithFoo(Foo: TFoo);
var
  a, b: Integer;
begin
  a := 10;
  Writeln('  before: a = ', a);
  try
    b := Foo.Frob(a);
    Writeln('  Result = ', b);
    Writeln('  after:  a = ', a);
  except
    on e: Exception do
      Writeln('  Exception: ', e.ClassName);
  end;
end;

procedure P;
var
  foo: TFoo;
  vmi: TVirtualMethodInterceptor;
begin
  vmi := nil;
  foo := TFoo.Create;
  try
    Writeln('Before hackery:');
    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('[before] Calling ', Method.Name, ' with args: ');
      for i := 0 to Length(Args) - 1 do
        Write(Args[i].ToString, ' ');
      Writeln;
    end;
    
    // Change foo's metaclass pointer to our new dynamically derived
    // and intercepted descendant
    vmi.Proxify(foo);
    
    Writeln('After interception:');
    WorkWithFoo(foo);
  finally
    foo.Free;
    vmi.Free;
  end;
end;

begin
  P;
end.

Here's what it outputs:

Before hackery:
  before: a = 10
  Result = 30
  after:  a = 20
After interception:
  before: a = 10
[before] Calling Frob with args: 10 
  Result = 30
  after:  a = 20
[before] Calling BeforeDestruction with args: 
[before] Calling FreeInstance with args: 

You'll notice that it intercepts all the virtual methods, including those called during destruction, not just the one I declared. (The destructor itself is not included.)

We can get more ambitious with what it does. I can change the implementation entirely, and skip calling the underlying (i.e. inherited) method body:

procedure P;
var
  foo: TFoo;
  vmi: TVirtualMethodInterceptor;
  ctx: TRttiContext;
  m: TRttiMethod;
begin
  vmi := nil;
  foo := TFoo.Create;
  try
    Writeln('Before hackery:');
    WorkWithFoo(foo);
    
    vmi := TVirtualMethodInterceptor.Create(foo.ClassType);
    
    m := ctx.GetType(TFoo).GetMethod('Frob');
    vmi.OnBefore := procedure(Instance: TObject; Method: TRttiMethod;
      const Args: TArray; out DoInvoke: Boolean; out Result: TValue)
    begin
      if Method = m then
      begin
        DoInvoke := False;
        Result := 42;
        Args[0] := -Args[0].AsInteger;
      end;
    end;

Here, I inhibit the invocation and hard-code the result to 42, while negating the first argument. The proof is in the output:

Before hackery:
  before: a = 10
  Result = 30
  after:  a = 20
After interception:
  before: a = 10
  Result = 42
  after:  a = -10

I could have inhibited the call by raising an exception instead:

    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;

And output:

Before hackery:
  before: a = 10
  Result = 30
  after:  a = 20
After interception:
  before: a = 10
  Exception: Exception

It's not limited to interception before the logically inherited call, but also interception after the call, again with the opportunity to fiddle with arguments and return value:

    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;

And output:

Before hackery:
  before: a = 10
  Result = 30
  after:  a = 20
After interception:
  before: a = 10
  Result = 1000030
  after:  a = 20

And if the inherited implementation raises an exception, that can even be squashed:

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;

Output:

Before hackery:
  before: a = 10
  Exception: Exception
After interception:
  before: a = 10
  Result = 30
  after:  a = 20

One thing the TVirtualMethodInterceptor class doesn't have, however, is a way to unhook (unproxify) the object. If the object is never unhooked, it's important that the object doesn't outlive the interceptor, because the interceptor needs to allocate executable memory in order to create the little stubs with which it redirects method calls to the events. Fortunately, it's pretty trivial to do:

    PPointer(foo)^ := vmi.OriginalClass;

Another point: the class inheritance chain is changed by the hooking process. This can be shown easily:

//...
    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;
// ...

And output:

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)
  TObject (004014F0)
After unhooking:
  before: a = 10
  Exception: Exception
Inheritance chain after unhooking:
  TFoo (0048BD84)
  TObject (004014F0)

The feature is primarily an infrastructure piece for advanced libraries, but hopefully you can see that it's not too difficult to get into.

12 comments:

Leonardo M. Ramé said...

Very interesting Barry, as usual. It would be nice also to have Property interceptors in a future version, not just properties that have getters and/or setters, but properties of the type "property myProp: string read FProp write FProp" (that write directly to variables).

Why this could be important to me?, imagine an RTTI based ORM, that assign just a couple of values to an object that has, say 10 properties, then call the method "save" to store the object in a databse. With "property interceptors" the programmer could know at runtime wich properties were changed, then automatically create an sql update command with just those properties.

Anonymous said...

Barry,

as always: you have very interesting concepts and ideas. But, which "normal" coder can cope with such high sophisticated language constructs? I like the "keep it simple and stupid". 2-5%??? It is not worth your effort.

gabr said...

Lovely!

Apollonius said...

Definitely interesting. .NET Remoting is much more powerful - it allows interception of nonvirtual methods and even field accesses - but requires inheritance from MarshalByRefObject. Although this is nice to have, I would not advocate it for an unmanaged language like Delphi. It would require cooperation from the compiler and cause a runtime overhead even for accesses of non-proxy objects, so it is not worth the disadvantages in my opinion.

The next step would be the possibility to implement additional interfaces which are not implemented by the original class. It would be even cooler if this could be done on demand like in .NET where you can implement IRemotingTypeInfo and thus decide if you implement an interface just when the cast occurs.

Barry Kelly said...

To Anonymous decrying the high-concept thing: I'd look at this feature as a piece of infrastructure, like I said, that third party libraries (that may be oriented towards testing or remoting etc.) may use. Its usefulness for the average developer may not be high, but the usefulness of the things you can build with it would, I believe, be much higher.

Uwe Raabe said...

Adding interface implementations should already be possible by intercepting the virtual method QueryInterface.

Barry Kelly said...

Apollonius - it's true, .NET remoting is more powerful, and I agree, instrumenting every method call, including statics, and every field access, including field-backed properties (as Leonardo suggests) would be a step too far, because the compiler would have no way ahead of time knowing which classes would be modified at runtime, and would therefore make every access slower. So it is only for explicitly labeled virtual methods for now. I could envision however an "instrumentable" class, specifically labeled as such, whereupon every property access and every non-virtual method gains "virtual-like" behaviour, not in the language, but in the runtime, with sufficient indirection to approach the power of .NET's remoting.

As to dynamically implementing interfaces, technically, much of that is possible today, so long as QueryInterface is in the call chain when the dynamic cast occurs. That's not always the case, of course.

Apollonius said...

Sure, it is easy to implement QueryInterface yourself and thus decide which casts you allow. However, you also have to manufacture an interface pointer to return so the caller can invoke the interface's methods. This involves dynamic generation of a vtable similar to what TVirtualMethodInterceptor does today.

Anonymous said...

I got to look at something like spring.net, castle.net etc. Even more interesting thing is PostSharp.

Magno Machado Paulo said...

It's a very powerfull feature, and I'm happy to see it implemented in Delphi.

BWT, for those who are using D2010, I've managed to implement something very similar to this to support mocking and other features on Emballo: http://code.google.com/p/emballo

It's a work in progress, but it's already working.

Unfortunately, being able to mock only virtual methods isn't very helpfull. My goal is to make the interception possible for non virutal methods by using code overwriting, the same way people use to do dll hooking.

Vitor Luiz Rubio said...

Really impressive! Is this a way to make APO (aspect oriented programing) in Delphi?

Remy Lebeau said...

An Unproxify() method was added to TVirtualMethodInterceptor in XE2.