havrlisan 24 Posted June 9, 2023 (edited) I've encountered a bug with generics which I believe is a mistake in the compiler. I'll try to be as detailed as possible throughout the code below. I'm only interested in opinions on what is causing this issue in the low-level code, and on possible workarounds that are not entirely different from the implementation I intended to do here. Note that this is a bare minimum of code implementation necessary to reproduce the bug. TL;DR skip to HERE to see where the bug occurs. program GenericsBugConsole; {$APPTYPE CONSOLE} {$R *.res} uses System.Classes, System.SysUtils, System.Generics.Collections; type // Simple child class and interface inheriting from TInterfacedObject. // Contains one property necessary to demonstrate the bug (by invokation). IAncestorObject = interface ['{8B57E255-F48D-4982-B9AF-71A6ABBCDBA4}'] function GetID: TGUID; property ID: TGUID read GetID; end; TAncestorObject = class(TInterfacedObject, IAncestorObject) protected function GetID: TGUID; end; // Generic list that acts as a wrapper for the generic TList<T>. // Implementation is bare minimum for demonstration purpose. IGenericList<T: IInterface> = interface ['{B449EFCE-527D-4062-924E-A0E0421B8A16}'] procedure Add(const AObj: T); function First: T; end; TGenericList<T: IInterface> = class(TInterfacedObject, IGenericList<T>) private FList: TList<T>; protected procedure Add(const AObj: T); function First: T; public constructor Create; destructor Destroy; override; end; // Classes implementation { TAncestorObject } function TAncestorObject.GetID: TGUID; begin // irrelevant Result := TGUID.NewGuid; end; { TGenericList<T> } constructor TGenericList<T>.Create; begin FList := TList<T>.Create; end; destructor TGenericList<T>.Destroy; begin FList.Free; inherited; end; procedure TGenericList<T>.Add(const AObj: T); begin FList.Add(AObj); end; function TGenericList<T>.First: T; begin Result := FList.First; end; begin try var LList: IGenericList<IAncestorObject> := TGenericList<IAncestorObject>.Create; // HERE: // This is the place where the compiler starts acting weird. The call to Supports() passes, // and the debugger correctly shows that TCastedList is "TGenericList<IAncestorObject> as IGenericList<IInterface>". // Calling LCastedList.Add() also passes, TAncestorObject.Create will be casted to IInterface (as per arg type). var LCastedList: IGenericList<IInterface>; if Supports(LList, IGenericList<IInterface>, LCastedList) then LCastedList.Add(TAncestorObject.Create); // LObj is fetched from LList (IGenericList<IAncestorObject>), // and the debugger also shows that correctly: "TAncestorObject as IAncestorObject". var LObj := LList.First; // This is where the exception occurs. Throughout 3 different projects, I've received three different exceptions: // 1) 'c0000005 ACCESS_VIOLATION' -> Private console app, +300000 lines of code // 2) 'access violation at 0x00000001: access of address 0x00000001' -> Test FMX app, bare minimum code with TForm // 3) 'Privileged instruction' -> Not sure where I got this one, and I cannot get it again. Possibly on another test console app. var LID := LObj.ID; except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; end. Edited June 9, 2023 by havrlisan Code syntax, Pascal does not recognize multiline comments Share this post Link to post
havrlisan 24 Posted June 9, 2023 I forgot to mention a very important information: If I add the object as IAncestorObject to LCastedList (which expects IInterface), the bug does not happen. And if I add the object as IAncestorObject to the LList (which expects IAncestorObject), the bug also does not happen. Share this post Link to post
Dalija Prasnikar 1396 Posted June 9, 2023 There is not bug in compiler, but in your code. When you say LCastedList.Add(TAncestorObject.Create) What will happen is equivalent of following code: var Tmp: IInterface := TAncestorObject.Create; LCastedList.Add(Tmp); This will be because LCastedList is declared as IGenericList<IInterface> and stores IInterface references. However when you retrieve stored reference you will call that on original list which is declared as IGenericList<IAncestorObject> and retrieved reference will be of type IAncestorObject and you can try to call LObj.ID on that retrieved reference. But in reality stored reference was IInterface, not IAncestorObject and it crashes when you call ID on it because that method (GetID) it simply does not exist there. Point of generics is that you have same type stored in the background, not to store incompatible types. even though there is same object behind those two interface references, those interfaces are completely different types represent different entry points to that object. Sometimes you can force "wrong" type to be stored at runtime, which is usually not what you should be doing, but if you do, you need to make sure that every time you retrieve something that might be "wrong" you cast it back to appropriate type. var LTmp := LList.First; var LObj: IAncestorObject; if Supports(LTmp, IAncestorObject, LObj) then var LID := LObj.ID ; 1 1 Share this post Link to post
havrlisan 24 Posted June 9, 2023 14 minutes ago, Dalija Prasnikar said: There is not bug in compiler, but in your code. When you say LCastedList.Add(TAncestorObject.Create) What will happen is equivalent of following code: var Tmp: IInterface := TAncestorObject.Create; LCastedList.Add(Tmp); This will be because LCastedList is declared as IGenericList<IInterface> and stores IInterface references. However when you retrieve stored reference you will call that on original list which is declared as IGenericList<IAncestorObject> and retrieved reference will be of type IAncestorObject and you can try to call LObj.ID on that retrieved reference. But in reality stored reference was IInterface, not IAncestorObject and it crashes when you call ID on it because that method (GetID) it simply does not exist there. Point of generics is that you have same type stored in the background, not to store incompatible types. even though there is same object behind those two interface references, those interfaces are completely different types represent different entry points to that object. Sometimes you can force "wrong" type to be stored at runtime, which is usually not what you should be doing, but if you do, you need to make sure that every time you retrieve something that might be "wrong" you cast it back to appropriate type. var LTmp := LList.First; var LObj: IAncestorObject; if Supports(LTmp, IAncestorObject, LObj) then var LID := LObj.ID ; You're right, thank you. I did think that was the issue as you've written, but I thought it would be implicitly cast into IAncestorObject when either Added to TList<T> or when retrieved via First(). Share this post Link to post
Dalija Prasnikar 1396 Posted June 9, 2023 4 minutes ago, havrlisan said: You're right, thank you. I did think that was the issue as you've written, but I thought it would be implicitly cast into IAncestorObject when either Added to TList<T> or when retrieved via First(). The problem is that all variants of IGenericList<T> have same GUID so Supports function will succeed even though those different generic lists are not compatible. 2 Share this post Link to post
havrlisan 24 Posted June 11, 2023 On 6/9/2023 at 3:47 PM, Dalija Prasnikar said: The problem is that all variants of IGenericList<T> have same GUID so Supports function will succeed even though those different generic lists are not compatible. Come to think of it, a compiler directive for raising errors in such scenarios would be an awesome option. Something like overflow and range checking compiler directives. Share this post Link to post