2006. szeptember 28., csütörtök

Inside Delphi's Classes and Interfaces Part II


Problem/Question/Abstract:

You've probably used classes & interfaces more than once in your delphi programs. Did you ever dtop to think how delphi implements this creatures ?

Answer:

Inorder to understand this article, you must read the previous article (Inside Delphi's Classes and Interfaces Part I).
In this article we'll finish covering Delphi's implementation of Interfaces, and review a few usefull conclusions.

Let's start with an indepth example :

type

  IInterface1 = interface
    procedure ActA;
    procedure ActB;
  end;

  IInterface2 = interface(IInterface1)
    procedure ActC;
    procedure ActD; stdcall;
  end;

  TSampleClass = class(TInterfacedObject, IInterface1, IInterface2)
    procedure ActA;
    procedure ActB;
    procedure ActC;
    procedure ActD; stdcall;
  end;

var
  Interface1: IInterface1;
  Interface2: IInterface2;
  Sample: TSampleClass;
begin
  Sample := TSampleClass.Create;
  Interface1 := Sample;
  Interface2 := Sample;
  Interface1.ActA;
  Interface1.ActB;
  Interface2.ActA;
  Interface2.ActB;
  Interface2.ActC;
  Interface2.ActD;
end;

Instead of looking at the compiled code for this example, I'll simlpy note the interesting aspects of it. First, when assigning a value to Interface1, we'd expect delphi to take the value of  what 'Sample' points to and add a specific amount ($10) and be done with it. When assigning a value to Interface2, we'd expect delphi to do the same, just add a smaller amount ($0C) because the interfaces are stored in memory from the last to the first.
But delphi doesn't do that. It assignes both Interface1 AND Interface2 the value that 'Sample' points to plus $0C. That's because IInterface2 inherites from IInterface1. Therefor,  IInterface2 includes IInterface1. Hence, any call to Interface1, will actually be executed through IInterface2's method list.

Second, when we call Interface1.ActA, it calles the 4th (every interface inherites from IUnknown) method on IInterface2's method list (because IInterface2 inherites from IInterface1). When we call Interface1.ActB it calles the 5th method on IInterface2's method list. When we call Interface2.ActA it calles the 4th method on IInterface2's method list, just the same as Interface1.ActA. That's because IInterface2 inherites from IInterface1.

Third, when we call Interface2.ActD delphi addes one additional instruction before calling the 7th method of IInterface2. That's because we've declared a different convention call to the method (stdcall). Notice that all of IUnknown's methods are defined with the stdcall directive.

The structor of an interface's method list always follows the following rule :

First Method
.
.
Last Method
The parent's interface's method list

In our case, IInterface2's method list is as follows :
  
ActC
ActD
// IInterface1's method list
   ActA
   ActB
   // IUnknown's method List
      QueryInterface
     _AddRef
     _Release

NOTE : The structor above is how the methods' code is organized in memory. The first entry in any interface's method list will belong to QueryInterface (the first method of IUnknown) but it will point to a place in memory (the implementation of that specific interface's QueryItnerface method) that is higher than the interfaces' own methods' implementation - as shown in the structor above. In our case, IInterface2's QueryInterface's implementation is higher in memory than IInterface2's ActB's implementation, which is higher in memory than ActD's implementation. Thou ActD is the 7th entry, ActB is the 5th entry and QueryInterface is the 1st entry in IInterface2's method list.

To fully understand what happens when delphi calls an interface's method, lets have a look at the compiled method list of IInterface2 in the example above. The following code is an exact copy of the compiled code (except for the comments) :

// ActC
add eax, -$0C
jmp TSampleClass.ActC
// ActD
add dword ptr[esp + $04], -$0C
jmp TSampleClass.ActD
// ActA
add eax, -$0C
jmp TSampleClass.ActA
// ActB
add eax, -$0C
jmp TSampleClass.ActB
// QueryInterface
add dword ptr[esp + $04], -$0C
jmp TInterfacedObject.QueryInterface
// _AddRef
add dword ptr[esp + $04], -$0C
jmp TInterfacedObject._AddRef
// _Release
add dword ptr[esp + $04], -$0C
jmp TInterfacedObject._Release

As you remember, an object's method is actually a regular function/procedure that accepts as a parameter an instance of the method's class. As you can notice, before each call to the real method ('TSampleClass.ActD' for example) there is one line of code that changes the value of either 'eax', or 'dword ptr [esp + $04]', depending on the calling convention. As you can notice, in all cases we subtract $0C form a variable. But, why 12 ($0C = 12) ? That's because this interface (IInterface2) is in the 3rd (FRefcount, IUnknown are before it) place after the pointer to VMT of the clasS TSampleClass. Therefore, the value of any instance of IInterface2 of TSampleClass (Interface2 for example) is actually the value of the pointer to that class' instance plus 12.

Here is another example that will help understand the section above. The following code continues the defenitions from the above code :

type

  IAnotherInterface = interface
    procedure ActE;
  end;

  TAnotherSample = class(TInterfacedObject, IInterface2, IAnotherInterface)
    procedure ActA;
    procedure ActB;
    procedure ActC;
    procedure ActD; stdcall;
    procedure ActE;
  end;

var
  Interface2: IInterface2;
begin
  Interface2 := TAnotherSample.Create;
  Interface2.ActC;
end;

Now, let's compare the entry for this example's IInterface2 and the previous' one :

IInterface2 of TAnotherSample:
add eax, -$10
jmp TAnotherSample.ActC

IInterface2 of TSampleClass:
add eax, -$0C
jmp TSampleClass.ActC

There are two obvious changes :

The actuall function that is called (either TAnotherSample.ActC or TSampleClass.ActC)  
The amount that 'eax' is changed by. Notice that when calling IInterface2 of TAnotherSample, 'eax' is changed by 16 ($10 = 16) as opposed to being changed by 12. That's because on TAnotherSample, the IInterface2 is the second interface in the instance's structor in memory, and therefor it is "farther away" from the instance itself and needs to be changed by additional 4 bytes.

And now to some usefull sutff :

First, if you want to check if 2 (or more) interface variables are of the same instance, you cannot simply compare them, even if they are of the same type. You must QueryInterface them to a single interface type, and then compare. As a general rule, if you want to compare interfaces, QueryInterface them to IUnknown and then compare.

Example :

type

  IBooA = interface
  end;

  IBooB = interface
  end;

  TBoo = class(TInterfacedObject, IBooA, IBooB)
  end;

var
  Boo: TBoo;
  BooA: IBooA;
  BooB: IBooB;
begin
  Boo := TBoo.create;
  BooA := Boo;
  BooB := Boo;

  // This won't complie
  if BooA = BooB then
  begin
    Beep;
  end;

  if Integer(BooA) = Integer(BooB) then
  begin
    // will never get here
    Beep;
  end;

  if IUnknown(BooA) = IUnknown(BooB) then
  begin
    // will never get here
    Beep;
  end;

  // the 'as' word is the same as QueryInterface when acting on interfaces
  if (BooA as IUnknown) = (BooB as IUnknown) then
  begin
    // Will always get here
    Beep;
  end;
end;

Explaination : The first comparing won't complie, becuase BooA and BooB are of 2 different types. The Second and third comparings will complie but never return true. That's because type casting doesn't change the value of the variable that's being type casted. It only allows the complier to complie the code though there are two different types involved. Hence, if BooA is different from BooB, comparing them will never return true, no matter what type casting is done to them.
But why do BooA and BooB have different values ? They were both assigned using the ":= Boo;" statment. The answer is simple. Remeber that I said that an interface's variable's value is actually the value of the instance itself (or at least the value of the pointer to the instance) plus a different number for each interface ? In our case, BooA is the same as what Boo points to, added 16. And BooB is the same as what Boo points to, added 12. That's why BooA and BooB are not that same.
The Forth comparing actually works. That's because if an interface is from the same type, then comparing it to an interface of that type will always return the expected result (if both interfaces were aquired via QueryInterface, not by type casting). That's because if they are of the same type, then the difference between them and the instance is the same. And if they are of the same instance, then they must be equal.
That is, each interface is equal to it's instance + a specific Delta (the Delta depeneds on the interface). In other words, Interface = Instace + Delta. If 'Instance' is the same for both interfaces, and the 'Delta' is the same (cause they are of the same interface type), then both interfaces must be equal.

Note : This is the way delphi works, for good and for bad. You should take this in mind when writing code for propertys of interface type. The following code wouldn't work properly :

TSample = class
private
  FData: IUnknown;
  procedure SetData(Value: IUnknown);
protected
  procedure Changed; virtual; abstract;
public
  property Data: IUnknown read FData write SetData;
end;

procedure TSample.SetData(Value: IUnknown);
begin
  // This is incorrect.
  if Value <> FData then
  begin
    FData := Value;
    Changed;
  end;
end;

It might seem that this code should work, but it might not work when someone would assgin the property 'Data' with an IUnknown retreived by a type cast. The correct code should be :

procedure TSample.SetData(Value: IUnknown);
begin
  if (Value as IUnknown) <> (FData as IUnknown) then
  begin
    FData := Value;
    Changed;
  end;
end;

Second, each interface you declare that a class implements (with exception of interfaces that inherite from other interfaces) means that each instance of that class will take up 4 more byte of memory. That might seem like nothing (and probably is) except for one case. Consider the following code :

IInterfaceA = interface
end;

IInterfaceB = interface
end;

TSampleClass1 = class(TInterfacedObject, IInterfaceA)
end;

TSampleClass2 = class(TSampleClass1, IInterfaceA, IInterfaceB)
end;

It would seem that each instance of TSampleClass1 should take up 16 bytes, and each instance of TSampleClass2 should take up 20 bytes (4 bytes more, because it supports one more interface). That is not true. Each instance of TSampleClass1 does take up 16 byte. But, each instance of TSampleClass2 takes up 24 bytes ! That's because delphi creates an interface entry even for interfaces that are already implemented by parent classes.
The solution to this is simple. Just remove the decleration of IInterfaceA from TSampleClass2. This will not change the fact that TSampleClass2 implements IInterfaceA, cause TSamlpeClass2 inherites from TSamlpeClass1, which implements IInterface1. This wouldn't have happened if IInterfaceB was a decendant of IInterfaceA.
  
This might add up to quit alot if you do your inheritence improporely. For example :

TSampleClass1 = class(TInterfacedObject, IUnknown)
end;

TSampleClass2 = class(TSampleClass1, IUnknown)
end;

TSampleClass3 = class(TSampleClass2, IUnknown)
end;

TSampleClass4 = class(TSampleClass3, IUnknown)
end;

TSampleClass5 = class(TSampleClass4, IUnknown)
end;

Each instance of TSampleClass5 takes up 32 bytes of memory, though it has no real data (except for FRefCount of TItnerfacedObject).

Nincsenek megjegyzések:

Megjegyzés küldése