delphi-interfaces.pdf

11

Click here to load reader

Upload: gestio-organitzacio-comunicacio-sa

Post on 01-Jun-2015

263 views

Category:

Technology


0 download

DESCRIPTION

delphi-interfaces.pdf

TRANSCRIPT

Page 1: delphi-interfaces.pdf

1 Delphi interfaces/abstract This article covers interfaces in unmanaged Windows and Linux code. It focuses on practical issues developers have to face when they use interfaces in their code.

2 Why interfaces? Interfaces enable us to write code that is implementation-independent. This is very useful when writing more complex applications and becomes even more important when we decide to split the application into packages. With appropriate use of interfaces we can change the implementation of a class in a package, recompile the package and still use it with the original application. Interfaces also allow us to write more loosely coupled class structures resulting in a more flexible and easily upgradeable application.

2.1 Interface history The first version of Delphi to support interfaces was Delphi 3. But there was a way to use and develop COM interfaces even in Delphi 2. How was that possible? The answer is simple. If you ignore the fact that a class can implement more than one interface, you can think of an interface as a pure abstract class. type

IIntf1 = class public

function Test; virtual; abstract; end;

IIntf2 = interface public

function Test; end;

Obviously, IIntf1 has many limitations, but this was the way to write COM interfaces in Delphi 2. The reason why the two constructs are comparable is the structure of the Virtual Method Table. You can think of an interface as a VMT definition. Delphi 3 introduced native interface support, making constructs like IIntf1 obsolete. It also added the biggest improvement to Object Pascal: multiple interface implementations. type

IIntf1 = interface … end; IIntf2 = interface … end; TImplementation = class(TAncestor, IIntf1, IIntf2) … end; // !

Construct on line // 1 would be illegal if IIntf1 and IIntf2 were declared as abstract classes.

2.2 Interface implementation Now that we have decided to use interfaces in our application we have to overcome a few difficulties in declaring and implementing the application.

Page 2: delphi-interfaces.pdf

2.2.1 GUIDs The most important difference between an abstract class and an interface is that an interface should have a GUID. GUID is a 128bit constant that Delphi uses to uniquely identify an interface. You may have encountered GUIDs in COM, and Delphi uses the same principles as COM to get access to an interface. type

ISimpleInterface = interface ['{BCDDF1B6-73CC-406C-912F-7148095F1F4C}'] // 1

end; GUID is shown on the line // 1. As you can see the GUID on line // 1 is not a 128bit integer, it is a string. Delphi compiler, however, recognizes the format of the string and converts it into GUID structure. type

TGUID = packed record D1: LongWord; D2: Word; D3: Word; D4: array[0..7] of Byte;

end; The same string to 128bit Integer also applies when defining a GUID constant: type

IID_ISimpleInterface: TGUID = '{BCDDF1B6-73CC-406C-912F-7148095F1F4C}';

2.2.2 Why are GUIDs important? Why does an interface need to be uniquely identifiable? The answer is simple: because Delphi classes can implement multiple interfaces. When an application is running, there has to be a mechanism that will get pointer to an appropriate interface from an implementation. The only way to find out if an object implements an interface and to get a pointer to implementation of that interface is through GUIDs.

2.3 Interface core methods Because an interface is simply a template for the implementation, it cannot control it life. This is why native Delphi (as well as COM) uses reference counting. Reference counting in Delphi is implemented in three fundamental interface-helper classes: TInterfacedObject, TAggregatedObject and TContainedObject. Each of these classes has its specific uses, which will be covered later in this article. What is common for all these classes, however, are the three fundamental interface methods: function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; Let us start with the simple ones; _AddRef and _Release. As you can probably guess from their names, _AddRef increases a reference counter by one and _Release decreases the counter. The behaviour of _Release depends on the class used in implementation. The pivotal method of interface management is QueryInterface. It takes GUID of an interface

Page 3: delphi-interfaces.pdf

to get and returns a pointer to its implementation in Obj. For COM-compatibility, the method returns OLE HResult result values.

2.3.1 QueryInterface, as operator and assignment operator How is QueryInterface related to as and assignment operators? The answer is simple: QueryInterface is used to get a pointer to an interface from the implementing class. Let us consider this code snippet. type

TCls = class(TInterfacedObject, IIntf1, IIntf2) protected

