In the source code of Delphi, I see this in the FMX.Forms
unit:
procedure TCommonCustomForm.SetHovered(const Value: IControl);
begin
if (Value <> FHovered) then
begin
....
end;
end;
I think doing Value <> FHovered
is fundamentally wrong because Value <> FHovered
can return true and at the same time both Value
and FHovered
can point to the same TControl
object. Am I wrong? (note this is what I saw in debugging).
Now a subsidiary question: why can 2 IControl
interfaces be different (from the view of pointers) but point to the same TControl
?
Note: below a sample that show how 2 IControl
can be different (from the pointer view) and still pointing to the same object:
procedure TForm.Button1Click(Sender: TObject);
var LFrame: Tframe;
Lcontrol: Tcontrol;
LIcontrol1: Icontrol;
LIcontrol2: Icontrol;
begin
Lframe := Tframe.Create(nil);
Lcontrol := Lframe;
LIcontrol1 := Lframe;
LIcontrol2 := Lcontrol;
if LIcontrol1 <> LIcontrol2 then
raise Exception.Create('Boom');
end;
Now also what could be the good way to fix this bug ?
CodePudding user response:
Problem with directly comparing interfaces is that each class can declare interface even if it was already declared in ancestor. That allows that redeclared interface can implement different methods in the derived class.
Every object instance has associated metadata attached, interface table. Interface table contains list of pointers for each declared interface pointing to the virtual method table for that particular interface. If the interface is declared more than once, each declaration will have its own entry in the interface table pointing to its own VMT.
When you take interface reference of particular object instance, value in that reference is the appropriate entry from that object's interface table. Since that table may contain multiple entries for the same interface, those values can be different even though they belong to the same object.
In context of Firemonkey, TControl
declares IControl
interface, but TFrame
which descends from TControl
also declares it. Which means TFrame
instances will have two different entries for IControl
interface in their interface table.
TControl = class(TFmxObject, IControl, ...
TFrame = class(TControl, IControl)
TFrame
redeclares the IControl
interface because it implements different GetVisible
method, which is declared as non virtual in ancestor class for the purpose of the Form Designer.
If each class in FMX hierarchy would declare IControl
only once, then simple comparison like the one in SetHovered
would work properly. But if not, then it is possible that comparison will return true for the same object.
Solution is either removing additional interface declaration which would also require implementing GetVisible
as virtual, or typecasting interfaces to objects and comparing objects, or typecasting to IUnknown
, but typecasting is slower solution from performance point of view. However, typecasting to object or IUnknown
is the best fast fix because it cannot possibly break anything else and it is not interface breaking change.
Small example that demonstrates what is going on in FMX classes TControl
and TFrame
type
IControl = interface
['{95283CFD-F85E-4344-8577-6A6CA1C20D00}']
procedure Print();
end;
TBase = class(TInterfacedObject, IControl)
public
procedure Print();
end;
TDerived = class(TBase, IControl)
public
procedure Print();
end;
procedure TBase.Print;
begin
Writeln('BASE');
end;
procedure TDerived.Print;
begin
Writeln('DERIVED');
end;
procedure Test;
var
Obj: TBase;
Intf1, Intf2: IControl;
begin
Obj := TDerived.Create;
// Obj is declared as TBase so assigning will use IControl entry associated with TBase class
Intf1 := Obj;
// Typecasting to TDerived will use IControl entry associated with TDerived class
Intf2 := TDerived(Obj);
Writeln(Intf1 = Intf2);
Writeln(TObject(Intf1) = TObject(Intf2));
Writeln(Intf1 as IUnknown = Intf2 as IUnknown);
Intf1.Print;
Intf2.Print;
end;
If you run the above code the output will be:
FALSE
TRUE
TRUE
BASE
DERIVED
Which shows that Intf1 and Intf2 when compared directly as pointers are different. When casted back to the owning object instance they point to the same object.
And when compared following the COM guidelines for which states the same COM object must return the same interface for IUnknown
they are equal (backed by the same object).
For any given COM object (also known as a COM component), a specific query for the IUnknown interface on any of the object's interfaces must always return the same pointer value. This enables a client to determine whether two pointers point to the same component by calling QueryInterface with IID_IUnknown and comparing the results. It is specifically not the case that queries for interfaces other than IUnknown (even the same interface through the same pointer) must return the same pointer value.