Linking a pxx object into another program
--emit-obj (or an output path ending in .o) writes a relocatable ELF object
instead of a linked executable, so pxx-compiled code can be linked into a build
driven by someone else's toolchain.
pascal26 --emit-obj mylib.pas mylib.o
gcc main.c mylib.o -o prog
Targets: x86-64, i386, aarch64, arm32, riscv32 and
xtensa. This page describes the x86-64 and i386 objects. The aarch64 and
arm32 writers were added on 2026-09-22, and this page does not yet cover
linking their objects. They share an export surface, but not a relocation model: an
x86-64 object is position-independent and links either way, an i386 object is
position-dependent and needs gcc -m32 -no-pie. The two ESP targets are a
different kind of object: they export app_main for an IDF image and are
covered under Targets.
What the object exports
The routines declared with the C convention, and nothing else. In Pascal
that is cdecl; in C source it is every function of the translation unit.
function pxx_sum(n: Integer): Integer; cdecl;
var i, s: Integer;
begin
s := 0;
for i := 1 to n do s := s + i;
pxx_sum := s;
end;
Everything else the compilation defines — your other routines and the whole RTL
it pulled in — is written as a local symbol: a debugger can see it, the
linker cannot, so an object can never collide with its host program over a name
like WriteLn. Calls out to external routines become ordinary undefined
symbols under their link names, which the linker resolves as it would for any
other object.
If nothing carries the C convention the object would define no linkable symbol, and pxx refuses to write it rather than hand you a file whose failure surfaces later at your link step.
Two things to know before you link
x86-64 needs no link flags; i386 needs -no-pie. On x86-64 the backend
reaches globals through [rip+disp32], so .text carries only
R_X86_64_PC32. A plain gcc main.c mylib.o is enough, and the object links
into a PIE and a non-PIE, under gcc and under clang. The R_X86_64_64
relocations that remain are all in .data — absolute pointers stored in
data, which a PIE resolves at load time like any other shared object.
The i386 backend still reaches globals through absolute operands, so an i386
object carries R_386_32 in .text. GNU ld will accept those in a PIE, but
only by making the text segment writable at load time:
warning: relocation in read-only section `.text'
warning: creating DT_TEXTREL in a PIE
That is why -no-pie remains the documented form for i386. It is not merely
cosmetic — a hardened link refuses it outright, while the same flag on an
x86-64 object links clean:
$ gcc -m32 -Wl,-z,text main.c mylib32.o # read-only segment has dynamic
# relocations -> error
$ gcc -Wl,-z,text main.c mylib.o # links
Before 2026-09-01 x86-64 behaved like i386 and this page said
-no-piewas required on both. Objects emitted by an older pxx still need it.
No initialisation runs. Linking an object into a foreign program does not
run the Pascal main body, unit initialisation, or anything else the program
would have done on startup — the host program's main is the entry point, and
it never calls yours. An exported routine must therefore not depend on a global
having been assigned at startup; give it what it needs through its parameters,
or export an explicit cdecl init routine for the host to call first.
What works today
Measured, not inferred: a gcc-built main linking a pxx object and calling
into it — Pascal and C sources, integer and floating-point signatures, string
work through the pxx heap, and pxx calling back out to a shared library
(sqrt from libm) resolved by the system linker.
Also a shared library — see below — including one written as a Pascal
library. Not yet: object output for arm32 and aarch64.
Writing it as a library
A source whose header says library instead of program is the Delphi and FPC
spelling for "this compilation is a library, not an executable", and pxx takes
it:
library mylib;
function pxx_sum(n: Integer): Integer; cdecl;
var i, s: Integer;
begin
s := 0;
for i := 1 to n do s := s + i;
pxx_sum := s;
end;
exports pxx_sum;
begin
end.
exports declares the surface; it does not select it. This is the one thing
to get straight coming from Delphi. The export surface is still exactly the
C-convention routines — the rule from What the object exports,
unchanged — so a cdecl routine you leave out of the exports clause is
exported anyway. What the clause buys you is that it is checked: each name
must resolve to a routine that this library actually defines and that the writer
will actually export. Naming something that is not a routine, that is external
(imported, so there is no body here), that is not cdecl, or that is C static
(internal linkage) is an error at the clause, with the reason — rather than a
symbol quietly missing from the .so and surfacing at someone else's link step.
Two other spellings are diagnosed rather than ignored, because silently dropping
either would write a library whose symbols are not the ones the source asked
for: exports Foo name 'bar' (renaming) is not supported yet — rename the
routine — and exports Foo index 3 (export by ordinal) is not supported at all,
since ELF has no ordinal table.
exports is rejected in a program, which has no export surface.
One difference from FPC worth knowing:
librarydoes not choose the output kind — the flag does. Compiling one without--sharedproduces an ordinary ELF executable, not a.so. Writepascal26 --shared mylib.pas mylib.so.
The main body is optional, as in FPC: library mylib; … exports f; end.
compiles, and so does the same file with a begin end. before the final end..
Note this is a library rule and not a general one — program p; end. is
refused, by pxx and by FPC alike.
And the rule from below applies here too: loading the library does not run the
main body, so a begin … end. is not a place to initialise anything an exported
routine depends on.
A shared library
--shared (or an output path ending in .so) writes an ET_DYN shared
library. x86-64 only.
pascal26 --shared mylib.pas mylib.so
gcc main.c ./mylib.so -o prog # link against it
# ...or load it at run time with dlopen/dlsym
The export surface is the same as an object's — the C-convention routines, and
nothing else — so the cdecl example above applies unchanged. A library that
exports nothing is refused rather than written. That also bounds which
frontends can produce one: Pascal cdecl and C functions are the only
spellings that mark a routine C-convention, so a NilPy source has nothing to
export and is refused.
A C translation unit meant to be a library needs no main, the same as for
--emit-obj.
What works inside one: the pxx heap, managed strings, dynamic arrays, classes
with virtual methods, and calls out to external routines, which are resolved
through the library's own GOT with a DT_NEEDED on the providing library.
No initialisation runs here either. Loading the library does not run the Pascal main body, so an exported routine must not depend on a global having been assigned at startup — the same rule as for objects, and for the same reason.
Before 2026-09-01 --shared served the .asm frontend only. A compiled source
produced a valid ET_DYN that exported nothing, because the writer built its
export list from the assembly frontend's label table. It was blocked on the
backend rather than the writer: a shared library is relocated at load by
definition, so the absolute address operands that -no-pie rescued in an
object could not work here at all.
One known defect
Worth knowing before you link an i386 object: a pxx routine clobbers EBX on
i386 and does not restore it, so a C caller keeping a live value there can
crash after your function has returned the right answer. x86-64 is
unaffected.