// implementation of interfaces.

end;

var C: TCls; I1: Intf1; I2: Intf2;

begin C := TCls.Create; I1 := C; // 1 I2 := C; // 2

// call methods of I1 and I2

I1 := nil; I2 := nil;

end; The code on lines //1 and //2 is compiled as call to _IntfCast procedure. This procedure calls QueryInterface, which returns a pointer to an interface in an implementation instance. It also releases previous value of destination. procedure _IntfCast(var Dest: IInterface; const Source: IInterface; const IID: TGUID); var

Temp: IInterface; begin

if Source = nil then Dest := nil

else begin

Temp := nil; if Source.QueryInterface(IID, Temp) <> 0 then // 1

Error(reIntfCastError) else

Dest := Temp; end;

end; Exactly the same code will be produced if we use as construct:

Page 4: delphi-interfaces.pdf

I1 := Impl as ISimpleInterface; // 1

The as and := operators raise EIntfCastError if QueryInterface returns nil pointer. If you want to avoid using exception handling, use QueryInterface instead:

Impl.QueryInterface(IAnotherInterface, A);

QueryInterface is one of the pivotal methods of interfaces in Delphi. The other two methods, _AddRef and _Release are used in controlling lifetime of an interface.

2.3.2 Interface creation and destruction An interface is created by calling implementation’s constructor. Then the RTL copies a pointer to the interface from the created implementation instance to the interface variable. You may have already guessed that copying of an interface is firstly a simple pointer assignment and then increase of the reference counter. To increase the reference counter, RTL calls _AddRef method provided by the implementation’s base class. Let us have a look at Delphi pseudo-code for lines //1 to //3: // line1

begin var C: TSimpleImplementation := TSimpleImplementacion.Create; if (C = nil) then Exit; var CVMT := C - VMTOffset; _IntfCopy(Intf, CVMT);

end; The code on line 1 constructs an instance of the implementation class, get pointer to its VMT and then call _IntfCopy function. The most important piece of code is _IntfCopy. procedure _IntfCopy(var Dest: IInterface; const Source: IInterface); var

OldDest: Pointer; begin

OldDest := Dest; // 1 if Source <> nil then

Source._AddRef; // 2 Dest := Source; if OldDest <> nil then

IInterface(OldDest)._Release; // 3 end; In most cases, the interface assignment means assigning non-nil pointer to existing interface to a nil pointer. If a destination interface is not nil – that means it already references an existing interface – it must be released after successful assignment of the new interface. This is why code on line // 1 copies old destination to a temporary variable. Then procedure then increases reference counter for source. It is important to increase the reference counter before the actual assignment. If the procedure did not do this, another thread might _Release an interface before _IntfCopy could finish executing. This would result in assigning a freed instance, which would result in an Access violation exception. Hence, line // 2 increases reference counter in the source interface before copying its value to the destination. Finally, if Dest was assigned to another interface, the interface is _Released.

Page 5: delphi-interfaces.pdf

Once the interface is created, reference counter increased and destination is assigned with the newly created interface, we can safely call its methods. // line 2:

begin var ImplVMT = Intf + VMTOffset; (ImplVMT + MethodOffset)(); // 2

end; Bearing in mind that an interface is simply a VMT template a method call must be a call to a method that is looked up in implementation’s VMT. In our simple example, Test is the only virtual method if the implementation, MethodOffset is going to be 0, and VMTOffset is going to be $0c. The actual compiled code looks like this: // set eax to the address of the first local variable

mov eax, [ebp - $04] // edx := @eax

mov edx, [eax] // call to ((procedure of object)(edx + VMTOffset + MethodOffset))()

call dword ptr [edx + $0c] The code actually calls Test method of the implementation class. The code is not too different from the call to a regular virtual method. Line 3 in the original listing is as important as line 1, because it controls destruction of the interface. It is important to remember that – in special cases – when an interface’s reference counter reaches zero, the implementation class is destroyed. The danger is that the pointer to the implementation may remain the same, thus an if-not-nil test for the implementation does no guarantee that an implementation still exists. // line 3:

begin _IntfClear(Intf);

end; As you can tell, the most important code is hidden in _IntfClear method. This method must _Release the interface, and (if appropriate, free the implementation). function _IntfClear(var Dest: IInterface): Pointer; var

