← All docs

Linking, heap, and conditional compilation

Linking native libraries and frameworks for FFI, sizing the runtime heap, and defining compile-time symbols for ifdef branches.

These flags control how the binary is linked, how much heap the program gets, and which compile-time branches are taken.

Linking native libraries

When a program calls into C libraries through extern/FFI, those libraries must be linked into the binary. Raw link flags, managed native packages, Composer source, Rust bridge crates, runtime capabilities, and toolchains are distinct mechanisms:

MechanismUse it forDo not use it for
elephc nativeReviewed runtime/builtin-oriented C packages with exact source, lock, recipe, and cached static outputsArbitrary FFI libraries, PHP packages, Rust crates, or tool installation
Composer/autoloadPHP source dependencies resolved ahead of timeC archives or Rust bridges
Auto-detected bridge / --with-NAMEOptional Elephc Rust staticlib implementations and explicit runtime capabilitiesCatalogued C sources themselves
User/OS toolchaincc, ar, ranlib, assembler, linker, Make, SDK, and cross toolsProject dependency locking

Raw extern linking is a separate user-supplied workflow layered onto the linker. In particular, DOOM and the SDL examples require a user-installed SDL plus extern and --link/--link-path; SDL is not installed or versioned by elephc native.

Managed native packages

Curated C/C++ dependencies are declared and installed with elephc native, not with raw linker flags. During a final link, the compiler resolves logical requirements against the nearest project’s elephc.toml, deterministic elephc.lock, and verified target/toolchain cache receipt. It passes exact static archive paths to the linker; compilation never downloads or builds them.

The current catalog contains PCRE2 10.47, zlib 1.3.2, OpenSSL 3.5.8, nghttp2 1.70.0, libssh2 1.11.1, curl 8.21.0, and libxml2 2.15.3. Regex use links PCRE2’s managed archives in the fixed shim/POSIX/8-bit order and has no production system-library fallback:

elephc native add pcre2
elephc app.php

Declaring PCRE2 does not force it into a program that does not use regex. Exact managed archives remain compatible with Linux’s static-link preference. zlib is the second exact pure-C recipe; it is available for curated runtime/builtin integration, not an automatic replacement for arbitrary extern "z" and -lz workflows.

curl is the first catalog package with non-empty dependencies: adding it also declares OpenSSL (curl’s TLS backend only — openssl_encrypt()/hash() stay on the separate crypto bridge) and zlib, and a final curl link resolves all three in the fixed libcurl.a -> libssl.a -> libcrypto.a -> libz.a order with the same no-system-fallback contract (no Homebrew/distro -lcurl/-lssl):

elephc native add curl
elephc app.php --with-curl

libxml2 backs the xml bridge the same way: elephc_xml’s parser and writer are libxml2 itself, reached through an Elephc-owned C shim the recipe compiles against the freshly built headers, and a link that plans the bridge (auto-detected xml use or --with-xml) resolves the package’s two archives in the fixed libelephc_libxml2_shim.a -> libxml2.a order, with no system -lxml2 fallback. The package has no catalog dependencies; Apple targets additionally link the SDK’s libiconv, which glibc already provides on Linux:

elephc native add libxml2
elephc app.php --with-xml

See Native dependencies for the full workflow.

Links an extra native library. Accepts the spaced form, the short flag, and the attached form; repeat it for multiple libraries.

elephc app.php --link sqlite3
elephc app.php -l sqlite3
elephc app.php -lsqlite3

Adds a directory to the library search path. Repeatable.

elephc app.php -l sqlite3 -L /opt/homebrew/lib
elephc app.php --link-path /usr/local/lib

--framework

Links a macOS framework. Repeatable.

elephc app.php --framework Cocoa --framework Metal

extern "libname" { ... } blocks in source add their own -l flags automatically; the flags above are for libraries not already named in the source. They do not override or satisfy a missing managed-package requirement such as PCRE2. See FFI & Extern.

Bridge crates and --with-NAME

