Jump to content
santiago

Interfaces defined in base classes

Recommended Posts

Hi there,

 

I have the following interface and two simple classes. A base class and a derived class (super class).

  IMyInterface = interface
  ['{34408757-240F-4B63-A1CA-4B1FC3BF5072}']
    procedure DoesNothing;
  end;

  TMyBaseClass = class(TInterfacedObject, IMyInterface)
    procedure DoesNothing;
  end;

  TMyClass = class(TMyBaseClass)
  end;

 

and wrote the following test method.
I would like to understand why we get the EAccessViolation with the hard (unsafe) cast.

 

procedure TMyDebugUnitTestCase.Test_MyTest1;
var
  MyObj1: IMyInterface;
  MyObj2: IMyInterface;
  MyObj3: IMyInterface;
  Obj: IInterface;
begin
  Obj := TMyClass.Create;
  CheckTrue(Supports(Obj, IMyInterface, MyObj1));
  MyObj1.DoesNothing;

  MyObj2 := Obj as IMyInterface;
  CheckNotNull(MyObj2);
  MyObj2.DoesNothing;

  MyObj3 := IMyInterface(Obj);
  CheckNotNull(MyObj3);
  MyObj3.DoesNothing;  // EAccessViolation -> Access violation at address 00000001 in module 'CoreTests.exe'. Read of address 00000001
end;

The problem also happens if I declare the derived class as follows:

 

  TMyClass = class(TMyBaseClass, IMyInterface)
  end;

 

Is it necessary to explicitly list interfaces already declared in base classes in the super class?
To me this just seems redundant. Am I correct here?
I have heard from other developers saying that this is necessary, but being unsure as to why,

 

Thank you!

 

 

Share this post


Link to post
Guest
12 hours ago, santiago said:

I would like to understand why we get the EAccessViolation with the hard (unsafe) cast.

Casting is not a language feature per se but more like a tool allowing the developer to twist the compiler arm into doing something it will not do by default, think of it like overriding, i am not saying it is totally bad or must not be used, on contrary it is useful and can short the code, and i used casting in many places, but there is many cases where it is become dangerous, like never between two managed types !, between simple types it is OK as long you know what you are doing, between simple type and managed one, here comes the danger because the compiler will not question you code and will generate code as to what it is built to do, specially for managed types, there is many magic (intrinsic) code involved and this might lead to all sort of problem, the worst of them all if it did work on you device and failed on other devices because may be the content of the stack is different, simply it will become unpredicted behaviour.

 

