← board

The address of an external routine is refused on i386 and xtensa

extern int gcc_thing(int);
typedef int (*fn)(int);
int use(void) { fn f = gcc_thing; return f(1); }
target result
x86-64 compiles
i386 error: @ on external routine not supported; wrap it in a local routine
xtensa same message, ir_codegen_xtensa.inc:4177

The suggested workaround in the message ("wrap it in a local routine") is not available to a C program that receives the pointer from a table it does not own, and it is not something a compiler can ask of hand-written C.

Why it surfaced now, and the one thing to know before fixing

test-c-abi-mixed-link gained two rows that pass a struct through a function POINTER, to close a hole in that gate (the indirect cdecl arm classified arguments in its own loop and had not been converted). Those rows initialise a fn-pointer from an extern, so the i386 arm of the gate now fails at COMPILE time rather than at run time.

That is a loss of information, not a new defect. i386 was already red there and stays red; but a compile failure means the other 13 rows report nothing, so whoever implements i386's aggregate ABI cannot see which of them their work has fixed. Fixing this unblocks the diagnosis, not the feature — worth doing first for that reason rather than for its own weight.

x86-64 reaches the address through the GOT for an external (EmitExternalProcAddr and the PatchDynCallSites relocation path). i386 has the same dynamic machinery for CALLS; it is the address-of arm that was never written.


i386 done 2026-09-01 (frankA); xtensa open

minimised repro, i386     refused  ->  compiles
linked, gcc -m32 host                  returns 42 (calls through correctly)

The fix is one arm in EmitExternalProcAddr (symtab.inc), which already carried x86-64, aarch64, arm32 and riscv32 — i386 and xtensa were simply absent, and the per-backend refusal was standing in for the missing arm.

Why the encoding needed care rather than copying. ModRM $05 is mod=00 rm=101, which is an absolute disp32 on i386 and rip-relative on x86-64 — the same two bytes meaning different things per target. It needed no new patch arm because PatchDynCallSites already splits exactly on that: x86-64 gets a distance, everything else gets the absolute slot VA. A refusal is loud and a wrong address is silent, so the test ROWS the call rather than only compiling it.

Xtensa: measured, and not the same fix

Still refused (ir_codegen_xtensa.inc:4177), confirmed on a repro reduced away from stdio.h — because #include <stdio.h> fails on xtensa for an unrelated reason, filed as bug-c-including-stdio-h-refuses-to-compile-for-xtensa. Worth knowing before anyone tries to reproduce this one there: the obvious repro dies before it reaches the site.

grep DynCallCodePos ir_codegen_xtensa.inc returns nothing. Xtensa has no GOT-slot external-call path to hang this on, so the i386 answer does not transfer — its externals resolve through some other model, and that model has to be read before an arm can be written. That is the whole of the remaining work, and it is why this is parked rather than finished.

xtensa: resolved

frankA, 2026-09-01. Compiler 102942dd50cf. Regression rows: test-emit-obj block 4b-nonies, riscv32 and xtensa.

The recorded reason it could not be done was about the wrong half of the mechanism. This ticket said xtensa "has no DynCall/GOT external path at all (no DynCallCodePos anywhere in ir_codegen_xtensa.inc)". It has no GOT — true — but it does have DynCall, and an ordinary extern CALL already emits exactly one R_XTENSA_32 against the UND symbol. Measured before touching anything: extern int gcc_thing(int); int use1(void){ return gcc_thing(1); } produces 0000002c R_XTENSA_32 gcc_thing + 0. The address lives in a literal, which is what the target does for a call — so taking the address is the call path minus the call, and the missing arm is the riscv32 one with xtensa's literal shape.

The grep was accurate and the conclusion drawn from it was not: DynCallCodePos is written in symtab.inc's shared emitters, never in a per-target codegen file, so its absence from ir_codegen_xtensa.inc says nothing about whether xtensa has the path.

The fix is a TARGET_XTENSA arm in EmitExternalProcAddr beside the riscv32 one, and IR_PROCADDR in ir_codegen_xtensa.inc routing an external there instead of erroring — the local-@proc path below it keeps its ProcAddrFix literal, which patches to entry+BodyAddr and has no meaning for an external.

Verified structurally, NOT by execution, and the limit is real. An xtensa EXECUTABLE refuses external dynamic symbols outright ("this backend emits no dynamic segment"), and the C frontend has no xtensa entry stub, so there is no way to run an @external on xtensa from this box. What can be checked is the object, and it now matches riscv32's already-accepted arm row for row: a bare call names the external in 1 relocation, taking its address names it in 3, on BOTH targets. The regression row asserts the DIFFERENCE and asserts the call-only control is nonzero first — a row asserting only that the fixture compiles would pass on a backend emitting no reference at all.

Both targets in the title are now done, so this leaves unfinished/.

Log