← board

How does a hand-built COM interface become callable?

The measurement, the repro and the mirror control live in [[bug-a-a-hand-built-com-interface-cannot-be-called]] and are not repeated here. One sentence of it: pxx's interface value is the INSTANCE, with the IMT recovered per call from the instance's RTTI blob; FPC's value IS the IMT pointer. The IMT contents already agree.

Not the fork

Whether this is a defect at all. It is. FPC accepts — and FPC's own packages depend on — a construct we cannot run, which is compat, ranked by how much real code uses it, and the real code blocks an umbrella. The mirror control is the OTHER direction and is disposed of by us accepting what FPC rejects is not a defect. Recorded because the mirror reads like a reason to close this, and it is not one.

THIS SECTION AS FIRST WRITTEN KEYED ON A SHAPE THAT DOES NOT OCCUR, and the measurement is in (1) below (frankH, 2304d362c): generics.defaults.pas contains zero pointer-to-interface CASTS. Every one of the six conversions is an implicit ASSIGNMENT of a Pointer-typed result into an interface-typed destination. A fix keyed on the cast would compile the motivating source and change nothing, and its green would be real — this ticket's own hazard from the other side. Read the two bullets below as the reasoning that produced the option, not as its trigger; the trigger is an ASSIGNMENT rule.

A Pointer-typed value reaching an interface-typed destination is precisely the hand-built case, and it is compile-time visible. Emit a shim object carrying real pxx RTTI whose interface table maps the target id to the raw pointer, so PXXIntfIMTOf finds it by the existing walk.

Option B — adopt FPC's representation

Make the interface value the IMT pointer.

What would settle it

Not a discussion — two measurements. (1) the cast-site count above, which prices A. (2) how many places actually read an interface value as an instance: I named four from reading, and that number is the price of B and I have not counted it. Whoever takes this should get (2) before arguing either way; my recommendation of A is made without it and should not outrank it.

Both measurements, taken 2026-09-09 (frankH) — and they move the fork

Taken because the ticket asks for them before either option is argued. Neither option is chosen here; what follows prices them.

(1) The cast sites in generics.defaults.pas: there are NONE

/home/neo/src/fpc-trunk/packages/rtl-generics/src/generics.defaults.pas contains zero written casts of a pointer to an interface type. The conversion is an IMPLICIT ASSIGNMENT: _LookupVtableInfo and _LookupVtableInfoEx are declared : Pointer, and six sites assign their result straight into an interface-typed Result

1108  Result := _LookupVtableInfo(giComparer, ...)            IComparer<T>
2756  Result := _LookupVtableInfo(giEqualityComparer, ...)    IEqualityComparer<T>
2764  Result := _LookupVtableInfoEx(giExtendedEquality..., ...)
2766  Result := _LookupVtableInfoEx(giEqualityComparer, ...)
2908  Result := _LookupVtableInfo(giExtendedEquality..., ...)
2918  Result := _LookupVtableInfoEx(giExtendedEquality..., ...)

(:3502 is the seventh occurrence and is Pointer-to-Pointer, not a conversion.)

So option A as written does not fire on the code that motivates it. Its trigger is "a hard cast to an interface type whose operand is statically a non-class pointer", and the real source never writes one. The discrimination point exists and is still compile-time visible — a Pointer-typed value reaching an interface-typed destination — but it is an ASSIGNMENT rule, not a cast rule, and an implementation keyed on the cast would compile generics.defaults and change nothing. This is the ticket's own hazard from the other side: the shape a fix keys on has to be the shape the source actually writes.

It also answers the storage sub-question, differently and better than "one site" would have. The operands are not dynamic: the hand-built instances are TYPED CONSTANTS, one per element type —

Comparer_Int32_Instance : Pointer = @Comparer_Int32_VMT;

— fourteen of them plus the ShortString family. So a shim can be keyed on the OPERAND (static, one shim per hand-built table, materialised beside it) rather than on the site, and both objections in A's "against" evaporate: no same-site-different-pointer problem, and no lifetime problem, because a shim for a static operand is itself static. The two dynamic comparers (Comparer_Binary, Comparer_DynArray — commented out in the const block as "dynamic instance") are the exception and would need the heap answer, so the count that matters is "how many operands are NOT static", not "how many sites".

(2) The places that read an interface value as an instance: not four, and the four is the wrong axis

The dereference [inst] -> vmt -> [vmt-8] -> rtti exists in exactly TWO functions in the whole tree, both in compiler/builtin/builtinheap.pas: PXXIntfIMTOf (:3602) and PXXIntfComIMTOf (:3628). Every other name in the family — PXXIntfAddRef, PXXIntfRelease, PXXIntfAddRefAny, PXXIntfReleaseAny, PXXIntfAddRefRaw, PXXIntfAssign, PXXIntfFromVariant — routes through one of those two and never touches the layout itself. So two of the named four (the ARC helpers, the variant path) are not independent sites; they are callers of the two that are.

One of the four is representation-agnostic and should come off the list. The emitted nil check (IRWrapNilChk, ir.inc:16556) compares the word to nil. An IMT pointer is nil-checkable exactly as an instance pointer is; that site does not care what the word means.