Anyway, i suggest to look at the assembly to get the picture of the compiler magic involved with interfaces, and when you casted it manually instead of the right way (using "as" or by extracting using "Supports") the compiler didn't have a chance to correct your code, and the code was faulty there, in other word the compiler didn't see the need to put the needed code ( in this case didn't perform QueryInterface ) because you told to handle it as IMyInterface, while in fact obj is IInterface.

Think of Interfaces like an attachments, these attachments are not referenced in the TObject VMT directly but in a table attached to the TObject/Class VMT, accessing them need some extra work and memory walk to see if they do exist and referenced in the object or not, also each interface have a reference to its object, meaning when you case using "as" on interface the compiler adds code to invoke search in the object attached to this interface and return the result, same as Supports or QueryInterface, something like reverse walk then search.

 

13 hours ago, santiago said:

Is it necessary to explicitly list interfaces already declared in base classes in the super class?

No.

Share this post


Link to post
Guest

Here an example i think will clear things a little

  IUnusedIntf = interface
  end;

procedure Test;
var
  MyObj1: IMyInterface;
  MyObj2: IMyInterface;
  MyObj3: IMyInterface;
  Obj: IInterface;
begin
..

  //MyObj3 := IMyInterface(Obj);
  MyObj3 := IUnusedIntf(Obj) as IMyInterface;   // we casted for unsed interface but the compiler casted it right, because internally it looked it up
  Assert(Assigned(MyObj3));
  MyObj3.DoesNothing;  // no more AV and DoesNothing is called
end;

 

Share this post


Link to post

This question is basically like asking why TButton(ListBox) fails when ListBox is a TListBox. That's directly analagous to your cast.

  • Like 1

Share this post


Link to post
8 hours ago, Kas Ob. said:

Casting is not a language feature per se but more like a tool allowing the developer to twist the compiler arm into doing something it will not do by default, think of it like overriding, i am not saying it is totally bad or must not be used, on contrary it is useful and can short the code, and i used casting in many places, but there is many cases where it is become dangerous, like never between two managed types !, between simple types it is OK as long you know what you are doing, between simple type and managed one, here comes the danger because the compiler will not question you code and will generate code as to what it is built to do, specially for managed types, there is many magic (intrinsic) code involved and this might lead to all sort of problem, the worst of them all if it did work on you device and failed on other devices because may be the content of the stack is different, simply it will become unpredicted behaviour.

 

Anyway, i suggest to look at the assembly to get the picture of the compiler magic involved with interfaces, and when you casted it manually instead of the right way (using "as" or by extracting using "Supports") the compiler didn't have a chance to correct your code, and the code was faulty there, in other word the compiler didn't see the need to put the needed code ( in this case didn't perform QueryInterface ) because you told to handle it as IMyInterface, while in fact obj is IInterface.

Think of Interfaces like an attachments, these attachments are not referenced in the TObject VMT directly but in a table attached to the TObject/Class VMT, accessing them need some extra work and memory walk to see if they do exist and referenced in the object or not, also each interface have a reference to its object, meaning when you case using "as" on interface the compiler adds code to invoke search in the object attached to this interface and return the result, same as Supports or QueryInterface, something like reverse walk then search.

 

No.

Thank you Kas.
What do you mean exactly by "managed types"?

If I understand correctly it is not safe to use such casts with TInterfacedObjects that implement many interfaces.

What puzzles me though is that in our source code we have several locations where we do precisely that:
 

IWhatever(SomeVariable).AMethod;

This has been used in our software for many years, way before I got started with Delphi.
The other day I removed a redundant interface declaration from a derived class. The interface was already listed and implemented in a base class. And all of the sudden some tests started failing. The reason was that such unsafe casts which had been working properly so far, no longer did.

 

Am I correct in saying that we must always use Supports or as when dealing with TInterfacedObjects and unsafe casts should be avoided unless you really know what you are doing?

 

 



 

Share this post


Link to post
2 hours ago, David Heffernan said:

This question is basically like asking why TButton(ListBox) fails when ListBox is a TListBox. That's directly analagous to your cast.

Am sorry David, but I don't quite follow.
Variable Obj is an instance of TMyClass and is declared as IInterface.

TMyClass does implement IMyInterface. So it should be a valid cast.

However when we call a method the access violation occurs.

 

We have some legacy source code that does such things:

IWhatever(SomeVariable).AMethod;

It has been running fine for years. The other day it broke because I removed an Interface declaration from a derived class, since the interface was already implemented in a base class.


So basically we had:

 

TDerivedClass = class(TBaseClass, ISomeInterface1, ISomeInterface2, ISomeInterface3)

 

and I changed it to:

 

TDerivedClass = class(TBaseClass, ISomeInterface2)


because TBaseClass already implements ISomeInterface1 and ISomeInterface3.

And suddenly

 

ISomeInterface1(AVariable).SomeMethod;

 

resulted in an access violation.

 

Thank you!

Share this post


Link to post
5 minutes ago, santiago said:

TMyClass does implement IMyInterface. So it should be a valid cast.

It is not! A cast tells the compiler to treat the given memory address as if it were of the casted type. To get a supported interface out of a class instance one has to call QueryInterface, which in the end is what the AS operator does.

  • Like 2

Share this post


Link to post
13 minutes ago, Uwe Raabe said:

It is not! A cast tells the compiler to treat the given memory address as if it were of the casted type. To get a supported interface out of a class instance one has to call QueryInterface, which in the end is what the AS operator does.

Great! Thank you! Understood now. 🙂

This means that we need to change all the locations in our legacy code that perform such casts. I am just surprised as to why we have not had any problems before that. They have been in there for ages....
And some are called rather frequently.

Share this post


Link to post
1 hour ago, Uwe Raabe said:

A cast tells the compiler to treat the given memory address as if it were of the casted type.

That's right. Which is essentially what the example in my comment was meant to demonstrate. 

  • Like 1

Share this post


Link to post
Guest
2 hours ago, santiago said:

What do you mean exactly by "managed types"?

Couldn't find a page in the documentation explain this nicely and precisely, but this should do

https://stackoverflow.com/questions/5351508/what-are-managed-types-are-they-specific-to-delphi-are-they-specific-to-window

 

And in short managed types in Delphi, are types that looks simple but hides few internal fields, sometimes inaccessible fields where compiler automatically adds code (intrinsic or magic) to manage them not only to manage their life cycle, but also for converting or accessing them.

 

1 hour ago, santiago said:

This means that we need to change all the locations in our legacy code that perform such casts. I am just surprised as to why we have not had any problems before that. They have been in there for ages....
And some are called rather frequently.

That exactly why i called it unpredictable behaviour that might work out of luck, until it fail, in your case i think the original developer had real hard time facing such AV, and shuffled the code many times until it started to work without understanding how did this happen.

I suggest that you get well familiar with "QueryInterface", "Supports" and "as", get to know how they work and when they are preferred as each has it merits, do this before starting to change your code, so you don't need to change it again later, and never use direct casting on interfaces would make sense as suggestion.

 

Also notice that the AV coming from interfaces wrong casting is cryptic and useless, unlike when casting the wrong object,

image.thumb.png.1b06148fee3e6eadf76609faa0be0ec8.png

Share this post


Link to post
11 hours ago, santiago said:

Variable Obj is an instance of TMyClass and is declared as IInterface.

TMyClass does implement IMyInterface. So it should be a valid cast.

No, it is not a valid cast, in this case.

 

Obj is declared as IInterface, so it is pointing at the IInterface portion of the TMyClass object.  But the object also has other portions in it, for TMyBaseClass, TInterfacedObject, IMyInterface, etc (not drawn exactly as the compiler lays it out, but you should get the idea):

-------------------
|     TMyClass     |
| ---------------- | <- Obj points here
| | IInterface   | |
| ---------------- |
| ---------------- |
| | IMyInterface | |
| ---------------- |
| ...              |
-------------------

You are type-casting Obj AS-IS from IInterface to IMyInterface, which tells the compiler to MIS-interpret Obj as pointing to the IMyInterface portion of the object EVEN-THOUGH it is actually pointing at the IInterface portion of the object. Whereas the 'as' operator and Support() function, which use QueryInterface() internally, will return a pointer that properly points to the IMyInterface portion of the object, eg:

-------------------
|     TMyClass     |
| ---------------- | <- IMyInterface(Obj) points here
| | IInterface   | |
| ---------------- |
| ---------------- | <- (Obj as IMyInterface) points here!
| | IMyInterface | |
| ---------------- |
| ...              |
-------------------

So, when you call IMyInterface(Obj).DoesNothing(), you are calling DoesNothing() on an invalid IMyInterface, so it does not access the correct area of the TMyClass object.

 

In order for the compiler to access the members of a TMyClass object through an IInterface pointer, an IMyInterface pointer, a TMyBaseClass pointer, etc, the pointer has to be ADJUSTED according to the offset of the pointer's dereferenced type in relation to the implementation class.  The compiler knows the offset of the IInterface portion of TMyClass, so given an IInterface pointer it knows how to adjust that pointer to reach TMyClass.  Same with IMyInterface, etc.  Thus, it adjusts a pointer according to the pointer's DECLARED type.

 

So, if you start out with an invalid pointer to begin with, those adjustments are not performed correctly, and you end up with bad behaviors, such as crashes, corrupted data, etc.

Quote

We have some legacy source code that does such things:


IWhatever(SomeVariable).AMethod;

That works only if SomeVariable is pointing at the memory address where a valid IWhatever exists.

Quote

The other day it broke because I removed an Interface declaration from a derived class, since the interface was already implemented in a base class.

And suddenly ...resulted in an access violation.

Because you altered the layout of the object in memory, but didn't update your pointer usage accordingly.

Edited by Remy Lebeau
  • Like 3

Share this post


Link to post

Wow Remy!
Thank you for such a detailed answer. 🙂

It is now crystal clear to me.

 

I never use those types of casts with interfaced objects, but since I had seen them since day 1 when I got started with Delphi I assumed it was legit. And oddly enough they have been working fine for many years!!

That is why I got so confused when I removed a redundant interface declaration and suddenly it stopped working.

 

In the meantime I have already replaced several of the unsafe casts with safe casts.
 

Share this post


Link to post
Guest

Well, this still lingering in my head, and now i don't see it a bug but may be lack of a feature or it can be better.

To explain this, lets see the following 

procedure Test;
var
  A: AnsiString;
  B: string;
  P: Pointer;
begin
  A := 'Test123';
  B := A;          //   [dcc32 Warning] : W1057 Implicit string cast from 'AnsiString' to 'string'
  Writeln(A);
  Writeln(B);
  //////////
  Writeln;
  //////////
  A := 'Test123';
  B := string(A);  //   No warning here, in fact the compiler generated the right conversion call to @UStrFromLStr
  Writeln(A);
  Writeln(B);
  //////////
  Writeln;
  //////////
  A := 'Test123';
  P := Pointer(A);
  B := string(P);  //   the content of Str is wrong, while the compiler rightfully invoked a call to @UStrLAsg
  Writeln(A);
  Writeln(B);
end;

And the result 

Quote

Test123
Test123

 

Test123
Test123

 

Test123
???3A

What is going here, in the third case the compiler is trusting the developer type casting and assigned the string as it, because it was between a managed type and non-managed type, the compiler is right here, 

in the second case the compiler override the type cast and detected the a type cast between two different managed types, both handled with special care by the compiler, and silently added the according conversion type call, also the compiler is doing nice and right work here.

 

Now to the interface, the compiler is not special handling the type casting of two interfaces, while it can , somehow it will be prettier even than using "as" or "Support", as shown in my example of typecasting with completely unused interface the code was working fine, while it could detect if the type of source from the casting and if it is not the destination then invoke "conversion" in this case invoke IntfCast on the right side of the assignment, just like what it did with two different types of string strings, in fact an automatic conversion just like assigning AnsiString to String with warning would be nice too, this will either work or will fail with raised error on runtime, that if the developer used such assigning between interfaces or between objects and interfaces, then he takes the blame for not checking against nil.

 

So two cases here, one is the type casting should be based on source type, and compiler should generate the IntfCast, and the other it might be good or not, which is to be more relaxed in simple interfaces assigning and generate the typecasting as magic silently or with warning.

 

 

On 9/30/2021 at 7:25 PM, santiago said:

To me this just seems redundant. Am I correct here?

It looks redundant for me too now, i suggest to open feature request for this.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×