Home > Enterprise >  What is a good way to compare 2 interfaces (IControl)? Is this a bug in Delphi?
What is a good way to compare 2 interfaces (IControl)? Is this a bug in Delphi?

Time:12-18

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).

IUnknown QueryInterface

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.

  • Related