Z-Engine reaches straight into the heart of the PHP runtime โ the Zend Engine โ and hands you its internals as ordinary PHP objects. Overload operators, make classes immutable, register real engine modules, rewrite the AST, redefine methods at runtime. No C, no compiler, no recompiling PHP. Just FFI and a lot of nerve.
โ ๏ธ Experimental โ not for production. Z-Engine operates on raw engine memory. Segfaults are a feature of the territory, not a bug in your code. Pin your PHP version, run it behind a debug build while developing, and never ship it in an app until 1.0.0.
Every other "runtime magic" library for PHP stops at the boundary of userland. Z-Engine walks straight through it. Using PHP FFI, it loads the exact C struct definitions of the running engine โ zend_class_entry, zval, zend_object_handlers, zend_module_entry โ and manipulates them the same way a compiled C extension would. The result is a set of capabilities that simply do not exist anywhere else in pure PHP:
| Capability | What you can do |
|---|---|
| ๐งฎ Operator overloading | Give your objects real +, -, *, /, **, == semantics via the engine's do_operation and compare handlers |
| ๐ Custom object handlers | Hook create_object, read/write/unset_property, cast_object, get_property_ptr_ptr โ build truly immutable objects, copy-on-write types, proxies |
| ๐งฉ Runtime engine modules | Register a genuine zend_module_entry at runtime, with persistent globals shared across requests โ an extension written entirely in PHP |
| ๐ณ Abstract Syntax Tree access | Parse source to the engine's own AST, inspect it, and rewrite it through the zend_ast_process hook |
| ๐ช Reflection on steroids | Make a final class non-final, add interfaces and methods at runtime, redefine method bodies, change a method's declaring class |
| โ๏ธ Opcode handlers | Install your own handler for any VM opcode |
FFI lets PHP load shared libraries, call C functions, and read C structures without a compiler or a third intermediate language. Z-Engine points that power back at PHP itself. It ships generated, version-exact FFI definitions of the engine's structures for each supported PHP version, and a runtime that refuses to boot unless the definitions match your interpreter down to the byte. That byte-exactness is what turns "insanely dangerous" into "dangerous but disciplined."
- PHP with the FFI extension enabled
- x64, non-thread-safe (NTS) builds
Engine memory layouts change between every PHP minor version, so each PHP minor has its own generated definitions and its own branch.
| PHP | OS / Arch / TS | Branch | Status |
|---|---|---|---|
| 8.5 | linux-x64-nts | master |
๐ง in progress |
| 8.4 | linux-x64-nts | 8.4 |
โ supported |
| 8.0 | linux-x64-nts | 8.0 |
๐ง frozen (legacy) |
| macOS / Windows / ZTS | โ | โ | ๐ tracked in issues |
Version matching is not optional. Running Z-Engine against a PHP minor it was not built for corrupts memory.
Core::init()enforces the match and aborts with a clear message rather than letting you crash.
Every value wrapper follows an explicit ownership model: owning constructors take their own
engine reference and release it deterministically (release()/destruction), fromCData()
factories stay borrowed, and all releases go through the engine's own primitives
(zval_ptr_dtor/rc_dtor_func) โ never through the FFI allocator. Engine hooks have a full
lifecycle (install()/uninstall()/reinstall()) backed by a registry, and Core::shutdown()
(registered automatically) restores every hooked engine pointer before the engine could ever
call a freed trampoline โ which is what makes worker loops and FPM + opcache preload viable.
Notable behaviour changes compared to older releases:
new StringEntry()/new ObjectEntry()/new ResourceEntry()addref and keep their target alive for the wrapper lifetime;ClosureEntry::setThis()releases the old bound$thisand references the new one (no more "object must outlive the closure").Compiler::parseString()trees free themselves when the last node wrapper is collected.AbstractHook::__destruct()no longer force-restores pointers at arbitrary GC moments.
See docs/long-running.md for the ownership tables, the hook lifecycle, runtime models (worker vs FPM), and the short list of immortal-by-design allocations.
composer require lisachenko/z-engineInitialize the library once, early in your bootstrap:
use ZEngine\Core;
require __DIR__ . '/vendor/autoload.php';
Core::init();For web (non-CLI) usage, enable FFI preloading by calling Core::preload() from the script named in your opcache.preload โ this loads the engine definitions once at server start instead of per request.
<?php
declare(strict_types=1);
use ZEngine\Core;
use ZEngine\Reflection\ReflectionClass;
require __DIR__ . '/vendor/autoload.php';
Core::init();
final class Sealed {}
$reflection = new ReflectionClass(Sealed::class);
$reflection->setFinal(false);
eval('class Extended extends Sealed {}'); // ...it just works.ZEngine\Reflection\ReflectionClass and ReflectionMethod extend the native reflection classes with write access to the engine:
$class = new ReflectionClass(Sealed::class);
$class->setFinal(false); // un-final a class
$class->setAbstract(true); // make it abstract
$class->addInterfaces(Countable::class); // graft on an interface at runtime
$class->addMethod('count', fn() => 42); // add a method from a closure
$method = new ReflectionMethod(Service::class, 'handle');
$method->setPublic();
$method->redefine(fn() => 'patched'); // swap the method bodyTurn a closure into a genuine engine function or method. Unlike a closure installed into an engine handler field (which ext/ffi calls back into through a slow libffi trampoline), a generated function is published straight into the engine's function table and afterwards dispatches through the normal Zend VM with zero FFI at call time โ exactly as fast as any ordinary PHP function:
use ZEngine\Reflection\ReflectionFunction;
ReflectionFunction::addFunction('twice', fn (int $x): int => $x * 2);
twice(21); // 42 โ a real global function, no trampoline
$class->addMethod('scale', fn (float $k) => ...); // same, as a methodSee docs/memory-model.md for how PHP zvals, FFI trampolines and native C handlers map to memory, and why this path is fast.
Give your value objects native arithmetic. Implement the extension interfaces and install the handlers with one call:
use ZEngine\ClassExtension\ObjectCreateInterface;
use ZEngine\ClassExtension\ObjectCreateTrait;
use ZEngine\ClassExtension\ObjectDoOperationInterface;
use ZEngine\ClassExtension\ObjectCompareValuesInterface;
use ZEngine\ClassExtension\Hook\DoOperationHook;
use ZEngine\Reflection\ReflectionClass;
class Matrix implements ObjectCreateInterface, ObjectDoOperationInterface, ObjectCompareValuesInterface
{
use ObjectCreateTrait;
public static function __doOperation(DoOperationHook $hook): self { /* ... */ }
// public static function __compare(CompareValuesHook $hook): int { ... }
}
(new ReflectionClass(Matrix::class))->installExtensionHandlers();
$c = new Matrix([10, 20, 30]) + new Matrix([1, 2, 3]); // Matrix([11, 22, 33])
$c *= 2; // โ Matrix([22, 44, 66])No access to the class source (e.g. it lives in vendor/)? Install the handlers imperatively instead:
$class = new ReflectionClass(Matrix::class);
$class->setCreateObjectHandler(Closure::fromCallable([ObjectCreateTrait::class, '__init']));
$class->setWritePropertyHandler(fn ($hook) => /* ... */);The available object hooks are create_object, cast_object, do_operation, compare, read_property, write_property, has_property, unset_property, get_property_ptr_ptr, get_properties_for, and interface_gets_implemented.
Install the
create_objecthandler first โ the other hooks live in memory that it allocates. Internal classes can't receive acreate_objecthandler.
Look up any live object by its handle โ an API PHP itself doesn't expose:
$instance = new stdClass();
$entry = Core::$executor->objectStore[spl_object_id($instance)];Parse PHP source to the engine's own AST and walk it:
$ast = Core::$compiler->parseString('echo 2 + 2;');
echo $ast->dump();You can also install a zend_ast_process hook to rewrite the AST of every file as it compiles.
Read the binary files opcache writes for opcache.file_cache, patch the compiled script through the framework wrappers, and write a valid binary back โ so the engine loads and executes your patched code on the next request:
use ZEngine\OpCache\BinaryCacheFile;
$file = BinaryCacheFile::compile(__DIR__ . '/Service.php', $cacheDir);
$reflection = $file->getReflection(); // ReflectionExtension-shaped handle over the cached script
// ... mutate literals, opcodes, flags through the usual wrappers ...
$file->refresh(); // rewrite the binary + invalidate the sourceThe payload is re-serialized from the mutated graph (not just byte-poked), so size-changing edits are written correctly. See docs/opcache-binary.md for the format, build-matching rules and current limits โ this is the foundation for AOP, transpiling and source-code protection on top of the file cache.
Register a real engine module at runtime, complete with persistent globals shared across requests:
use ZEngine\EngineExtension\AbstractModule;
final class Counter extends AbstractModule
{
protected static function globalType(): ?string { return 'unsigned int[10]'; }
}
$module = new Counter('counter');
$module->register();
$module->startup();
$globals = $module->getGlobals(); // FFI-backed, survives across requestsThese libraries are built entirely on Z-Engine and make good, real-world reading:
- lisachenko/immutable-object โ mark a class immutable with a single interface; property writes outside the constructor throw
- lisachenko/native-php-matrix โ a
Matrixtype with fully overloaded arithmetic operators
Z-Engine has a couple of unusual rules โ most importantly, match your PHP version to the branch and develop against a debug build. See CONTRIBUTING.md and AGENTS.md (the full contract for humans and automated tools). Engine definitions are generated from the PHP source by tools/generator/ and never hand-edited.
composer test # safe suite
composer test:internal # destructive tests, on a debug PHP build
composer phpstan # static analysis at level max
composer cs:check # coding standardsReleased under the MIT License.