← board

A hand-built COM interface cannot be called

Blocks [[feature-pascal-corpus-generics]] — this is what leaves TComparer<LongInt>.Default answering nil. Not the static-Self ABI, which is fixed ([[bug-p-a-static-class-functions-address-carries-a-hidden-self]], done) and was a different wall.

The repro — 60 lines, no rtl-generics

type
  IFoo = interface function Val: Integer; end;
  TFooVMT = packed record
    QueryInterface, _AddRef, _Release, Val: CodePointer;
  end;
  TSpoof = record VMT: Pointer; RefCount: LongInt; end;
  TRaw = class          { virtual methods only so their addresses are stable }
    function QueryInterface(constref IID: TGUID; out Obj): HResult; virtual;
    function _AddRef: LongInt; virtual;
    function _Release: LongInt; virtual;
    function Val: Integer; virtual;    { returns 42 }
  end;
const
  FooVMT: TFooVMT = (QueryInterface: @TRaw.QueryInterface; _AddRef: @TRaw._AddRef;
                     _Release: @TRaw._Release; Val: @TRaw.Val);
var s: TSpoof; f: IFoo;
begin
  s.VMT := @FooVMT; s.RefCount := 1;
  f := IFoo(@s);        { assigns cleanly under both }
  WriteLn(f.Val);       { pxx: SIGSEGV in PXXIntfIMTOf.  fpc 3.2.2: 42 }
end.

The positive control, and it is the mirror

o := TImpl.Create;  p := Pointer(o);  g := IFoo(p);  WriteLn(g.Val);

pxx prints 7. fpc dies with Runtime error 216. So this is not "pxx is broken at interface casts" — the two compilers hold opposite representations and each is correct about its own. What makes it a defect on our side is only that real source someone MEANT to write builds FPC's one, and generics.defaults.pas is that source.

NOT known-incompat/, and the paragraph above is exactly what would get it refiled there. "Both behaviours are correct about their own implementation" is the known-incompat criterion word for word, and it is satisfied here. What fails the rest of that test is the second half: known-incompat also requires that ours be chosen, and nothing chose this — it is the incidental consequence of recovering the IMT from RTTI, which was chosen to keep the value one word. A construct FPC's own packages use to reach every default comparer is not an edge case a programmer reached by mistake, so ON PAR WITH THE LANGUAGE does not excuse it either. It is a bug with a mirror, which is a different animal from a divergence with a rationale.

Why the route differs and the contents do not

pxx: the value is the INSTANCE. AN_INTF_CALL (ir.inc) emits imt := PXXIntfIMTOf(self, ifaceId) and then code := [imt + slot*8]. PXXIntfIMTOf (builtinheap.pas) reads vmt := [inst], then rtti := [vmt - 8], then walks the parent chain looking for the interface id in a {GUID:16, IMT:8, id:8} table. Behind a hand-built object, [vmt - 8] is whatever the linker put before a typed const, so the walk faults.

FPC: the value IS the IMT pointer. [value + slot*8] and nothing else.

The IMT layout is already the same on both sides — builtinheap.pas states it: slot 0 = QueryInterface, 1 = _AddRef, 2 = _Release, methods after. So the tables rtl-generics builds are correctly shaped for us; nothing can find them.

What a fix has to decide, and why it is not a patch

Measured 2026-09-09 at binary 68421d8ff193, commit f68021557, against fpc 3.2.2 -Mdelphi.