Shared Libraries (cdylib)
Compile PHP functions into a C-callable shared library with --emit cdylib and #[Export].
--emit cdylib compiles a PHP file into a loadable shared library instead of a
standalone executable. Top-level functions marked with #[Export] become
C-ABI symbols. Unnamespaced C-safe PHP names keep their existing spelling;
namespaced names replace namespace separators with underscores, so
Demo\roundtrip is exported as Demo_roundtrip. The compiler rejects any
resulting public-name collision and writes a deterministic C header containing
the exact mapped ABI.
The mode supports macOS aarch64 (.dylib), iOS device and Simulator aarch64
(.dylib), Linux aarch64 (.so), and Linux x86_64 (.so). iOS libraries are
cross-compiled from macOS with the matching Xcode SDK; staticlib remains the
usual Xcode delivery form. The ABI is single-threaded: a host must not call the
same Elephc library concurrently. Boundary depth and concat scratch state are
nest-safe, so internal recovery cannot accidentally deactivate an outer
boundary.
Building a cdylib
elephc --emit cdylib auth.php
# Linux: auth.php -> libauth.so and libauth.h
# macOS: auth.php -> libauth.dylib and libauth.h
The aliases --emit dylib and --emit shared are accepted. A cdylib has no
main entry point and does not run top-level statements when loaded; the host
drives it through the public functions.
--emit staticlib produces lib<name>.a plus the same generated header and
public ABI. It is linked into the final host executable instead of loaded with
dlopen, and uses direct final-link relocations instead of PIC/GOT references.
Exporting functions
<?php
#[Export]
function add_i64(int $a, int $b): int {
return $a + $b;
}
#[Export]
function roundtrip(string $input): string {
if ($input === "__elephc_fail__") {
throw new RuntimeException("requested roundtrip failure");
}
return $input;
}
Only top-level functions can be exported. The fully-qualified spelling
#[\Elephc\Export] is also accepted. In executable mode the attribute is
ignored with a warning.
Generated header and type marshaling
The generated libauth.h includes standard integer/size types, ABI and status
constants, C++ linkage guards, boundary declarations, every resolved export
prototype, and ownership comments.
Existing scalar signatures keep their original ABI:
| PHP type | C parameter | C scalar return |
|---|---|---|
int | int64_t | int64_t |
float | double | double |
bool | int64_t (0 or 1) | int64_t |
string | const char *ptr, size_t len | see the owned-string ABI below |
void | — | void |
Scalar calls now run through the same recoverable native boundary while keeping
those return types unchanged. On failure they return the zero value for their C
return type (0, 0.0, or no value for void); the host must call
elephc_last_status() to distinguish that failure from a legitimate zero.
Every fixed parameter list made from the supported scalar and string types can
return string. The wrapper preserves the ordinary flattened inputs and
appends the caller-owned output addresses. A function such as roundtrip has
this C prototype:
int32_t roundtrip(
const char *input_ptr,
size_t input_len,
char **output_ptr,
size_t *output_len
);
output_ptr and output_len are required. The wrapper clears both outputs
before doing work and keeps them NULL/zero on every failure. A non-zero input
length requires a non-NULL input pointer; NULL, 0 is an empty PHP string.
Input and output are byte sequences, so embedded NUL and non-UTF-8 bytes are
preserved.
On success the wrapper returns ELEPHC_STATUS_OK, publishes an independently
owned copy, and sets its exact byte length. The current buffer also has a
convenience trailing NUL, which is not included in output_len; hosts must use
the length as authoritative. Release every successful result with
elephc_free(). Never call free() on it or retain a borrowed runtime pointer.
Zero-argument string returns therefore receive only output_ptr and
output_len; mixed or multi-parameter signatures place those two parameters
after all flattened inputs. Arrays, objects, callables, nullable, variadic, and
by-reference export parameters and returns remain unsupported.
Boundary API and statuses
Every generated header declares:
uint32_t elephc_abi_version(void);
int32_t elephc_init(void);
void elephc_shutdown(void);
int32_t elephc_last_status(void);
const char *elephc_last_error(void);
void elephc_free(void *ptr);
Call elephc_init() after loading the library and elephc_shutdown() before
unloading it. elephc_abi_version() must match ELEPHC_ABI_VERSION from the
header. Initialization also arms the call-stack overflow guard for host-entered
PHP calls. elephc_free(NULL) is safe.
ABI version 3 adds recovery for the pre-existing scalar signatures and the
elephc_last_status() query. A successful scalar or owned-string call records
ELEPHC_STATUS_OK; a recovered scalar failure records the same named status
that the owned-string wrapper would return directly.
The named status constants are:
| Status | Meaning |
|---|---|
ELEPHC_STATUS_OK | Call succeeded and any output ownership transferred |
ELEPHC_STATUS_INVALID_ARGUMENT | Required pointers or pointer/length pairs were invalid |
ELEPHC_STATUS_PHP_EXCEPTION | A supported PHP exception escaped the exported function |
ELEPHC_STATUS_ALLOCATION_FAILURE | Runtime or output-buffer allocation failed |
ELEPHC_STATUS_RUNTIME_FAILURE | Another recoverable boundary failure occurred |
After a failure, elephc_last_error() returns a borrowed, NUL-terminated
diagnostic. It returns NULL when no error is recorded. The pointer is owned by
the library, must not be freed, and remains valid until the next exported call
or lifecycle reset. Every successful exported call and each lifecycle reset
clears the recorded error. An exception whose message is empty still records an
error: the function returns a non-NULL pointer to "", not NULL.
Recoverable errors and restrictions
Every export wrapper installs Elephc’s native exception boundary before it
calls PHP. Escaping supported Throwables are converted to
ELEPHC_STATUS_PHP_EXCEPTION; boundary-reachable allocation failures receive
their own status. A guarded call-stack overflow or a defensive escape from a
runtime exit path becomes ELEPHC_STATUS_RUNTIME_FAILURE rather than
terminating the host. Function-frame cleanup runs before control returns to C,
so a host can inspect the error and call the library successfully again.
ABI validation failures, supported PHP exceptions, and allocation failures are recoverable. Hardware faults, memory corruption, and foreign code that aborts the process are not contained.
exit and die cannot return a status, so the compiler rejects them when they
are transitively reachable from any export, including through fixed
constructors/destructors, implicit object conversion and property hooks,
ArrayAccess, Countable, JsonSerializable, Iterator/IteratorAggregate,
include-variant dispatchers, and statically resolved runtime callbacks or
closures. It also rejects reachable fatal EIR terminators, fatal builtin
argument subsets, eval, runtime-selected constructors, foreign calls, and
other opaque invocation paths when it cannot prove process termination
unreachable. The same validation runs for exported code under plain --check
and --emit-ir, not only during final cdylib emission.
C consumption
Prefer including the generated header rather than copying declarations:
#include "libauth.h"
#include <string.h>
int main(void) {
const char input[] = {'A', 0, 'B'};
char *output = NULL;
size_t output_len = 0;
if (elephc_init() != ELEPHC_STATUS_OK ||
roundtrip(input, sizeof(input), &output, &output_len) != ELEPHC_STATUS_OK) {
return 1;
}
int valid = output_len == sizeof(input) &&
memcmp(input, output, sizeof(input)) == 0;
elephc_free(output);
elephc_shutdown();
return valid ? 0 : 2;
}
The library can be linked normally with -L. -lauth and an appropriate loader
search path, or loaded with dlopen/dlsym. See examples/cdylib/ for a
complete error-and-recovery host.
Symbol visibility and PIC
The public symbol table contains only declared #[Export] functions plus
elephc_abi_version, elephc_init, elephc_shutdown,
elephc_last_status, elephc_last_error, and elephc_free. ELF internals use
hidden visibility; Mach-O internals are private externs. On Linux the
CRT-supplied _init/_fini definitions are localized as well. Runtime helpers,
buffers, data constants, and non-exported PHP functions therefore do not become
host ABI or preempt another loaded Elephc library’s state.
Cdylib code generation is position-independent. Global references use the GOT
(@GOTPCREL on x86_64 and :got:/:got_lo12: on AArch64), allowing the
dynamic loader to relocate the library.
Current limits
- One
.phpor.lfcentry source per cdylib; normal includes/requires still work. - The ABI is single-threaded and exposes no per-host runtime context.
- String results use the caller-owned status/out surface for every fixed list of
supported scalar/string inputs; scalar-return signatures preserve their
existing C prototypes and expose recovered status through
elephc_last_status(). - Array, object, callable, nullable, variadic, by-reference, and generic string value signatures are not public ABI.