Introduction
Interface is a very important element in object-oriented programming language, it is described as a set of services, for the client, we are only concerned about the services provided, but not the implementation of the service, for the service-side class, if it wants to implement a service, to implement the interface associated with the service, It also does not have to interact with clients that use the service too much. This good design method has been widely used.
Early in Delphi 3 introduced the concept of the interface, at that time was entirely because of the emergence of COM, but after so many versions of evolution, Delphi interface has become an Object Pascal language part, we can completely use interface to complete our design, Without having to think about COM-related things.
So the interface in Delphi is how to achieve, many people think very complex, in fact, its essence is also some simple data structure and call rules. The author assumes that the reader already has the experience of using the interface, this article attempts to show you the implementation of the interface in Delphi, so that when you use the interface, you know its why.
The distribution of interfaces in memory
An interface is conceptually not an entity, it needs to be associated with a class that implements an interface, and if you break out of these classes, the interface becomes meaningless. But the interface still has its layout in memory, which is attached to the object's memory space.
The Delphi object is essentially a pointer to a specific memory space, the first four bytes of memory is a pointer to the VMT table of the class, then the data members of the object, if the object implements the interface, then a series of pointers in the back, we can think that these pointers are the corresponding interface, Each pointer points to an interface method table. Let's take a look at a simple example:
type
itest1 = interface
[' {5347bb0d-89b7-4674-a991-5c527be6f8a8} ']
procedure SayHello1;
end ;
itest2 = interface
[' { 567B86BB-711D-40C2-8E5E-364B742C2FF1} ']
procedure sayhello2;
end ;
ttest = class (TINTERFACEDOBJECT, ITEST1, ITEST2)
public
procedure sayhello1;
procedure SayHello2;
end ;
...
Implementation
{ ttest }
Procedure Ttest.sayhello1;
begin
ShowMessage (IntToStr (Frefcount));
ShowMessage (' Itest1 say hello ');
End;
procedure Ttest.sayhello2;
Begin
ShowMessage (IntToStr (Frefcount));
ShowMessage (' Itest2 say hello ');
End;
End.
Above is a declaration of two interfaces and a class implementing an interface, the TTest class in memory distribution can be used to represent:
Where Frefcount is a member of the parent class Tinterfacedobject, the next is to store the Tinterfacedobject implementation of the interface IInterface, and then the TTest class implementation ITest2 and ITest1 pointers respectively. Each interface pointer points to its own method table, noting that ITest2 and ITest1 are inherited from iinterface, so naturally there are all the methods of IInterface. Each pointer in the method table points to the place where the method is actually implemented, which is only temporary, and later explains what the pointers in the method table really point to and explains why.
The above memory distribution is not the author randomly think out, but after several tests confirmed, the following we use some code to confirm the above distribution map:
Var
Test:itest2;
Begin
Test: = Ttest.create;
Test. SayHello2;
End
Before proving the memory layout of the interface, it is necessary to understand what the variables of the interface are, such as what the above test is, which is essentially a pointer to null before it is assigned, and when the object is assigned, it points to the Itest2 in the above distribution map. For multiple interface variables of the same object, their "values" are not necessarily equal, such as the following code:
Var
Test1:itest1;
Test2:itest2;
Test:ttest;
Begin
Test: = ttest.create;
Test1: = Test;
Test2: = Test;
If Integer (Test1) <> Integer (TEST2) Then
ShowMessage (' It is not eqeual ');
End;
Finally, a dialog box appears stating that Test1 and Test2 are not equal, and that the two variables are equal only if the property is the same interface type, such as Test1 and Test2 are iinterface, then their "values" are equal.
OK, look back at the previous code snippet, set a breakpoint in line 4th, run the program and make the above code execution, the program executes to the breakpoint, abort, press Ctrl+alt+c call CPU window, you can see the following disassembly code:
Unit1.pas.49:test: = ttest.create;
MOV dl,$01
mov eax,[$00458e0c]; EAX point to VMT Address
Call Tobject.create; Create TTest object, eax point to the TTest object's first address
MOV edx,eax; EdX points to the eax point where edx also points to the TTest object's first address
Test Edx,edx; To test whether the TTest object is valid
JZ +$03
Sub edx,-$0c; Object First address offset 12 bytes, to ITest2 pointer
Lea EAX,[EBP-$04]; The address of the test variable is the value of ebp-04, eax points to this address
Call @IntfCopy; Call intfcopy, copy the values of edx to EAX, reference count management
Unit1.pas.50:test. SayHello2;
mov eax,[ebp-$04]; Assign the address that the test points to to eax, at which point eax points to the Itest2 address
mov Edx,[eax]; Assign the content of EAX to edx, at which point edx points to the method table that the ITest2 points to
Call DWORD ptr [EDX+$0C]; The method table that the call ITest2 points to is offset by 12 bytes.
... ...
Ret
Sub edx,-$0c This sentence, edx originally pointed to the object's memory space, offset 12 bytes exactly where? Just to the ITest2 interface pointer. Next eax points to the address of the test variable in the stack, and if it is not logically wrong to assign edx directly to EAX, it is not possible to manage the reference count for the interface. So to call Intfcopy, make an assignment to the interface address, plus a reference count.
Intfcopy is actually called the _intfcopy in the system unit, which is implemented as follows:
procedure _intfcopy ( var Dest: IInterface; const source: iinterface);
{$IFDEF purepascal}
var
p: pointer;
Begin
p := pointer (Dest); //save Dest, no reference count
if source <> Nil then
source._ addref; //increases the reference count of the Source, which increases the reference count of ITest2
pointer (Dest) := pointer (Source); // Assigns the value of source to dest, no reference count
if p <> Nil then
iinterface (P). _release; //reduces the reference count for the target interface, but P here is a null pointer, so this sentence will not be called
end ;
At this point the dest parameter is EAX, which is the address of the test variable, and the source parameter is edx, which is exactly the address of the ITest2 in the object's content space. We see that it's just a copy of the interface address and an increase in the reference count of the interface. If Dest has content, it reduces its reference count, but here dest is empty, so the code that reduces the reference count is not called.
Next to the call DWORD ptr [Edx+$0c],edx points to ITest2 point to the method table head address, and edx+$0c offset to where, look at the memory diagram above, just to ISayHello2. At this point, call ISayHello2 to address the code, we can simply consider to call Ttest.sayhello2. But that's not the truth, why? Because before calling SayHello2, you first specify the value of EAX as the self pointer of the TTest object, which is passed into SayHello2 as an implicit argument.
We can take a look at [edx+$0c] 's address, press F8 to execute the execution point to the call DWORD ptr [edx+$0c], then press F7, and jump to [edx+$0c] address, you can see the following disassembly code:
Add eax,-$0c; EAX an upward offset of 12 bytes is exactly the first address of the object memory.
JMP Ttest.sayhello2; jump to Ttest.sayhello2.
Look closely at the front of the sink code, you can know that eax just point to the ITest2 pointer, the upward offset of 12 bytes is good to the first address of the object memory. Then call Ttest.sayhello2 to finish.
The above example not only proves the layout of the interface in the object memory space, but also draws the following conclusions:
1. When an object that implements a particular interface is created and assigned to the interface, the compiler does some work to point the interface variable to a specific address in the object's memory.
2. When invoking the method of an interface, it is actually called a specific address in the interface method table, where the compiler calculates the first address of the object memory that implements the interface, and then invokes the corresponding method of the object.
The formation of Interface memory space
The previous section describes how the interface is distributed in object memory space, but the object memory space is generated at run time, and how the memory space of the interface is generated, as described in this section.
Before that, let's go back to the object memory graph above, the object memory of the first address is a pointer to a VMT table, and the Delphi class is actually a pointer, this pointer is also pointing to the VMT table. Classes are determined at compile time, and the VMT table is, of course, compiler-generated.
The VMT table is a pointer at the negative offset vmtintftable (-72) Byte, which points to the following data structure: pinterfacetable = ^tinterfacetable;
tinterfacetable = Packed record
Entrycount:integer;
ENTRIES:ARRAY[0..9999] of Tinterfaceentry;
End
EntryCount represents the number of interfaces implemented by an object.
Entries is a pointer to an array of tinterfaceentry structures, and Tinterfaceentry represents an interface entry point, which is declared as follows:
Pinterfaceentry = ^tinterfaceentry;
Tinterfaceentry = Packed record
Iid:tguid;
Vtable:pointer;
Ioffset:integer;
Implgetter:integer;
End
The IID represents the GUID of the interface, and if the interface does not specify a GUID, the value inside it is all 0.
Vtable the method table that points to the interface.
Ioffset indicates the offset of the interface from the first address of the object.
Implgetter is a method pointer that, when Ioffset is not available, points to the address of the interface, typically not, initialized to 0.
The above data structure is generated at compile time, so how is the corresponding interface memory generated when an object is created? After the object is created, the Tobejct.initinstance (Instance:pointer) class method is called to initialize the object's data. Look at its code:
classfunctionTobject.initinstance (instance:pointer): TObject;
{$IFDEF purepascal}
var
intftable:pinterfacetable;
Classptr:tclass;
I:integer;
begin
Clear All Objects 0
Fillchar (instance^, instancesize, 0);
Specifies that the first address is self, which is a pointer to VMT
Pinteger (Instance) ^: = Integer (self);
Classptr: = self;
Establishing an interface memory distribution for an object
whileClassptr <>Nil Do
begin
Get Interface Table
Intftable: = classptr.getinterfacetable;
ifIntftable <>Nil Then
forI: = 0 toIntftable.entrycount-1 Do
withIntftable.entries[i] Do
begin
ifVTable <>Nil Then
Object offset at Ioffset, set to pointer to vtable
Pinteger (@PChar (Instance) [Ioffset]) ^: = Integer (VTable);
End;
Continue to establish interface memory memory for its parent class
Classptr: = classptr.classparent;
End;
Result: = Instance;
End;
We look at Pinteger (@PChar (Instance) [Ioffset]) ^: = Integer (VTable), @PChar (Instance) [Ioffset] is the address of the object offset Ioffset, And Ioffset is the ioffset of intftable.entries[i], this value is specified at compile time, and is the offset value of interface to object. So, after the method call above, the object's memory space is the same as it was previously drawn.
Now that we have a good understanding of the interface in memory, we can use this knowledge to implement some very powerful functions. In our experience, object generation can be directly assigned to an interface, and the compiler automatically offsets the pointer to the interface. But if, in turn, assigning an interface to an object is not allowed, because the information is not enough Ah, any class can implement this interface, the compiler does not know that this interface is implemented by that class, so it is impossible to convert. If we provide a real class of this interface, and then according to the interface information in the VMT of the class, we can get ioffset, so that we can not offset to the object's first address, the following routine can get the object implementing the interface from an interface, provided that the class that implements this interface must be provided:
functiongetobjfromintf (Aclass:tclass;ConstIntf:iinterface): TObject;
var
pintftable:pinterfacetable;
Intfentry:tinterfaceentry;
I:integer;
begin
Result: =Nil;
Get Interface Table structure
Pintftable: = aclass.getinterfacetable;
ifPintftable =Nil ThenExit;
whileAClass <>Nil Do
begin
forI: = 0 topintftable^. EntryCount-1 Do
begin
Intfentry: = pintftable^. Entries[i];
Determine if the address that the interface table points to is the same as the address that the incoming interface points to
ifPpointer (Intf) ^ = Intfentry.vtable Then
begin
Offset to the first address of the object
Result: = TObject (Integer (Intf)-intfentry.ioffset);
Exit;
End;
End;
Continue to find in the parent class
AClass: = aclass.classparent;
End;
End;
Look at the following example:
Var
Intf:itest2;
Obj:ttest;
Begin
Intf: = ttest.create;
Intf.sayhello2;
OBJ: = TTest (getobjfromintf (TTest, Intf));
Obj.sayhello1;
End;
Executes the above code, first pops up the Hello2 dialog, then pops up the Hello1 object, shows that the getobjfromintf function executes successfully, we implemented from the interface to the object the transformation process.
Reference count of interfaces
The memory space of the above interface is compatible with the COM interface on the binary, that is, the interface is a pointer to vtable, and another feature that is compatible with COM is the automatic management of the life cycle of COM objects by reference counting. C + + programmers must manually manage the increase or decrease of the reference count, and the Delphi compiler helps us do these things, because the reference count is regular, as long as you follow these rules, you can automatically manage the increase or decrease of the reference count. IInterface's statement is as follows:
IInterface = interface
[' {00000000-0000-0000-c000-000000000046} ']
function QueryInterface (const IID:TGUID; out OBJ): HResult; stdcall;
function _addref:integer; stdcall;
function _release:integer; stdcall;
End
Any class that implements IInterface must implement the above three methods, where _addref and _release are implemented with reference count management. Delphi provides the Iinterfaceobject class default implementation interface, which declares a member Frefcount:integer specify a reference count, and _addref is called when it simply adds 1 Frefcount:
Result: = InterlockedIncrement (Frefcount);
_release is called when the Frefcount is reduced, and if Frefcount is 0 o'clock, the call destroy destroys itself:
Result: = InterlockedDecrement (Frefcount);
If Result = 0 Then
Destroy;
If you want to implement an interface and do not want to manage the lifecycle by reference counting, you can simply return the result to 1 in AddRef and release, which is the case with the Tcomponent class.
So Delphi is how to implement the management of interface reference count, there are the following rules:
1. When a non-null interface variable is to be assigned to another interface variable, the non-null interface variable should be called AddRef.
2. When a non-null interface variable is to be assigned a value by another interface variable, the non-null interface variable should be called release.
3. If you have enough knowledge of the reference count of the interface, some AddRef and release can be optimized.
For the first case, there is already a description in the previous section that looks at the _copyintf code. In the second case, in the case of an interface variable declaration and application, the compiler invokes _intfclear at the end of the routine, with the following code:
function _intfclear (var dest:iinterface): Pointer;
{$IFDEF purepascal}
Var
P:pointer;
Begin
Result: = @Dest;
if Dest <> Nil Then
begin
P: = Pointer (Dest);//Save interface first
Pointer (Dest): = nil;//Empty The interface
IInterface (P). _release;//call the original interface method to reduce the reference count
End;
End;
From the above, we can not arbitrarily call _addref and _release, otherwise it will disrupt the interface reference count, like the above code, just call the _release, if the object's reference count is not 0, it will not be released.
On the interface reference count, to the compiler to manage the line, we just follow some rules, we can flexibly use the interface for program design.
Conversion of interfaces
Another feature of the interface is that multiple interfaces implemented by a class should be convertible to each other. The method is to call QueryInterface (const IID:TGUID; out OBJ): HResult;
For the implementation of this feature, I do not want to wordy here, in fact, as long as the first part and the second part of understanding, this feature is very easy to infer how to achieve, not to mention the source code is there, why not give yourself a chance to practice?
The bottom implementation of the Delphi interface (the interface still has its layout in memory, it is attached to the object's memory space, there is a compilation interpretation)-Interface memory structure diagram, simple and clear, deep good