← board

--shared for compiled sources, not just .asm

Deferred by [[meta-a-pxx-produces-linkable-code]] until the object writer's shape was known.

Why this is not the same job as the .o

The general x86-64 object writer landed with an absolute relocation model and a documented -no-pie requirement. That trade is available because a non-PIE executable chooses its own load address.

A shared library does not. It is relocated at load time by definition, so the same absolute operands that a -no-pie link resolves cannot work here at all. writeELFSharedX64's own comment states the property it depends on: the .asm frontend's addressing "is already position-independent by construction — there is no absolute-address operand form in this frontend at all", so it emits zero R_X86_64_RELATIVE relocations. EmitDataRef and EmitGlobRef are precisely that missing form.

So the blocker is backend work — [[feature-a-x86-64-object-output-is-position-dependent]] — and not writer work. Either the backend grows a rip-relative global-reference form, or the .so writer grows R_X86_64_RELATIVE emission for every Fixups/GlobFix/DataPtrFix/MethodFix site plus a DT_RELA/DT_RELACOUNT pair. The second is the cheaper of the two and should be priced first — it is writer-local, it reuses the relocation inventory writeELFRelX64General already walks, and it does not move a single instruction.

Do not start here expecting the .o work to carry over; the export surface and symbol partition do, the relocation model does not.

Umbrella

[[meta-a-pxx-produces-linkable-code]]

Resolved — 2026-09-01, Track A (frankA), 0419bab94

Binary ac47455d7c6c. The blocker, [[feature-a-x86-64-object-output-is-position-dependent]], landed first in d0537380a / 44b256356 / a3b1af61a.

What works

pascal26 --shared mylib.pas mylib.so
gcc main.c ./mylib.so -o prog        # or dlopen at run time
consumer result
dlopen + dlsym + call virtual dispatch, heap, strings, extern GOT — all correct
gcc main.c ./lib.so links, runs
.asm frontend --shared (regression) still builds, dlopens, runs — and is now linkable too

Two things this ticket got wrong, both worth recording

1. "compiler.pas says so in the option handler." It does not. The handler has a comment saying --shared is .asm-frontend only, and no check. So a compiled source already reached writeELFSharedX64 and came out as a valid ET_DYN exporting nothing — which is worse than a refusal, because it looks like a build that worked. A comment recording a restriction is not the restriction.

2. "this one is small afterwards." The relocation model was the blocker, as stated, but three more things were missing and none of them is in this ticket: the export list, .bss, and section headers.

The four changes

The .asm and compiled paths are one path over an export list built up front, not a branch at each of the six use sites.

The control, and the aim check it produced

"The .so loads and runs" is exactly the claim an empty population produces: a library with no absolute data pointers would load and run correctly with no relocations at all.

arm result
suppress the RELATIVE entries, rebuild, run the dlopen host SEGFAULT
restore SHARED LIB OK

So the Makefile asserts RELACOUNT > 0 before any behavioural row, and says why in the recipe. Without it, a test source that quietly stopped containing an absolute data pointer would go on reporting success — the third time in this ticket family that the population, not the instrument, was what needed checking.

test/test_shared_lib.pas is written to contain the classes it tests: a virtual method, SetLength, AnsiString concatenation, an external libc call, and a string literal returned as PChar. Its header says why each is there rather than in a smaller program.

Not done here

A Pascal library unit with an exports clause — library is not a keyword today, so a shared library is written as a program with cdecl routines and an empty main body. i386 --shared is refused (the backend is still position-dependent there). No DT_SONAME or versioning.

Log

Follow-up — 2026-09-01, a359a2abb152

Both of these came from checking a claim I had already written into the docs, the summary and the umbrella: "a Pascal/C/NilPy source builds a .so". I had tested Pascal only.

C was broken. --shared reused --emit-obj's export surface but not its entry-stub guard, so the C frontend still demanded a main:

$ pascal26 --shared lib.c lib.so
error: main function not found
$ pascal26 --emit-obj lib.c lib.o        # the identical file
ok

A shared library has no ELF entry point either, so the two modes had to agree and only one had been told. test/test_shared_lib.c covers it and has no main on purpose — adding one would make it pass against the broken compiler. Control run: against the pre-fix binary ac47455d7c6c it fails with exactly main function not found.

NilPy was never possible. ProcCdecl is set only from pasparser_proc.inc, pasparser_decl.inc and cparser.inc, so no NilPy (or Rust, or Zig) routine can be marked C-convention and a .so from one exports nothing. The refusal message is correct; my claim was not. cli.md also claimed --emit-obj supported NilPy — same error, pre-existing, corrected in the same pass.

The four sites that spell EmitObjMode or EmitSharedMode were NOT collapsed into one predicate, deliberately, and the reason is at the fixed site: they agree in value and not in meaning. This one and ir_codegen.inc's are there is no entry point; asmfront.inc's is the program cannot fall off its end; dce.inc's is the output carries its own code-offset relocations; symtab.inc's is the writer relocates by section, not absolute address. One predicate would merge four rationales and a fifth output mode would break some of them together, silently.