In a comment on the recent interface-list blog post Huseyn asks:
“In Delphi we expect all interfaces be descendants of IUnknown [or IInterface - HV], but in [the] C world there are many even more basic interfaces which are not inherited from IUnknown. I came across one of them in a DLL which I need to use. But I couldn't do it using Delphi.”
It is true that a Delphi interface declaration always implicitly inherits from IUnknown or IInterface (to distinguish between COM interfaces and Delphi language interfaces). This means that all interfaces have the three methods QueryInterface, _AddRef and _Release. This makes it hard (or impossible) to implement a pure interface from a non-COM DLL using a Delphi interface declaration.
Back in the dark ages of Delphi 2, there where no explicit interfaces and COM support was built on the fact that the VMT table layout matched COM’s binary contract, and “interfaces” were declared using pure abstract base classes. This is how interfaces are still declared in C++, for instance. The class that wanted to implement such an “interface” would simply inherit directly from the pure abstract class, overriding and implementing the all the abstract methods. This was a very restrictive way of implementing interfaces; due to the fact the Delphi does not support multiple inheritance of classes - if you wanted to support multiple interfaces, you had to write one class per interface and manually write the QueryInterface method to return the correct interface/object reference. C++ does not have that restriction and is probably why it doesn’t have a separate language construct for interfaces, in contrast to languages such as Java, Delphi and C# that all have a single-inheritance model.
This little history lesson should give us a clue of how we can avoid the IUnknown methods of normal interfaces – we can simply declare and implement a pure abstract class instead.
For instance:
program TestPureInterface;
{$APPTYPE CONSOLE}
type
TMyPureInterface = class
procedure FirstMethod stdcall; virtual; abstract;
procedure SecondMethod stdcall; virtual; abstract;
end;
TMyImplementation = class(TMyPureInterface)
public
procedure FirstMethod; override;
procedure SecondMethod; override;
end;
procedure TMyImplementation.FirstMethod;
begin
Writeln('TMyImplementation.FirstMethod');
end;
procedure TMyImplementation.SecondMethod;
begin
Writeln('TMyImplementation.SecondMethod');
end;
procedure TestClient(PureInterface: TMyPureInterface);
begin
PureInterface.FirstMethod;
PureInterface.SecondMethod;
end;
var
MyImplementation: TMyImplementation;
begin
MyImplementation := TMyImplementation.Create;
TestClient(MyImplementation);
readln;
end.
This little sample code first declares a pure abstract class with to virtual abstract methods. This is a declaration of the COM-less interface the C-based DLL in question expects to talk to. Note that I assume the calling convention is stdcall and the specific ordering and semicolon use the compiler insists upon (which disagrees with Error Insight in Delphi 2006):
procedure FirstMethod stdcall; virtual; abstract;
Then we implement the “interface” by writing a class that inherits from the abstract base class, implementing the required methods. Notice that the compiler does not require that we repeat the calling convention for the overrides:
procedure FirstMethod; override;
Finally I’ve written some test code that exercises calling the implemented methods through the “interface” reference, typed as the abstract base class. This corresponds to the C code in the DLL we’re providing the interface implementation for.
That has got to be the single most type unsafe approach I have ever seen.
ReplyDeleteThanks for a very interesting post. Can you assume that the structure of your abstract Delphi class' VMT is equivalent to that of the C interface?
ReplyDeleteWhat if the DLL also provides the implementation? How would you typecast the returned pointer to your Delphi class to be able to call its methods?
I've recently come across a DLL (Subversion client API) which declares a pure interface and also exports a function to return an implementation of it. I assumed I had to translate the interface to Delphi as a record type with method pointers.
"Anonymous said...
ReplyDeleteThat has got to be the single most type unsafe approach I have ever seen."
What do you think is type unsafe about it? It is just as safe as COM interfaces is.
"TOndrej said...
Can you assume that the structure of your abstract Delphi class' VMT is equivalent to that of the C interface?"
Well, I guess that depends on how you define a "C interface". If it follows the binary interface of Win32 COM interfaces (bar the requirement to have the IUnknown methods), it should work with abstract base classes in Delphi.
"What if the DLL also provides the implementation? How would you typecast the returned pointer to your Delphi class to be able to call its methods?"
Cast it to your TMyPureInterface.
"I've recently come across a DLL (Subversion client API) which declares a pure interface and also exports a function to return an implementation of it. I assumed I had to translate the interface to Delphi as a record type with method pointers."
That wont work. A record cannot have virtual methods, so there will be no virtual dispatch. I haven't looked into the Subversion API, but I' guess that redeclaring the interfaces using pure abstract classes in Delphi should work.
It seems I didn't make myself clear, sorry. The things I'm talking about are declared in C as structs, too. You can see one simple example here: http://tinyurl.com/l3b43
ReplyDeleteI don't think that kind of declaration can be written as a Delphi class, do you? I think the hidden Self parameter would produce invalid function signatures. What I could do is write helper Delphi classes to encapsulate this mess.
I've just tried to call the API from Delphi (using a record type with a function pointer) and the code seems to work.
I think I've also seen an instance of such "interface" being implemented within the DLL and returned by an exported function but I cannot find it at the moment.
Anyway, thank you for this blog, it's a lot of interesting stuff, keep up the great work!
Ah - I see.
ReplyDeleteIt's just a record that contains one or more more procedural fields - each field expplicitly assigned to a global routine that should handle the callback.
I would't really call these interfaces, though. They are not object oriented and doesn't follow any standard binary protocol (such as COM - due to the absence of the Self parameter). As I said above - it all depends how you define "interface" ;).
Hallvard, thank you for an answer. I have followed your advice and I think I made a progress, but now I get an access violation at the very end.
ReplyDeleteLet me show if I am a good student by the code I created with your advice and then you can say if I made a mistake.
So here is the c interface in dll
class ICustomSource {
public: virtual HRESULT setCustomSourceAndLoad(IFilterDataSource* inDataSource) = 0;
};
where IFilterDataSource defined as following
class IFilterDataSource
{
public:
//Empty Constructor and destructor to ensure proper deletion
IFilterDataSource(void) {}
virtual ~IFilterDataSource(void){}
virtual unsigned long seek(unsigned long inPos) = 0;
virtual void close() = 0;
virtual bool open(string inSourceLocation, unsigned long inStartByte = 0) = 0;
virtual void clear() = 0;
virtual bool isEOF() = 0;
virtual bool isError() = 0;
virtual unsigned long read(char* outBuffer, unsigned long inNumBytes) = 0;
virtual string shouldRetryAt() = 0;
};
class CustomSourceClass
: public IFilterDataSource
{
public:
CustomSourceClass(void);
virtual ~CustomSourceClass(void);
//IFilterDataSource Interface
virtual unsigned long seek(unsigned long inPos);
virtual void close();
virtual bool open(string inSourceLocation, unsigned long inStartByte = 0);
virtual void clear();
virtual bool isEOF();
virtual bool isError() { return false; }
virtual unsigned long read(char* outBuffer, unsigned long inNumBytes);
virtual string shouldRetryAt() { return ""; }
//
protected:
fstream mSourceFile;
};
Here is analog of what i want to do in c++
ICustomSource* OggCustSource = NULL;
...
SomeDll->QueryInterface(IID_ICustomSource, (void**)&OggCustSource);
CustomSourceClass* FileSourceInterface = new CustomSourceClass;
FileSourceInterface ->open("D:\\testfile.ogg");
OggCustSource->setCustomSourceAndLoad(FileSourceInterface);
And here is what I did in delphi following your advice:
IFilterDataSource = class
function seek (inPos:LongInt ) :LongInt stdcall; virtual; abstract;
procedure close() stdcall; virtual; abstract;
function open( inSourceLocation:PWideChar; inStartByte:LongInt =0):bool stdcall; virtual; abstract;
procedure clear() stdcall; virtual; abstract;
function isEOF() :bool stdcall; virtual; abstract;
function isError() :bool stdcall; virtual; abstract;
function read(outBuffer: pchar; inNumBytes: LongInt ):LongInt stdcall; virtual; abstract;
function shouldRetryAt():PWideChar stdcall; virtual; abstract;
end;
CustomSourceClass= class( IFilterDataSource)
public
function seek (inPos:LongInt ) :LongInt ; override;
procedure close(); override;
function open( inSourceLocation:PWideChar; inStartByte:LongInt=0):bool;override;
procedure clear() ; override;
function isEOF() :bool; override;
function isError() :bool; override;
function read(outBuffer: pchar; inNumBytes: LongInt):LongInt; override;
function shouldRetryAt():PWideChar; override;
// construction / destruction
Constructor Create;
destructor Destroy; override;
private
FData: Tfilestream;
end;
ICustomSource = class
function setCustomSourceAndLoad(pinDataSource: IFilterDataSource):HResult stdcall; virtual; abstract;
end;
-------------------------
var
OggCustSource: ICustomSource;
FileSourceInterface: IFilterDataSource;
begin
oggCustSource:=nil;
SomeDll.QueryInterface(CLSID_ICustomSource, OggCustSource);
FileSourceInterface:=nil; FileSourceInterface:=CustomSourceClass.Create();
FileSourceInterface.Open('D:\\testfile.ogg');
OggCustSource.setCustomSourceAndLoad(FileSourceInterface);
end;
And here I get accessviolation.
One thing i suspect there is that in c interface constructor and destcutor of IFilterDataSource are empty and I didn't how to realize this in delphi.
"And here I get accessviolation."
ReplyDeleteIt looks like the C++ declarations does not explicitly specify a calling convention. The default is cdecl - so I would try to change from "stdcall" to "cdecl" in your Delphi code.
"One thing i suspect there is that in c interface constructor and destcutor of IFilterDataSource are empty and I didn't how to realize this in delphi."
I think you can safely ignore these.
Instead of abstract classes, I typically use my own version of TInterfacedObject in which the reference counting is disabled:
ReplyDeleteunit InterfaceClasses;
interface
uses
Contnrs;
type
TInterfacedObjectNR = class(TInterfacedObject)
protected
function _AddRef: integer; stdcall;
function _Release: integer; stdcall;
end;
implementation
{ TInterfacedObjectNR }
function TInterfacedObjectNR._AddRef: integer;
begin
Result := -1;
end;
function TInterfacedObjectNR._Release: integer;
begin
Result := -1;
end;
end.
Then, when I make my actual class, I inherit like so:
TMyClass = class(TInterfacedObjectNR, IMyOneInterface, IMyOtherInterface, IMyOtherOtherInterface)
What do I get for this over using abstract classes?
- multiple inheritance, or parallel polymorphism
- compile-time checking of whether required methods have been implemented.
As far as I can tell, this approach doesn't use COM and my object must be created and free like normal TObject classes.
There is fierce debate in my office about the merits of this approach over Abstract classes, but this is the approach I prefer.
Doesn't this implementation defeats the purpose of Interfaces - multipleinheritance???
ReplyDeleteActually, there is no need for interfaces to be implemented by a class. An interface is simply a pointer to a pointer to an array of pointers to "methods". These methods can just as well be a bunch of standalone procedures and functions, as long as they follow the calling convention and have an "extra" first parameter representing "self". Of course some casting is required, here and there. This is how interfaces (usually, but not necessarily, COM) are implemented in C.
ReplyDeleteThis can come in handy if you want to implement singleton interfaces, i.e. an instance simply has no data but only a bunch of methods. They don't need allocation, nor translation of the self pointer to point to the base of the implementing class, so they can be a lot faster than normal interfaces.