P: Pointer; begin

Result := @Dest; if Dest <> nil then begin

P := Pointer(Dest); Pointer(Dest) := nil; // 1 IInterface(P)._Release; // 2

end; end; The line //1 sets the destination pointer to nil, and line //2 releases the interface. _Release method must call implementation’s destructor when the reference counter reaches 0. Let us have a look at the compiled code of our testing example:

Page 6: delphi-interfaces.pdf

// load effective address of the first local variable

lea eax, [ebp - $04] // in _IntfClear: // edx := @eax

mov edx, [eax] // if (edx = nil) then goto $0e (end);

test edx, edx jz $0e // eax^ := 0;

mov [eax], 0 // push original value of eax

push eax // push Self parameter

push edx // eax := @edx

mov eax, [edx] // call _Release.

call dword ptr [eax + $08] // restore eax

pop eax The most important thing to realize is that after line //3 in the original listing, the interface is nil and the implementation is destroyed. The danger in this may be more obvious from this code snippet: var

Impl: TSimpleImplementation; Intf: ISimpleInterface;

begin Impl := TSimpleImplementation.Create; Intf := Impl; Intf.Test; Intf := nil; if (Impl <> nil) then Impl.Free; // 1

end; The danger is on line // 1: after an interface’s reference counter has reached zero, implementation’s destructor is called; however, the value of the pointer to the instance of the implementation still remains not nil. Line // 1 will result in a call to a destructor of already destructed instance, which – in most cases – will cause an access violation.

2.3.3 Implications of automatic implementation destruction What are the implications of the destruction mechanism? Perhaps the most important one is that if you want to keep your code easily maintainable you should never have variable for both implementation and interface.

Page 7: delphi-interfaces.pdf

Another issue is that you have to do some extra coding if you want to use your implementation alive. Let’s consider this situation: an method of a class returns an interface, but you do not want to instantiate an implementation class every time a call is made to the method. type

TCls = class public

function GetInterface: ISimpleInterface; end;

It is easy to forget the destruction rules and write this code: type

TCls = class private

FImpl: TSimpleImplementation; public

constructor Create; destructor Destroy; override; function GetInterface: ISimpleInterface;

end;

constructor TCls.Create; begin

inherited Create; FImpl := TSimpleImplementation.Create;

end;

destructor TCls.Destroy; begin

if (FImpl <> nil) then FImp.Free; inherited;

end;

function TCls.GetInterface: ISimpleInterface; begin

Result := FImpl; end; The first error is to use instance of implementation instead of interface. The problems (access violations, to be more specific) that you will encounter are the result of misunderstood implementation destruction. The only instance when this class will function correctly is when GetInterface method is not called. If GetInterface is called once an error will occur in TCls’s destructor, if it is called more than once, an error will occur when you try to call ISimpleInterface’s Test method. The way out of this mess is to use the correct base implementation class: Delphi’s System unit provides three base implementation classes – TinterfacedObject, TAggregatedObject and TContainedObject. These three classes provide thread-safe implementation of interfaces.

Page 8: delphi-interfaces.pdf

2.3.4 TInterfacedObject This is the simplest class for interface implementation. The requirement for thread-safe implementation has interesting implications. First of all, TInterfacedObject has to make sure that an interface is not released before it is completely constructed. This situation can easily happen in a multi-threaded application. Consider a case where thread constructs an instance of interface implementation class to get access to the interface. Before the instance is fully constructed, thread 2 releases previously acquired interface of the same type. This will trigger release mechanism and if the situation had not been thought of this could result in premature release of the constructed interface. The following code is taken directly from Delphi’s System.pas unit: procedure TInterfacedObject.AfterConstruction; begin // Release the constructor's implicit refcount. Thread-safe increase is // achieved using Win API call to InterlockedDecrement in place of Dec

InterlockedDecrement(FRefCount); end;

procedure TInterfacedObject.BeforeDestruction; begin

if RefCount <> 0 then Error(reInvalidPtr);

end;

// Set an implicit refcount so that refcounting // during construction won't destroy the object.

class function TInterfacedObject.NewInstance: TObject; begin

Result := inherited NewInstance; TInterfacedObject(Result).FRefCount := 1;

end;

function TInterfacedObject.QueryInterface(const IID: TGUID; out Obj): HResult; begin

