WhatsApp 9892124323 âś“Call Girls In Kalyan ( Mumbai ) secure service
Â
delphi-interfaces.pdf
1. 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.
2. 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
3. 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:
4. 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.
5. 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:
6. // 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.
7. 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.
8. 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.
9. 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.
10. 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.
11. 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.