And the one that decides B is not on the list at all: Self. AN_INTF_CALL's lowering (ir.inc:16542) takes the callee's Self from the interface value itself — Self = [iface] — and then calls PXXIntfIMTOf(self, ci) for the code address. Under B the value is the IMT, and pxx's IMT is a bare array of code addresses: no offset-to-object field, no adjustor thunks. There is nowhere for Self to come from. B therefore is not "four read sites move"; it is every interface method call's receiver plus a change to how IMTs are BUILT (rtti_emit.inc) to carry what FPC's carry.

So the answer to "is four the number or the first four" is neither. Two of it collapses into one pair of functions, one of it is not a read of the meaning, and the item that prices B was absent. The recommendation of A was correctly flagged as made without this measurement; with it, B is more expensive than the recommendation assumed and A is cheaper — but A must be re-specified onto the assignment, because the cast it keys on does not occur.

Still not decided here, and deliberately. What (1) and (2) settle is the price. What they do not settle is whether pxx wants FPC's interface ABI as a GOAL — the-goal-cross-cross wants foreign objects and documented layouts to work, and that is an argument for B that no cost measurement can answer.

2026-09-09 — do NOT escalate this yet; the FPC target may answer it for free

frankH's observation, filed when the fleet switched to application-driven work and worth acting on before anyone spends the owner on this.

This ticket's own closing line is "whether pxx WANTS fpc's interface ABI as a goal is the part no cost measurement answers." That was true while the only evidence available was a cost measurement. It is no longer the only evidence available: umbrella-pxx-compiles-fpc-itself (prio 85, filed 0500e9198) points pxx at ~400k lines of FPC's own Object Pascal, and FPC's source will either need that ABI or it will not.

If the attempt reaches a hand-built COM interface, the fork is answered by demand rather than by taste, and this becomes an ordinary blocker wired under that umbrella. If 400k lines never reach one, that is a measurement too — and the honest disposition then is low-prio/ or rainy-day/, not an owner decision.

So: leave this open, unowned, and do not put it in front of the owner. The cheapest way to settle a fork of intent is to find out whether real source wants the behaviour, which is the standing rule for compat here — evidence that settles it is real source that wants the behaviour. The attempt is running now. Re-read this ticket when it has covered enough of FPC to say.

2026-09-09 — the fork, restated so its actual reader can answer it

This ticket went to the owner and came back: "i hate to admit that i don't understand that question ... i dont know fpc's internals." The analysis above is sound and it is addressed to the wrong reader. He owns the goals, not our representations. Restating, with no implementation noun:

Do we want pxx-compiled and FPC-compiled code to hand interfaces to each other at the BINARY level — link together, share objects? Or do we only want FPC's source to compile and run correctly under pxx?

Both sentences name the same fork as "A or B" above. Only this one is answerable by the person it is addressed to.

And stated that way, an existing rule very nearly decides it. "We do NOT chase FPC parity — we just care for correct compiling pascal code, not emulating every behaviour." Correct-compilation is A. B is only required by a binary-interop goal, and the sole hint of one is the foreign-object aim in the-goal-cross-cross.md, which is about foreign objects generally rather than about FPC's ABI specifically.

Default: A, unless he names a binary-interop goal. Not marked decided — he was asked and has not yet answered the restated question — but a seat picking this up should implement A rather than wait, per never wait on an event only the owner can cause. If B is later wanted, A does not block it: A is local, and the two functions that price B (PXXIntfIMTOf, PXXIntfComIMTOf) are untouched by it.

One correction to this ticket's own recommendation while restating it: the trigger is an ASSIGNMENT rule, not a cast rule. generics.defaults.pas contains zero pointer-to-interface casts; all six conversions are implicit assignments. An A keyed on the cast would compile the motivating source, change nothing, and be green — this ticket's own hazard, from the direction that looks like success.

2026-09-09 — DECIDED BY THE OWNER: option A. Binary interop is not a goal.

Asked in goal terms, he answered in one turn:

"the challenge is just to compile FPC as a proof of pudding. we don't target any advanced compatibility. i can see what FPC is doing, sortof. we don't care. FPC is a great compiler and we have other goals, the common thing is pascal and that we sayd we target FPC's dialect as de-facto standard."

Option A. The dialect is the target; the implementation is not. Nothing in pxx moves toward FPC's interface representation, and B is not a deferred plan — it is off the table until someone names a binary-interop goal, which he has explicitly declined to. Do not leave B ranked as future work; that is how a rejected option comes back as a rainy-day item nobody re-reads.

For whoever implements A, the one trap, carried from 2304d362c: the trigger is an ASSIGNMENT rule, not a cast rule. generics.defaults.pas holds zero pointer-to-interface casts; all six conversions are implicit assignments of a Pointer-typed result into an interface-typed destination. An A keyed on the cast compiles the motivating source, changes nothing, and reports green — this ticket's own hazard arriving from the direction that looks like success.

Moving to done/ as decided. The implementation lives on in [[bug-a-a-hand-built-com-interface-cannot-be-called]], which is where the repro and the measurement already are.