if GetInterface(IID, Obj) then Result := 0

else Result := E_NOINTERFACE;

end;

function TInterfacedObject._AddRef: Integer; begin

Result := InterlockedIncrement(FRefCount); end;

function TInterfacedObject._Release: Integer; begin // _Release thread-safely decreases the reference count, and

Result := InterlockedDecrement(FRefCount); // if the reference count is 0, frees itself.

Page 9: delphi-interfaces.pdf

if Result = 0 then Destroy;

end; This is the most important code in interface support. It is important to understand the rules of interface and implementation creation and destruction. Let’s now move on to other base implementation classes.

2.3.5 TContainedObject and TAggregatedObject These two classes should be used when using implements syntax on interface property. Both classes keep a weak reference to the controller that implements the interfaces. type

TCls2 = class(T[Contained|Aggregated]Object, ISimpleInterface) private

function GetSimple: ISimpleInterface; public

property Simple: ISimpleInterface read GetSimple implements ISimpleInterface;

end;

function TCls2.GetSimple: ISimpleInterface; begin

Result := Controller as ISimpleInterface; end;

var C: TCls2;

begin C := TCls2.Create(TSimpleImplementation.Create); // 1 // Call interface methods C.Free; // 2

end; Lines // 1 and // 2show differences between TInterfacedObject an TContainedObject. Firstly, because of implements clause you do not have to implement methods of ISimpleInterface in TCls2. Instead, TCls2 must provide a property and a selector method to get a pointer to ISimpleInterface. The implementation of the selector method for the Simple property gets interface from the controller. An instance of controller is passed as a parameter of the constructor method. Perhaps the most important difference between TContainedObject and TInterfacedObject is the destruction mechanism. You must manually free an instance of TContainedObject. There is no automatic destructor calling, however, the automatic destructor calls for the container class are still in place.

2.3.6 TAggregatedObject TAggregatedObject and TContainedObject are suitable base classes for interfaced objects intended to be aggregated or contained in an outer controlling object. When using the "implements" syntax on an interface property in an outer object class declaration, use these types to implement the inner object.

Page 10: delphi-interfaces.pdf

Interfaces implemented by aggregated objects on behalf of the controller should not be distinguishable from other interfaces provided by the controller. Aggregated objects must not maintain their own reference count - they must have the same lifetime as their controller. To achieve this, aggregated objects reflect the reference count methods to the controller. TAggregatedObject simply reflects QueryInterface calls to its controller. From such an aggregated object, one can obtain any interface that the controller supports, and only interfaces that the controller supports. This is useful for implementing a controller class that uses one or more internal objects to implement the interfaces declared on the controller class. Aggregation promotes implementation sharing across the object hierarchy. TAggregatedObject is what most aggregate objects should inherit from, especially when used in conjunction with the "implements" syntax. Let TCls2 be descendant of TAggregatedObject: in that case we can write this code: var

C: TCls2; begin

C := TCls2.Create(TSimpleImplementation.Create); C.Simple.Test; // 1 (C as ISimpleInterface).Test; // 2 C.Free;

end; The line // 1 is legal; it simply gets a pointer to ISimpleInterface using GetSimple selector method, which gets the appropriate interface from the controller. Line // 2 is not legal, because TAggregatedObject can use only the controller to return the appropriate interface.

2.3.7 TContainedObject The purpose of TContainedObject is to isolate QueryInterface method on the aggregate from the controller. Classes derived from this class will only return interfaces that the class itself implements, not the controller. This class should be used for implementing interfaces that have the same lifetime as the controller. This design pattern is known as forced encapsulation. Let TCls2 be descendant of TContainedObject: var

C: TCls2; begin

C := TCls2.Create(TSimpleImplementation.Create); C.Simple.Test; // 1 (C as ISimpleInterface).Test; // 2 C.Free;

end; Unlike the previous case, we can now use both statements C.Simple.Test as well as (C as ISimpleInterface).Test.

Page 11: delphi-interfaces.pdf

3 Conclusion Interfaces are very powerful tool for writing flexible and extensible applications. Just like every powerful tool, they can be very dangerous to use if you do not know what you want to write and how the compiler is going to interpret the code. In the next article I will focus on .NET interfaces and Delphi .NET compiler issues.