← All docs

Hosting PHP extensions

Build real PECL, PIE, or in-house C extensions from source and call them from compiled programs with elephc extension.

Strict mode: hosted extensions are declared through the project’s elephc.toml, not in source, so --strict-php programs can use them. What they add to a program is exactly the PHP surface the extension registers.

Elephc can host real PHP extensions — a PECL release, a PIE package from Packagist, or an extension you maintain yourself — inside a compiled program. The extension is built from its own C (or C++) sources, and every function it registers becomes an ordinary PHP function your code calls by name:

elephc extension add [email protected]
<?php
$config = simdjson_decode(file_get_contents("config.json"), true);
echo $config["name"], "\n";
elephc main.php && ./main

No PHP installation is involved, at build time or at run time: the binary is still standalone.

How it works

A PHP extension is written against the Zend engine’s C API: its hash tables, strings, argument parsing, class registration, objects and exceptions. Elephc does not use php-src for that. It ships its own implementation of that API, written in Rust, in the elephc-zend crate (libelephc_zend.a, installed next to the other bridge archives):

  • the types and functions an extension sees — zval, zend_string, HashTable with PHP’s exact memory layout, zend_parse_parameters(), zend_hash_*, add_assoc_*, zend_throw_exception()… — are Rust definitions. The C header the extension’s compiler reads is generated from them when Elephc is built, so the two cannot drift;
  • the macros (RETURN_STR, Z_TYPE_P, ZEND_HASH_FOREACH_*, ZEND_PARSE_PARAMETERS_START…) are the one part that must be C text, since an extension expands them at compile time. They live in a single zend_macros.h, beside a small C file for the variadic entry points (zend_error(), spprintf(), zend_parse_parameters()) Rust cannot define;
  • the include files an extension asks for (php.h, zend_API.h, ext/standard/info.h…) all lead to that generated header.

Nothing is downloaded for this: the headers are embedded in the elephc binary and written once to the managed cache.

elephc extension add then:

  1. downloads the extension and pins the archive’s SHA-256 in elephc.toml (PECL publishes no checksums, so the first download is trusted and every later one must match it);
  2. evaluates the extension’s own config.m4 — the file phpize feeds to configure — to learn its sources, compiler flags and config.h defines. Options take the defaults the extension declares, and autoconf probes are answered as a modern POSIX system would, except function, library and header checks (AC_CHECK_FUNCS, AC_CHECK_LIB/PHP_CHECK_LIBRARY, AC_CHECK_HEADERS), which link or compile a test program with the target toolchain since platforms differ there;
  3. compiles it with the target C/C++ toolchain into its own static archive;
  4. starts it once — running its MINIT against the engine — and records what it registered: functions with their argument info, classes, constants and INI directives. Taking the surface from the running extension rather than from a .stub.php means #ifdefs, aliases and functions declared only in C arginfo (as simdjson’s are) are all seen.

When you compile, Elephc declares that surface to your program and links the extension’s archive and the engine. A hosted function whose extension your program never calls costs nothing: its declaration is pruned and nothing from its archive is linked.

Sources

elephc extension add apcu                  # newest stable PECL release
elephc extension add [email protected]           # an exact PECL release
elephc extension add vendor/[email protected]  # a PIE package (Packagist type php-ext)
elephc extension add demo --path ext/demo  # a source tree in your project

The manifest records each source:

[extension]
schema = 1

[extension.dependencies]
simdjson = { version = "4.0.0", sha256 = "1fb48fe5…" }
apcu = { pie = "apcu/apcu", version = "5.1.28", sha256 = "…" }
demo = { path = "ext/demo" }

A path source is rebuilt whenever its contents change. PIE packages must be hosted on GitHub. Zend extensions (php-ext-zend, such as Xdebug) are refused: they hook the PHP engine’s executor, which a compiled program does not have, so they would link cleanly and never run.

elephc extension install builds every declared extension that is missing (for example on a fresh CI machine), elephc extension list shows what is declared and built, and elephc extension remove <name> drops a declaration.

INI directives

Directives in [extension.ini] are applied before any extension starts, the way php.ini is:

[extension.ini]
"apc.enable_cli" = "1"
"apc.shm_size" = "64M"

What crosses the boundary

From your programTo the extensionNotes
int, float, bool, null, stringthe same zval typesstrings are copied
indexed and associative arrays, nestedPHP arrayscopied, so the extension may keep them
objects of the extension’s classes, including inside arraysthe same objectnot copied: see Classes
closures, function names, first-class callables, [$object, "method"]a callable the extension can callsee Callables
other objects—not yet: see below
From the extensionTo your program
scalars, strings, arrays (any nesting)the same Elephc values
stdClass objects, including inside arraysstdClass objects
objects of the extension’s classes, including inside arraysinstances of those classes
other objects, resourcesan Error naming the type
a value that contains itselfan Error; a value shared along two paths arrives as two copies

Exceptions thrown by the extension are rethrown as the same class when Elephc declares it — the extension’s own exception classes are declared for you, with their parents — or as the nearest ancestor that is declared. An E_WARNING is reported on stderr and the call returns, as in PHP; an E_ERROR ends the program with exit status 255.

Argument types come from the extension’s arginfo. An optional parameter whose default is written in arginfo is declared with it; one whose default only the C code knows is passed only when your call passes it, exactly as PHP does. Named arguments work as in PHP, including PHP’s errors for an unknown name and for skipping a parameter whose default is not known. By-reference arguments the extension wrote are written back even when it then throws.

Classes

