1

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?

Wim ten Brink
  • 25,901
  • 20
  • 83
  • 149
  • Be aware: Only the System unit is allowed! That's a severe limitation... – Wim ten Brink Nov 19 '20 at 13:05
  • I think this is probably a dupe of these: https://stackoverflow.com/questions/51368925/is-there-a-way-to-get-guid-from-a-generic-constraint-type and https://stackoverflow.com/questions/4493013/how-to-test-the-type-of-a-generic-interface – David Heffernan Nov 19 '20 at 13:19
  • I know you say that you can only use the `System` unit, because you need to be cross platform, but you probably mean the `System` namespace. In which case the RTTI based solutions in the dupes is probably the best you can do. – David Heffernan Nov 19 '20 at 13:40
  • @DavidHeffernan, I have to avoid the use of RTTI units. It is a severe limitation, but if I have to include more units then I prefer to not use this class at all. Other developers will have to maintain this in the future and they will be less experienced than me. I don't want to encourage them to go deeper into the RTTI. So if it's not possible then it's not a big problem. Just an annoyance. – Wim ten Brink Nov 19 '20 at 18:30
  • @WimtenBrink RTTI is cross-platform, there is no need to avoid the RTTI units. And you can't solve your Generic problem without using RTTI. – Remy Lebeau Nov 19 '20 at 18:32
  • @RemyLebeau As I said, I only want to use the Systems unit. SysUtils is not an option. As for Supports(), it just wraps around the QueryInterface method. Besides, I've tested it and it too doesn't work with this generic method. Same error. As for the This method... I want to avoid the use of typecasting in the code. It's a method of just one line of code, anyways. (It returns Self.) – Wim ten Brink Nov 19 '20 at 18:36
  • @WimtenBrink "*SysUtils is not an option*" - WHY, though? It is available on multiple platforms. Why are you so restricted to just the `System` unit for cross-platform coding? That is a very unnecessary restriction. "*Supports() ... just wraps around the QueryInterface method ... I've tested it and it too doesn't work with this generic method. Same error*" - I wasn't suggesting you use `Supports()` *inside of* the `Cast` class, I was suggesting you use `Supports()` *instead of* the `Cast` class. – Remy Lebeau Nov 19 '20 at 18:40
  • Demanding that a solution exists given these constraints doesn't make it so. I find it very hard to scratch my nose when my hands are tied behind my back. Telling myself that I will scratch my nose doesn't change the impossibility of doing so. – David Heffernan Nov 19 '20 at 18:49
  • @RemyLebeau This unit will hold a lot of interface definitions and I'm trying to keep it as limited as possible. So if 'Cast' cannot be implemented without other units then I just keep it out of this unit. (And put it in a separate unit that can make use of other units!) This specific unit needs to stay clean of any other units. And using Supports() elsewhere would be the alternative if Cast cannot work without other units. – Wim ten Brink Nov 19 '20 at 18:52
  • @DavidHeffernan The limitations are just for this specific unit. Elsewhere in the code I can work around this easily, but this unit needs to stay clean. It's a base unit that should have no dependencies. But for me it's just weird that Delphi supports generic methods, yet it fails this specific function when using a Generic while it succeeds if I use an interface in a non-generic way. – Wim ten Brink Nov 19 '20 at 18:57
  • 1
    @WimtenBrink Generics are not quite as flexible as people expect them to be. More times than not, you end up needing to employ hacks to work around limitations in them. I suggest you [report this issue to Embarcadero](https://quality.embarcadero.com) as a compiler bug. – Remy Lebeau Nov 19 '20 at 19:03
  • @RemyLebeau You're right. This seems to be a compiler bug. I do hate the need of hacks to get things to work. I'm too used to Generics in C# anyways. I'm expecting too much from Delphi. :) – Wim ten Brink Nov 19 '20 at 19:10
  • I don't think it's a compiler bug. It's one of many similar limitations of the design. But it's not a bug in the sense that the compiler and libraries are behaving as designed. – David Heffernan Nov 19 '20 at 19:14
  • As far as being a unit that doesn't have any dependencies, that's reasonable. What is not reasonable is to decide that you can impose such a requirement and not be limited in what can be achieved. – David Heffernan Nov 19 '20 at 19:15
  • @DavidHeffernan The problem is that this unit will be maintained by other developers once it's ready. Some of them very inexperienced. The restriction of "no units" is to make sure they're not going to include any trouble in the future. And the reason for using interfaces is because they often forget to free objects and interfaces generally free themselves. But Delphi not being able to handle restricted generics the same way as regular interfaces is unexpected. And bad in my opinion, as the compiler should be smart enough to recognize these situations. – Wim ten Brink Nov 20 '20 at 14:14
  • 1
    "The problem is that this unit will be maintained by other developers once it's ready." So what. Define the rules. Make them clear. Supervise these developers. " And the reason for using interfaces is because they often forget to free objects and interfaces generally free themselves." Teach them. If they can't learn this, then let them go. If they can't learn this, then they won't produce anything of lasting value. In reality, the problem is not that the junior developers are inexperienced, the problem is weak leadership. – David Heffernan Nov 20 '20 at 15:03
  • "But Delphi not being able to handle restricted generics the same way as regular interfaces is unexpected. And bad in my opinion, as the compiler should be smart enough to recognize these situations." Sure, it would be good if the compiler / language had more functionality in the area of generic constraints. But it is what it is. And you yourself setting imposing further limitations seems bizarre. It's like you complain about a limitation of the tool, and then decide to impose further limitations. I've never come across such a strange and warped decision. – David Heffernan Nov 20 '20 at 15:06

1 Answers1

1

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);
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • SysUtils.Supports is doing exactly the same as QueryInterface and fails for the same reason. Adding additional units makes this unit a bit more complex, which I want to avoid as it will be used by almost every other unit in the project. So if this functionality cannot be added in this unit then I'll just remove it as that Supports() can be used everywhere else, if need be. But I was hoping for a more generic solution in this one unit. – Wim ten Brink Nov 19 '20 at 18:42
  • The This is used for List Views and other List classes with objects attached. Rather than requiring (Data as TObject) I just want to use Data.This instead. The class definitions are hidden in the code so I can't typecast to any specific class. (Which is on purpose!) The only reason why it's of type TDataObject instead of TObject is because TObject has no IInterface support. (Need TInterfacedObject for that.) Thus, if need be the TDataObject.Items can be cast back to a specific interface, if need be. (TDataObject.This as ISomeInterface) – Wim ten Brink Nov 19 '20 at 18:48