← board

An --emit-obj object linked into a non-pxx program runs no initialisers

Split out of [[bug-a-c-a-shared-library-never-runs-its-initialisation]], whose two shared library halves are fixed (7d1aed2b5, 9b6ca0475). This is the case that ticket's reasoning excluded by a premise that holds everywhere except the feature's purpose.

Measured

$ pascal26 -Fulib/crtl --emit-obj test/test_shared_lib.c lib.o
$ gcc host.c lib.o -o host && ./host
obj from_data=(null) tag=pxx-c-shared

shared_c_from_data returns a file-scope static const char * initialised to a string literal. The identical source compiled as a pxx program prints pxx-c-data and matches the gcc oracle, and as a .so it now does too.

Why the object case is different from the .so case

The C frontend emits CompilePendingGlobalInits at the start of main (cparser.inc), so a translation unit with no main emits them nowhere. For --shared that was fixed by emitting them into a DT_INIT thunk. An object has no dynamic section, so there is nothing to point at: the standard mechanism is a .init_array section holding a pointer to the initialiser function, which the linker concatenates across objects and the host C runtime runs before main.

So the work is:

Scope note, and the reason this is not filed as "the same bug"

The environ half does not apply here: an object linked into a gcc program gets glibc's environ, not ours, and __pxx_run_initializers is not wanted. Only the file-scope initialisers are missing. Filing them together would overstate what is broken.

Not measured

Whether a pxx object linked into a pxx program is affected — the parent ticket says it is not, because that program's entry stub runs the initialisers, and nothing here contradicts it. Worth one run before the fix, since the fix must not run them twice.


C half resolved 2026-09-01 (frankA)

41b08f2bf — .init_array/.fini_array in writeELFRelX64General, appended after .shstrtab's INDEX so no existing section index moves, with the init thunk from the .so fix under a widened condition. One thunk, two carriers: glibc calls a DT_INIT and an .init_array entry alike, with (argc, argv, envp). SharedInitOff/SharedFiniOff became InitThunkOff/FiniThunkOff.

c1bb99ec2 — corrects two things in the above. An object with nothing to initialise now gets no thunk and no .init_array (decided by emitting the body and reading CodeLen, not by a predicate that would need updating when a fourth source of pre-main work arrives). And the byte-identity claim in 41b08f2bf was verified against a PASCAL object, which the Pascal frontend never gives a thunk — a population that could not contain the phenomenon, so it agreed without being able to disagree. Every C object had in fact been growing both sections.

Two mechanisms, not one, same as the .so half: from_data() needed the initialisers to run at all; environ needed the environment, and the object does not reach glibc's environ — it carries private storage nothing filled. I had reasoned a gcc link would bind it to glibc's own and the measurement said -1, which is the direction that ships half a fix.

On CNeedsEnvironInit, which the parent ticket warns against widening: that warning is about widening it to BAIL under EmitSharedMode, which would delete a dead-code tell while keeping a wrong value. This widened it the other way, to be true where it was falsely false. The parent's premise for exempting --emit-obj — "that object is linked into a program whose entry stub does run them" — is the thing this ticket refutes.

Pascal arm: measured, attempted, reverted

pinit.pas --emit-obj, gcc host    flag=0  msg=          (want 4242 / pxx-pascal-init)
with the C-style widening         flag=4242 msg=pxx-pascal-init, host exit 0
same run, test-emit-obj           expected `45 pxx-emit-obj`, got `done99 pxx-emit-obj`

The third line is not a stale expectation. test/test_emit_obj.pas reads g deliberately so that a foreign caller sees 45 rather than 45+9, and its comment states that this is the property being pinned and that the value says which world we are in. The tripwire was written before this work and it fired on the first run. Retuning it would be taking the decision quietly, so the change is reverted and the fork is filed.

Why Pascal cannot borrow the C answer: a C translation unit has file-scope initialisers and no main body, so "run the initialisers" is unambiguous. A Pascal program emits CompilePendingGlobalInits, then the unit init sections, then the program body — contiguous, no ret between them, all at offset 0. The first two should run in a foreign host; the third is a different question. They share a code offset by accident of emission order, not by design.


Pascal arm resolved 2026-09-01 — decided A, landed

The decision came back A: an object runs the program body, because --shared already did and the two library-shaped outputs had no reason to disagree about the same source. Full argument in the decision ticket; the short version is that the comment I had read as pinning a design was written in 41045d7b4 describing behaviour that existed only because the mechanism did not, and library foo; does not parse, so program is the only spelling a user has.

pinit.pas --emit-obj, gcc host   flag=0 msg=  ->  flag=4242 msg=pxx-pascal-init

The target gate is the part to not lose. EmitSharedThunkPrologue emits raw x86-64 pushes; --shared is x86-64-only so its call site never needed a guard, but --emit-obj also accepts i386, xtensa and riscv32. My first attempt widened the Pascal terminal ungated and would have spliced x86-64 bytes into those objects. It passed unnoticed because make stops at the first mismatched row and never reached the i386 one — the x86-64 expectation failed first and masked it. Anyone widening the gate must widen the emitter first.

test/test_emit_obj.pas's tripwire comment is rewritten rather than deleted: it now pins the opposite property and says why it changed, because a tripwire retired quietly is worse than none.

Log