Every class and interface an extension registers is declared to your program under its own name, with its constants and public methods:

<?php
use Ds\Map;

$map = new Map(["a" => 1]);
$map->put("b", 2);
$map["c"] = 3;                     // ArrayAccess
echo count($map), "\n";            // Countable
echo json_encode($map), "\n";      // JsonSerializable
$pair = $map->first();             // a Ds\Pair the extension created
echo $pair->key, "=", $pair->value, "\n";

The object itself lives in the engine, exactly as the extension built it (its own C struct included); your program holds it through an instance of the declared class. So:

  • new, instance and static methods, class constants and properties (read, written, isset, unset) go to the extension, which answers as it would in PHP.
  • An object the extension returns — a factory’s, an element of an array, a method returning $this — arrives as an instance of its class. An object your program already holds comes back as that very instance, so === and object-keyed collections see one object, as in PHP. Keeping track of it takes a weak reference, which uses an object id of its own: objects created after one of these get a higher spl_object_id() (and #N in var_dump()) than in PHP.
  • var_dump() and print_r() show the engine object’s debug table, unless your subclass declares its own __debugInfo().
  • Such objects can be passed back as arguments, typed with their class.
  • You may extend the classes; your subclass keeps the extension’s behaviour.
  • Countable, ArrayAccess and JsonSerializable are declared on a class whose methods have exactly their shape, so count(), $object[...] and json_encode() work.
  • A class that is Traversable in the extension (an IteratorAggregate, an Iterator, or one with only a get_iterator handler) can be walked with foreach, keys included, and passed to iterator_to_array(), iterator_count() and iterator_apply(). Breaking out of the loop releases the extension’s iterator.
  • An object is released when its last instance is; the extension’s destructor and free_obj handler then run, as in PHP.
  • clone goes through the class’s clone_obj handler, so the copy carries the extension’s own state; a class without one throws PHP’s Error (“Trying to clone an uncloneable object”).

Callables

A function or method taking a callable (one or a variadic callable ...$callbacks) accepts any PHP callable:

<?php
use Ds\Seq;

$v = new Seq([3, 1, 2]);
$v->sort(fn($a, $b) => $b <=> $a);
echo json_encode($v->map(fn($x) => $x * 10)), "\n";   // [30,20,10]
echo apcu_entry("k", "strtoupper"), "\n";             // K

The extension calls it with zend_call_function(), as it would in PHP: the arguments it passes arrive as Elephc values, and the callable’s result goes back to it. An exception thrown by the callable stops the extension’s work and reaches your program as the very same object. An extension may keep the callable after the call returns; it is released when the extension drops it. A function taking a callable by reference (callable &$callback) is declared but cannot be called yet: it throws an Error saying why.

Current limits

  • Objects of your own classes cannot be passed in yet, except to a parameter typed with the extension’s own class. A function taking any other object is still declared, but calling it throws an Error explaining why, and elephc extension add lists them.
  • Magic methods other than __toString and __debugInfo (__serialize, __invoke…) are not forwarded. var_dump() and print_r() show an extension object’s debug table, as PHP does.
  • The engine is PHP’s extension API, not PHP’s runtime. Elephc’s engine provides what extensions call to handle values, parse arguments, register functions, classes, constants and INI directives, throw exceptions, serialize, hash (MD5, SHA-1, SHA-256), convert with iconv and format numbers. It does not run PHP code: an extension calls back only the callables it is given, so one that calls a PHP function by name (call_user_function()) or needs the compiler (ast) cannot work. For the same reason, serializing an extension object whose class has its own hooks (Serializable, __serialize/__unserialize, __sleep/__wakeup) is refused rather than done without running them.
  • The SAPI an extension sees is the program’s: cli, or cli-server for a --web program (PHP_SAPI, sapi_module.name, sapi_cli_is_cli()), as php_sapi_name() answers. Resources still open when a request ends are closed then, and persistent resources at program exit, as in PHP.
  • Integrations with other extensions are compiled out. Elephc does not host session, pcre, streams or output buffering, so an optional integration guarded by HAVE_PHP_SESSION and the like (msgpack’s and igbinary’s session serializers) is simply not built. An extension that requires another extension’s C API fails to link, and add names the missing symbols; one that reaches a stream or output-buffering entry point at run time ends the program with a fatal error naming it.
  • A by-reference output parameter must be passed a declared variable, whatever it holds ($success = null; apcu_fetch("k", $success);): it comes back holding whatever the extension wrote. An undefined variable is refused at compile time.
  • Extensions are built for the host target. Their surface is read by running them, so cross-installing for another target is not supported yet.

Tested extensions

These PECL extensions are built from their released sources and their output compared line for line with PHP 8.5 loading the same extension:

ExtensionVersionResult
apcu5.1.28identical
base581.0.2identical
brotli0.21.0identical
geospatial0.3.5identical
igbinary3.2.17RC1identical
lzf1.7.0identical
msgpack3.0.1identical
scrypt2.0.1identical
simdjson4.0.0identical (C++)
trader0.5.1identical
var_representation0.1.5identical
mailparse3.2.0identical, except that an uncaught exception omits PHP’s in FILE:LINE and stack trace
translit0.7.2identical, except that warnings omit PHP’s in FILE on line N suffix
zstd0.18.0identical, except that warnings omit PHP’s in FILE on line N suffix
bitset3.2.1identical (classes, static factories, object arguments, subclassing)
ds2.0.0identical (callbacks, comparators, foreach, Ds\Pair)
jsonpath3.1.0identical
astrefused: it needs the PHP compiler’s internals

See also