Some optional features are implemented as Rust bridge crates (staticlib archives) that elephc links into the program: pdo (database access), tls (https:///ftps:// streams), crypto (the hash()/md5()/sha1() family), bcmath (exact arbitrary-precision decimal arithmetic), iconv (character-set conversion and the character-oriented iconv_* functions), phar (Phar archives), tz (timezone introspection), image (GD/Imagick image processing), pcntl (Unix process control and signals), xml (the ext/xml SAX parser and ext/xmlwriter), eval (the Magician interpreter fallback for dynamic eval()), web (the --web server), and curl (the libcurl-backed ext/curl surface).

By default a bridge is linked only when the program uses it — using a hash function pulls in crypto, opening an https:// stream pulls in tls, calling a bc* function pulls in bcmath, calling an iconv* function pulls in iconv, referencing PDO pulls in pdo, creating an XMLWriter or calling an xml_* function pulls in xml, and so on. An eval() call pulls in Magician only when it needs runtime parsing: eligible literal fragments can be parsed at compile time and lowered to native EIR without the interpreter bridge. Programs that do not need a feature never link its crate, so binaries stay small.

--with-CRATE force-enables a bridge regardless of that auto-detection. It force-links the staticlib (whole-archived, so it is retained even if no symbol references it) and, for crates whose PHP surface comes from an injected prelude (pdo, mysqli, tz, image, xml), force-injects that prelude so the classes/functions are available. This is useful when a program reaches a feature through indirection that detection cannot see. The flag is repeatable:

elephc app.php --with-pdo
elephc app.php --with-crypto --with-tls
elephc app.php --with-bcmath
elephc app.php --with-iconv
elephc app.php --with-pcntl
elephc app.php --with-eval

--with-pcntl force-links the process-control bridge for indirect or opaque runtime calls. Statically visible pcntl_* calls auto-link it. See PCNTL for target availability, fork/signal semantics, and documented limits.

--with-eval force-links elephc_magician; it does not enable new syntax or change which fragments are eligible for AOT lowering. Normal eval usage is detected automatically. See Eval for language semantics and Eval Runtime Architecture for the AOT/fallback decision and scope ABI.

--with-regex is a runtime-capability flag rather than a Rust bridge flag. Dynamic eval source cannot be inspected for feature use, so the flag requests the ordinary regex runtime and managed PCRE2 archives, then registers that provider with Magician:

elephc native add pcre2
elephc --with-regex app.php

Without it, dynamic eval still compiles and runs non-regex code, but preg_* names are unavailable there and calls fail at runtime. A statically visible regex use enables the same provider automatically. Declaring PCRE2 without either trigger does not link it.

--with-mysqli is the other runtime-capability flag. It force-injects the mysqli prelude — which links the shared elephc_pdo archive — without injecting the PDO classes; there is no separate elephc_mysqli bridge.

--with-curl is a bridge flag, like --with-pdo, but it is also the first one that itself needs a managed native package: force-linking the whole elephc_curl archive only satisfies the Rust side, and the final link also needs the curl package’s libcurl.a/libssl.a/libcrypto.a/libz.a declared and installed with elephc native add curl (see Managed native packages above). There is no --with-regex-style split between a runtime capability and the bridge here: --with-curl is the one flag that both force-links the crate and requires the package.

elephc native add curl
elephc app.php --with-curl

--with-xml is the second such flag. The elephc_xml archive carries the PHP-facing XMLParser / XMLWriter implementation, but its parser is libxml2 itself, so the final link also needs the libxml2 package’s libelephc_libxml2_shim.a and libxml2.a, declared and installed with elephc native add libxml2. Auto-detected xml use requires the package just the same; the flag only adds the force-link and the prelude injection. Whichever way the bridge is linked, the XML guide’s runtime limits apply — a per-handler-invocation heap cost that --heap-size accommodates:

elephc native add libxml2
elephc app.php --with-xml

--with-web is an alias for --web (the full server mode, which owns the program entry point). An unknown capability name is rejected with the list of valid names. Forcing a bridge increases binary size, since the whole archive is included.

Bridge crates are Elephc’s optional Rust workspace components. They are not installed or versioned by elephc native. A bridge or runtime-capability flag may require a separately declared managed package — --with-regex requires pcre2, --with-curl requires curl (which in turn declares openssl and zlib), --with-xml requires libxml2 — but the flag itself does not install it. Composer dependencies are PHP source handled by the compile-time autoload pipeline and remain separate.

Heap size

The compiled program uses a fixed-size runtime heap, 8 MB by default. Programs that allocate a lot of arrays, strings, or objects may need more.

--heap-size

Sets the heap size in bytes. The minimum is 65536 (64 KB).

elephc --heap-size=16777216 heavy.php   # 16 MB

If a program exhausts its heap it aborts with a fatal “heap memory exhausted” error; raising --heap-size is the fix. See Memory Model.

Runtime dead stripping

The compiler ships a single runtime with helpers for every supported builtin, but a given program only uses a few of them. When linking an executable, the linker keeps only the runtime helpers reachable from the program and drops the rest, so a small program does not carry the whole runtime. This is automatic — there is no flag — and never changes behavior, only binary size.

It works the same on every supported target, using each platform’s native mechanism:

  • Linux emits each runtime helper into its own section and links with --gc-sections.
  • macOS emits the runtime object with .subsections_via_symbols so each helper is a separately collectable atom, and links with -dead_strip.

Shared libraries (--emit cdylib) are collected the same way. Every symbol outside the export allowlist — the lifecycle entry points plus #[Export] trampolines — is emitted .hidden on ELF and .private_extern on Mach-O, so it is not an export and not a collection root; only the public ABI and what it reaches survives.

Symbol stripping

Dead stripping removes unreachable code. Stripping removes the names of the code that stays. The two are independent: the first changes what runs, the second changes only what the file says about itself.

A linked executable is stripped of its symbol table. Nothing in a compiled program reads those names — Throwable::getTrace() and getTraceAsString() are not implemented, and the uncaught-exception report prints no stack trace — so they are dead weight at run time, and they are roughly a quarter of the file:

programlinkedstripped
<?php echo 1;182 680 B132 760 B (−27%)
a realistic program213 528 B152 152 B (−29%)

The share grows with the program rather than shrinking, because the symbol table scales with the number of declarations while the text section does not.

Two flags keep the names, for the two reasons to want them:

  • --debug-info already means “I am going to debug this”, so the pipeline does not invoke strip in this mode. On macOS, dsymutil still bakes the dSYM and the linked executable keeps its symbol table too.
  • --keep-symbols is for profilers, which read the symbol table and have no other source of names.

Shared libraries are never stripped. Their exported symbols are their interface: a host resolving one with dlsym would get a null it may well treat as “feature absent” rather than as an error.

If the strip tool is missing, or cannot read the target’s object format, the compiler warns and keeps the larger binary. A failed build would be the worse outcome for what is only a size optimization.

Binary hardening

Compiled binaries are hardened by default. There is no flag: the options below are always applied and cannot be turned off.

On Linux, every executable and shared library is linked with:

OptionEffect
-z noexecstackMarks the stack non-executable (PT_GNU_STACK RW). elephc assembles its objects with as, which emits no .note.GNU-stack section, so without this GNU ld infers an executable stack and warns. Nothing elephc produces needs one: there is no JIT, and Fiber stacks are mapped read/write with a guard page.
-z relroMaps the relocated head of the data segment read-only once startup relocation is done.
-z nowResolves all relocations eagerly at load time, so relro can cover the GOT (full RELRO).

Whether the executable is also position-independent is decided by the system toolchain, not by elephc: Linux executables are linked -static whenever the program needs no dynamic library, and a driver configured with default-PIE (for example Alpine/musl) turns that into a static PIE, while a driver without it (for example Debian/Ubuntu glibc) produces a classic non-PIE static executable. elephc does not force -static-pie, because it requires a libc built with static-PIE support (rcrt1.o) that many distributions do not ship, and a missing one is a hard link failure.

On macOS these options do not apply: ld64 does not accept -z, binaries are position-independent by default, and the stack is non-executable at the platform level.

Conditional compilation

elephc supports compile-time feature branches with ifdef. Symbols are defined on the command line and the branches are resolved before optimization and code generation, so unused branches are never compiled.

--define / --define=

Defines a compile-time symbol. Repeatable. It may be combined with --strict-php, but strict auditing still rejects every ifdef in physical PHP source. The combination exists for mixed projects and LFC source: LFC ifdef consumes the symbol while PHP source remains audited.

elephc --define DEBUG app.php
elephc --define=DEBUG --define=METAL app.php
elephc --strict-php --define DEBUG app.lfc
ifdef (DEBUG) {
    echo "debug build\n";
}

See Conditional Compilation for the full ifdef syntax and semantics.