← board

passing a CLASS instance to an INTERFACE parameter stores a raw object pointer — later interface calls jump into data

Naming note (2026-07-14, after a user question): FPC's "COM" and "CORBA" interface modes have NOTHING to do with Microsoft COM or OMG CORBA. They are FPC's names for two in-process Pascal interface flavours: COM mode = descends from IUnknown, refcounted (ARC); CORBA mode = no IUnknown, no refcounting. Both are platform-neutral and both are used heavily by FPC/Lazarus code ON LINUX (TInterfacedObject, the whole fcl). This ticket is NOT a Windows-compat item. We implement both modes already; what diverges is the VALUE MODEL below, and it breaks Linux code exactly as it would break Windows code.

Repro shape (fcl-fpcunit, unmodified)

type TTracer = class(TInterfacedObject, ITestListener) ... end;
var L: TTracer;
L := TTracer.Create;
Res.AddListener(L);        { AddListener(AListener: ITestListener) }
Res.  ... run ...          { TTestResult.StartTest iterates FListeners and
                             calls ITestListener methods }

Crashes at call *0x20(%rax) with rax pointing into RODATA — the "interface" retrieved from the listener list is not a fat pointer / dispatchable value. Passing TTracer.Create directly as the argument does not even parse ("near: AddListener TTracer Create").

What works vs what doesn't

ANALYZED 2026-07-13 (fable-nightA) — root cause is the VALUE MODEL, not arg coercion

Class→interface ARG coercion works (minimal repros pass, plain and COM/GUID, incl. storing through a param into a field and dispatching later). What fpcunit actually does is:

FListeners.Add(pointer(AListener));            { TFPList of raw pointers }
...
ITestListener(FListeners[i]).StartTest(ATest); { cast back, then call }

Pointer(intf)ITestListener(ptr) is an idiomatic FPC ROUNDTRIP: an FPC COM interface value IS one pointer (methods dispatch through [ptr] with compiler thunks adjusting Self). Our interface value is a 16-byte CORBA fat pointer {IMT, instance} — pointer(intf) can only keep one word, and the rebuilt "interface" dispatches through garbage (observed: call *0x20(%rax) with rax in rodata).

So the fix is a MODEL decision: real single-pointer COM interfaces (embedded per-interface vtables in the instance + Self-adjusting thunks), or an interning table mapping the fat pair to a stable handle so Pointer() roundtrips. The former is FPC's ABI and what Delphi-shaped code keeps assuming; the latter is a shim with lifetime questions. Track A ticket-sized either way.

Why it matters

fpcunit's real console/XML runners attach listeners this way; any COM/CORBA callback registration does. Currently silent memory corruption.

Gate

make test + self-host byte-identical; a b-test with a unit-declared interface

RESOLVED 2026-07-14 (b337) — interface values are now ONE pointer, FPC's ABI

Root cause was the VALUE MODEL, as analysed above, and it is fixed: an interface value is the INSTANCE pointer (8 bytes), not a fat {IMT, instance} pair. The IMT is recovered from the instance's class RTTI blob by interface id at the call (PXXIntfIMTOf) — a short table walk, deliberately trading a few ns for correctness; optimisable later to hidden per-interface vtable fields WITHOUT changing semantics, since the value model is now right.

Verified: fcl-fpcunit's real ITestListener attaches to a TTestResult and receives all 203 StartTest callbacks while the fcl-json suite stays 203/203 — that is the exact code that segfaulted. Also pinned in test/test_interface_single_pointer_abi_b337.pas: Pointer(intf) roundtrip (via a variable, inline, and out of a pointer-shaped container), cast-and-call on a getter result, identity as a plain pointer compare, SizeOf(intf) = SizeOf(Pointer).

Fixed on the way (both silent):

Gate: full tier GREEN + self-host fixedpoint; ARC refcounts still exact (test_interface_arc); CORBA/multi-interface tests green.

Log