delphi-interfaces.pdf
Upcoming SlideShare
Loading in...5
×

Like this? Share it with your network

Share
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
No Downloads

Views

Total Views
415
On Slideshare
415
From Embeds
0
Number of Embeds
0

Actions

Shares
Downloads
14
Comments
0
Likes
1

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. 1 Delphi interfaces/abstractThis article covers interfaces in unmanaged Windows and Linux code. It focuses onpractical 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 usefulwhen writing more complex applications and becomes even more important when wedecide to split the application into packages.With appropriate use of interfaces we can change the implementation of a class in apackage, 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 moreflexible and easily upgradeable application.2.1 Interface historyThe first version of Delphi to support interfaces was Delphi 3. But there was a way to useand develop COM interfaces even in Delphi 2. How was that possible? The answer issimple. If you ignore the fact that a class can implement more than one interface, you canthink 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 inDelphi 2. The reason why the two constructs are comparable is the structure of theVirtual Method Table. You can think of an interface as a VMT definition.Delphi 3 introduced native interface support, making constructs like IIntf1 obsolete. Italso 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 abstractclasses.2.2 Interface implementationNow that we have decided to use interfaces in our application we have to overcome a fewdifficulties in declaring and implementing the application.
  • 2. 2.2.1 GUIDsThe most important difference between an abstract class and an interface is that aninterface should have a GUID. GUID is a 128bit constant that Delphi uses to uniquelyidentify an interface. You may have encountered GUIDs in COM, and Delphi uses thesame 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 128bitinteger, it is a string. Delphi compiler, however, recognizes the format of the string andconverts 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: becauseDelphi classes can implement multiple interfaces. When an application is running, therehas to be a mechanism that will get pointer to an appropriate interface from animplementation. The only way to find out if an object implements an interface and to geta pointer to implementation of that interface is through GUIDs.2.3 Interface core methodsBecause 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-helperclasses: TInterfacedObject, TAggregatedObject and TContainedObject. Each of theseclasses has its specific uses, which will be covered later in this article. What is commonfor 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 guessfrom their names, _AddRef increases a reference counter by one and _Release decreasesthe counter. The behaviour of _Release depends on the class used in implementation. Thepivotal method of interface management is QueryInterface. It takes GUID of an interface
  • 3. to get and returns a pointer to its implementation in Obj. For COM-compatibility, themethod returns OLE HResult result values.2.3.1 QueryInterface, as operator and assignment operatorHow 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 procedurecalls QueryInterface, which returns a pointer to an interface in an implementationinstance. 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:
  • 4. I1 := Impl as ISimpleInterface; // 1The as and := operators raise EIntfCastError if QueryInterface returns nil pointer. If youwant 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 twomethods, _AddRef and _Release are used in controlling lifetime of an interface.2.3.2 Interface creation and destructionAn interface is created by calling implementation’s constructor. Then the RTL copies apointer 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 pointerassignment 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:// line1begin 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 itsVMT 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; // 3end;In most cases, the interface assignment means assigning non-nil pointer to existinginterface to a nil pointer. If a destination interface is not nil – that means it alreadyreferences an existing interface – it must be released after successful assignment of thenew interface. This is why code on line // 1 copies old destination to a temporaryvariable. Then procedure then increases reference counter for source. It is important toincrease the reference counter before the actual assignment. If the procedure did not dothis, 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 violationexception. Hence, line // 2 increases reference counter in the source interface beforecopying its value to the destination. Finally, if Dest was assigned to another interface, theinterface is _Released.
  • 5. Once the interface is created, reference counter increased and destination is assigned withthe newly created interface, we can safely call its methods.// line 2:begin var ImplVMT = Intf + VMTOffset; (ImplVMT + MethodOffset)(); // 2end;Bearing in mind that an interface is simply a VMT template a method call must be a callto a method that is looked up in implementation’s VMT. In our simple example, Test isthe only virtual method if the implementation, MethodOffset is going to be 0, andVMTOffset is going to be $0c. The actual compiled code looks like this:// set eax to the address of the first local variablemov eax, [ebp - $04]// edx := @eaxmov 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 toodifferent from the call to a regular virtual method.Line 3 in the original listing is as important as line 1, because it controls destruction ofthe interface. It is important to remember that – in special cases – when an interface’sreference counter reaches zero, the implementation class is destroyed. The danger is thatthe pointer to the implementation may remain the same, thus an if-not-nil test for theimplementation 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 methodmust _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. _Releasemethod must call implementation’s destructor when the reference counter reaches 0. Letus have a look at the compiled code of our testing example:
  • 6. // load effective address of the first local variablelea eax, [ebp - $04]// in _IntfClear:// edx := @eaxmov edx, [eax]// if (edx = nil) then goto $0e (end);test edx, edxjz $0e// eax^ := 0;mov [eax], 0// push original value of eaxpush eax// push Self parameterpush edx// eax := @edxmov eax, [edx]// call _Release.call dword ptr [eax + $08]// restore eaxpop eaxThe most important thing to realize is that after line //3 in the original listing, theinterface is nil and the implementation is destroyed. The danger in this may be moreobvious 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; // 1end;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 ofthe implementation still remains not nil. Line // 1 will result in a call to a destructor ofalready destructed instance, which – in most cases – will cause an access violation.2.3.3 Implications of automatic implementation destructionWhat are the implications of the destruction mechanism? Perhaps the most important oneis that if you want to keep your code easily maintainable you should never have variablefor both implementation and interface.
  • 7. Another issue is that you have to do some extra coding if you want to use yourimplementation alive. Let’s consider this situation: an method of a class returns aninterface, but you do not want to instantiate an implementation class every time a call ismade 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 ofmisunderstood implementation destruction.The only instance when this class will function correctly is when GetInterface method isnot called. If GetInterface is called once an error will occur in TCls’s destructor, if it iscalled more than once, an error will occur when you try to call ISimpleInterface’s Testmethod.The way out of this mess is to use the correct base implementation class: Delphi’s Systemunit provides three base implementation classes – TinterfacedObject, TAggregatedObjectand TContainedObject. These three classes provide thread-safe implementation ofinterfaces.
  • 8. 2.3.4 TInterfacedObjectThis is the simplest class for interface implementation. The requirement for thread-safeimplementation has interesting implications. First of all, TInterfacedObject has to makesure that an interface is not released before it is completely constructed. This situationcan easily happen in a multi-threaded application. Consider a case where threadconstructs an instance of interface implementation class to get access to the interface.Before the instance is fully constructed, thread 2 releases previously acquired interface ofthe same type. This will trigger release mechanism and if the situation had not beenthought 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 constructors 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 wont 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.
  • 9. if Result = 0 then Destroy;end;This is the most important code in interface support. It is important to understand therules of interface and implementation creation and destruction.Let’s now move on to other base implementation classes.2.3.5 TContainedObject and TAggregatedObjectThese 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; // 2end;Lines // 1 and // 2show differences between TInterfacedObject an TContainedObject.Firstly, because of implements clause you do not have to implement methods ofISimpleInterface in TCls2. Instead, TCls2 must provide a property and a selector methodto get a pointer to ISimpleInterface. The implementation of the selector method for theSimple property gets interface from the controller. An instance of controller is passed as aparameter of the constructor method.Perhaps the most important difference between TContainedObject and TInterfacedObjectis the destruction mechanism. You must manually free an instance of TContainedObject.There is no automatic destructor calling, however, the automatic destructor calls for thecontainer class are still in place.2.3.6 TAggregatedObjectTAggregatedObject and TContainedObject are suitable base classes for interfaced objectsintended 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, usethese types to implement the inner object.
  • 10. Interfaces implemented by aggregated objects on behalf of the controller should not bedistinguishable from other interfaces provided by the controller. Aggregated objectsmust not maintain their own reference count - they must have the same lifetime as theircontroller. To achieve this, aggregated objects reflect the reference count methods to thecontroller.TAggregatedObject simply reflects QueryInterface calls to its controller. From such anaggregated object, one can obtain any interface that the controller supports, and onlyinterfaces that the controller supports. This is useful for implementing a controller classthat uses one or more internal objects to implement the interfaces declared on thecontroller class. Aggregation promotes implementation sharing across the objecthierarchy.TAggregatedObject is what most aggregate objects should inherit from, especially whenused 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 selectormethod, which gets the appropriate interface from the controller. Line // 2 is not legal,because TAggregatedObject can use only the controller to return the appropriateinterface.2.3.7 TContainedObjectThe purpose of TContainedObject is to isolate QueryInterface method on the aggregatefrom the controller. Classes derived from this class will only return interfaces that theclass itself implements, not the controller. This class should be used for implementinginterfaces that have the same lifetime as the controller. This design pattern is known asforced 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 asISimpleInterface).Test.
  • 11. 3 ConclusionInterfaces are very powerful tool for writing flexible and extensible applications. Just likeevery powerful tool, they can be very dangerous to use if you do not know what you wantto 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.