Reputation: 26682
This is a bit puzzling for me as I'm working on an unit with several dozens of interfaces that are all based on this base interface definition:
type
IDataObject = interface(IInterface)
['{B1B3A532-0E7D-4D4A-8BDC-FD652BFC96B9}']
function This: TDataObject;
end;
ISomeObject = interface(IDataObject)
['{7FFA91DE-EF15-4220-A43F-2C53CBF1077D}']
<Blah>
end;
This means they all have a method 'This' that returns the class behind the interface, which is sometimes needed to put in listviews and stuff, but for this question it doesn't really matter because I want a generic class with additional functions that can be applied to any derived interface. (And any derived interface has their own GUID.) This is the generic class:
type
Cast<T: IDataObject> = class(TDataObject)
class function Has(Data: IDataObject): Boolean;
class function Get(Data: IDataObject): T;
end;
Doesn't look too complex and the use of class methods is because Delphi doesn't support global generic functions, unless they're in a class. So in my code I want to use Cast<ISomeObject>.Has(SomeObject)
to check if the objects supports the specific interface. The Get()
function is just to return the object as the specific type, if possible. So, next the implementation:
class function Cast<T>.Get(Data: IDataObject): T;
begin
if (Data.QueryInterface(T, Result) <> S_OK) then
Result := nil;
end;
class function Cast<T>.Has(Data: IDataObject): Boolean;
begin
Result := (Data.QueryInterface(T, Result) = S_OK);
end;
And this is where it gets annoying! Elsewhere in my code I use if (Source.QueryInterface(ISomeObject, SomeObject) = 0) then ...
and it works just fine. In these generic methods the ISomeObject
is replaced by T
and should just work. But it refuses to compile and gives this error:
[dcc64 Error] DataInterfaces.pas(684): E2010 Incompatible types: 'TGUID' and 'T'
And that's annoying. I need to fix this but can't find a proper solution without hacking deep into the interface code of the System unit. (Which is the only unit I'm allowed to use in this code as it needs to run on many different platforms!)
The error is correct as QueryInterface expects a TGUID as parameter but it seems to get that from ISomeObject. So why not from T?
I think I'm trying to do the impossible here...
To be a bit more specific: Source.QueryInterface(ISomeObject, SomeObject)
works fine without the use of any other unit. So I would expect it to work with a generic type, if that type is limited to interfaces. But that's not the case and I want to know why it won't accept T while it does accept ISomeObject.
Can you explain why it fails with a generic type and not a regular interface type?
Upvotes: 1
Views: 875
Reputation: 706
This is some handy wrapper.
It demonstrates trick of autoinitialized generic classes. IID takes long path to fetch via RTTI, so this path is taken inside generic class constructor. Sometimes when class constructors are used heavily, class is not initialized, so autoinitialization is required from class properties. Class constructors are running in single thread environment and don't require mutex. Multiple threads are assumed to only start after all class constructors, so mutex is also not required anymore. Once IID is fetched, it becomes available via generic class variable which is statically addressed in compiled program, should be fast enough.
Also trick with generic method type inference is demonstrated. Type inference is only for generic method, not for generic type, but class variables are for generic type, not for generic method, so generic method uses generic type to get everything from Delphi.
program Interfaces;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
System.TypInfo;
type
Support = record
public
class function Check<T: IInterface>
(const Instance: IInterface; out Intf: T): Boolean; overload; inline; static;
class function Check<T: IInterface>
(const Instance: IInterface): Boolean; overload; inline; static;
class function CastTo<T: IInterface>
(const Instance: IInterface): T; overload; inline; static;
class function TryCastTo<T: IInterface>
(const Instance: IInterface): T; overload; inline; static;
class function GetTypeName<T>: string; overload; inline; static;
class function GetTypeNamePtr<T>: PString; overload; inline; static;
class function GetIID<T: IInterface>: TGUID; overload; inline; static;
class function GetIIDPtr<T: IInterface>: PGUID; overload; inline; static;
private type
TTypeName<T> = record
private class var
FTypeName: string;
private
class constructor Create;
class procedure Initialize; static;
class function GetTypeName: string; static;
class function GetTypeNamePtr: PString; static;
public
class property TypeName: string read GetTypeName;
class property TypeNamePtr: PString read GetTypeNamePtr;
end;
TInterfaceIID<T: IInterface> = record
private class var
FInitialized: Boolean;
FIID: TGUID;
private
class constructor Create;
class procedure Initialize; static;
class function GetIIDPtr: PGUID; static;
class function GetIID: TGUID; static;
public
class property IIDPtr: PGUID read GetIIDPtr;
class property IID: TGUID read GetIID;
end;
end;
{ Support }
class function Support.Check<T>(const Instance: IInterface; out Intf: T): Boolean;
begin
Result := Supports(Instance, GetIIDPtr<T>^, Intf);
end;
class function Support.Check<T>(const Instance: IInterface): Boolean;
begin
Result := Supports(Instance, GetIIDPtr<T>^);
end;
class function Support.CastTo<T>(const Instance: IInterface): T;
begin
if not Supports(Instance, GetIIDPtr<T>^, Result) then
begin
raise EIntfCastError.CreateFmt
('Interface %s is not supported', [GetTypeNamePtr<T>^]);
end;
end;
class function Support.TryCastTo<T>(const Instance: IInterface): T;
begin
if not Supports(Instance, GetIIDPtr<T>^, Result) then
begin
Finalize(Result);
end;
end;
class function Support.GetTypeName<T>: string;
begin
Exit(TTypeName<T>.TypeNamePtr^);
end;
class function Support.GetTypeNamePtr<T>: PString;
begin
Exit(TTypeName<T>.TypeNamePtr);
end;
class function Support.GetIID<T>: TGUID;
begin
Exit(TInterfaceIID<T>.IIDPtr^);
end;
class function Support.GetIIDPtr<T>: PGUID;
begin
Exit(TInterfaceIID<T>.IIDPtr);
end;
{ Support.TTypeName<T> }
class constructor Support.TTypeName<T>.Create;
begin
Initialize;
end;
class procedure Support.TTypeName<T>.Initialize;
const
PrefixLength = Length('Support.TTypeName<');
SuffixLength = Length('>');
var
LocalName: string;
begin
if not FTypeName.IsEmpty then
begin
Exit;
end;
LocalName := System.TypInfo.GetTypeName(TypeInfo(Support.TTypeName<T>));
FTypeName := LocalName.Substring
(PrefixLength,
LocalName.Length - PrefixLength - SuffixLength);
end;
class function Support.TTypeName<T>.GetTypeName: string;
begin
Initialize;
Exit(FTypeName);
end;
class function Support.TTypeName<T>.GetTypeNamePtr: PString;
begin
Initialize;
Exit(@FTypeName);
end;
{ Support.TInterfaceIID<T> }
class constructor Support.TInterfaceIID<T>.Create;
begin
Initialize;
end;
class procedure Support.TInterfaceIID<T>.Initialize;
var
TypeData: PTypeData;
begin
if FInitialized then
begin
Exit;
end;
TypeData := GetTypeData(TypeInfo(T));
if TIntfFlag.ifHasGuid in TypeData.IntfFlags then
begin
FIID := TypeData.GUID;
end
else
begin
FIID := TGUID.Empty;
end;
FInitialized := True;
end;
class function Support.TInterfaceIID<T>.GetIIDPtr: PGUID;
begin
Initialize;
Exit(@FIID);
end;
class function Support.TInterfaceIID<T>.GetIID: TGUID;
begin
Initialize;
Exit(FIID);
end;
type
IFoo = interface(IInterface)
['{E4D5BA38-6E9E-4CA3-999E-5EEC8A8656C2}']
end;
TFoo = class(TInterfacedObject, IFoo);
var
Bar: IInterface;
Baz: IFoo;
begin
try
{ TODO -oUser -cConsole Main : Insert code here }
Bar := TFoo.Create;
if Support.Check(Bar, Baz) then
begin
WriteLn('Bar is supported as interface of Baz');
end;
except
on E: Exception do
WriteLn(E.ClassName, ': ', E.Message);
end;
end.
Upvotes: 0
Reputation: 598011
QueryInterface()
takes a TGUID
as input, but an interface type is not a TGUID
. The compiler has special handling when assigning an interface type with a declared guid to a TGUID
variable, but that doesn't seem to apply inside of a Generic parameter that uses an Interface constraint. So, to do what you are attempting, you will just have to read the interface's RTTI at runtime to extract its actual TGUID
(see Is it possible to get the value of a GUID on an interface using RTTI?), eg:
uses
..., TypInfo;
class function Cast<T>.Get(Data: IDataObject): T;
var
IntfIID: TGUID;
begin
IntfIID := GetTypeData(TypeInfo(T))^.GUID;
if (Data.QueryInterface(IntfIID, Result) <> S_OK) then
Result := nil;
end;
class function Cast<T>.Has(Data: IDataObject): Boolean;
begin
Cast<T>.Get(Data) <> nil;
end;
That being said, why are you duplicating functionality that the RTL already provides natively for you?
Your entire Cast
class is unnecessary, just use SysUtils.Supports()
instead (the SysUtils
unit is cross-platform), eg:
uses
..., SysUtils;
//if Cast<ISomeObject>.Has(SomeObject) then
if Supports(SomeObject, ISomeObject) then
begin
...
end;
...
var
Intf: ISomeObject;
//Intf := Cast<ISomeObject>.Get(SomeObject);
if Supports(SomeObject, ISomeObject, Intf) then
begin
...
end;
Also, your IDataObject.This
property is completely unnecessary, as you can directly cast an IDataObject
interface to its TDataObject
implementation object (Delphi has supported such casting since D2010), eg:
var
Intf: IDataObject;
Obj: TDataObject;
Intf := ...;
Obj := TDataObject(Intf);
Upvotes: 1