← 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), mbstring (the shared multibyte string engine), 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 or using BcMath\Number pulls in bcmath, calling an iconv* function pulls in iconv, calling mb_strlen() pulls in mbstring, referencing PDO pulls in pdo, and creating an XMLWriter or calling an xml_* function pulls in xml. 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.

When building from a source checkout, elephc refreshes bridge archives after changes to their local Cargo dependencies, including shared contracts. Workspace manifests, the lockfile, and Cargo configuration also participate in this freshness check.

--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-mbstring
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-mbstring requires oniguruma and pcre2, and --with-xml requires libxml2. Ordinary mbstring and opaque eval use the contract-owned default MIME matcher without PCRE2. Custom MIME expressions in opaque eval need the complete --with-mbstring capability. These flags do not install the packages. Composer dependencies are PHP source handled by the compile-time autoload pipeline and remain separate.

Where bridge archives are found

The archives have to travel with the compiler. Copying just the elephc binary out of its build tree — into a project’s tools/bin, say — makes every build that needs a bridge fail at link time, because the archives are separate files and not embedded in the binary. Either of the two supported layouts works:

tools/bin/elephc            # archives beside the binary
tools/bin/libelephc_web.a

tools/bin/elephc            # or in a sibling lib/, the usual install layout
tools/lib/libelephc_web.a

elephc resolves each archive in this order, taking the first that exists:

  1. ELEPHC_<NAME>_LIB_DIR, if set and non-empty — ELEPHC_WEB_LIB_DIR, ELEPHC_PDO_LIB_DIR, and so on, one per bridge. This wins over everything below, and names a directory, not the archive itself.
  2. The directory the running elephc binary is in.
  3. Its sibling lib/ (<bin>/../lib).
  4. $CARGO_TARGET_DIR/debug and $CARGO_TARGET_DIR/release, when that variable is set.
  5. target/debug and target/release, relative to the current directory.

Run from an elephc source checkout, a missing archive is built on demand, which is why this never comes up during development. A relocated binary has no checkout to build from, so it reports what it wanted and where it looked:

Linker error: required Elephc bridge `elephc_web` could not be found
  needs: libelephc_web.a
  looked in:
    /opt/tools/bin
    /opt/tools/lib
    target/debug
    target/release

Set ELEPHC_WEB_LIB_DIR to a directory containing libelephc_web.a, or keep the
bridge archives next to the elephc binary (or in a sibling lib/).
`elephc --print-capabilities` lists every archive this binary can need.

ELEPHC_<NAME>_LIB_DIR replaces the search rather than joining it: when it is set and non-empty, resolution stops there and steps 2–5 are never consulted. So an override pointing somewhere without the archive fails even when a perfectly good copy sits beside the binary, and the message says so rather than listing directories it did not read:

Linker error: required Elephc bridge `elephc_web` could not be found
  needs: libelephc_web.a
  looked in:
    /opt/stage/lib

ELEPHC_WEB_LIB_DIR is set to /opt/stage/lib, which takes priority over every
other location — the elephc binary's own directory, a sibling lib/ and the build
tree were NOT consulted. Put libelephc_web.a in /opt/stage/lib, point
ELEPHC_WEB_LIB_DIR somewhere that has it, or unset ELEPHC_WEB_LIB_DIR to search
those locations again. `elephc --print-capabilities` lists every archive this
binary can need.

When the override directory does hold an entry by that name but it is not a usable archive (an empty file, a directory, or a symbolic link, dangling or not), the error names that entry in needs: and says to replace it rather than to put the archive there:

Linker error: required Elephc bridge `elephc_web` could not be found
  needs: /opt/stage/lib/libelephc_web.a
  looked in:
    /opt/stage/lib

ELEPHC_WEB_LIB_DIR is set to /opt/stage/lib, which takes priority over every
other location, so no other directory was consulted. Replace invalid archive
/opt/stage/lib/libelephc_web.a with a non-empty regular file, point
ELEPHC_WEB_LIB_DIR to a directory containing a valid libelephc_web.a, or unset
ELEPHC_WEB_LIB_DIR to search the other locations again. `elephc
--print-capabilities` lists every archive this binary can need.

When an exact archive path in the link plan is missing, empty, a directory, or a symbolic link, the error names that path in needs: and omits looked in:. The exact-path plan does not run the fallback search. An invalid archive found by fallback discovery also names its path, and retains the locations reached before it was found. The error tells you to replace the invalid file or point the directory override at a valid archive. Discovery checks each candidate without following symbolic links, so a dangling link where an archive belongs stops the search and is reported this way instead of being skipped.

The eval() bridge has two archive names

elephc_magician is the one bridge whose archive filename depends on the program being compiled. A program that calls eval() links libelephc_magician.a. A program that calls eval() and uses the curl surface links libelephc_magician_curl.a instead — a separate file, not the same slot rebuilt, because cargo build --features curl would otherwise overwrite the curl-free archive an earlier build left behind and a later curl-free program still expects.

Both names go through the search order above and through the same ELEPHC_MAGICIAN_LIB_DIR override, so an install directory has to carry whichever of the two the programs built against it need; the release tarball and the Homebrew formula ship both. A missing curl-aware archive is reported under its own name:

Linker error: required Elephc bridge `elephc_magician` could not be found
  needs: libelephc_magician_curl.a

--print-capabilities is the authoritative list for the binary you are holding — one line per capability with the archives it needs — so a packaging script can check a tarball carries everything the compiler inside it advertises rather than guessing from this page. It reports the eval bridge under the plain libelephc_magician.a; the curl-aware variant above is the one name it does not print.

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. Runtime introspection uses compiler-emitted descriptors rather than linker symbol names. Throwable::getTrace() and getTraceAsString() currently return an empty array and string, and the uncaught-exception report prints no stack trace. The linker names are unnecessary at run time and 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.