PHPackages                             lisachenko/z-engine - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. lisachenko/z-engine

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

lisachenko/z-engine
===================

Write PHP extensions in pure PHP: direct FFI access to the Zend Engine internals

0.9.1(5y ago)45723.7k↑114.7%24[19 issues](https://github.com/lisachenko/z-engine/issues)[1 PRs](https://github.com/lisachenko/z-engine/pulls)3MITPHPPHP 8.0.\*CI passing

Since Sep 25Pushed 1w ago15 watchersCompare

[ Source](https://github.com/lisachenko/z-engine)[ Packagist](https://packagist.org/packages/lisachenko/z-engine)[ GitHub Sponsors](https://github.com/lisachenko)[ RSS](/packages/lisachenko-z-engine/feed)WikiDiscussions 8.4 Synced 2w ago

READMEChangelog (3)Dependencies (1)Versions (12)Used By (3)

⚡ Z-Engine
==========

[](#-z-engine)

### Write PHP extensions in pure PHP.

[](#write-php-extensions-in-pure-php)

**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.

[![CI](https://camo.githubusercontent.com/045435d0cc5ef84eac77d8115a9aba86f6a547521739dba4a3bc41f2c0c7dfaf/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6c6973616368656e6b6f2f7a2d656e67696e652f63692e796d6c3f6272616e63683d6d6173746572266c6162656c3d4349)](https://github.com/lisachenko/z-engine/actions/workflows/ci.yml)[![GitHub release](https://camo.githubusercontent.com/923019e78b236114dec319ab72520c27a14ea9af00c3e48d5fbd4a82911d651c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f72656c656173652f6c6973616368656e6b6f2f7a2d656e67696e652e737667)](https://github.com/lisachenko/z-engine/releases/latest)[![PHP Version](https://camo.githubusercontent.com/8d2c6579d8a6d6a3882ce078eb514a0f52e840e4563ed7e6ca2bfd0ad41528e4/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d382e34253230253743253230382e352d3838393242462e737667)](https://php.net/)[![License](https://camo.githubusercontent.com/1984901eb37c10a5a1a3a8a09a0231df9178e0f4d1efc6cbb5cf79671ed608ad/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6c6973616368656e6b6f2f7a2d656e67696e652e737667)](https://packagist.org/packages/lisachenko/z-engine)[![PHPStan](https://camo.githubusercontent.com/b6d441ad4fe8332cb16c72aa27f22cc685181dfd74ae34964afc92c6c1146b3c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6c6576656c2532306d61782d627269676874677265656e2e737667)](https://phpstan.org/)

---

> **⚠️ 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.

Why this is different
---------------------

[](#why-this-is-different)

Every other "runtime magic" library for PHP stops at the boundary of userland. Z-Engine walks straight through it. Using [PHP FFI](https://www.php.net/manual/en/book.ffi.php), 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:

CapabilityWhat 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 opcodeHow it works
------------

[](#how-it-works)

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."

Requirements &amp; support matrix
---------------------------------

[](#requirements--support-matrix)

- PHP with the **FFI** extension enabled
- **x64/arm64** builds, **NTS and ZTS** (the opcache file-cache relocator is not yet supported on ZTS — [\#118](https://github.com/lisachenko/z-engine/issues/118) — nor on Windows — [\#119](https://github.com/lisachenko/z-engine/issues/119))

Engine memory layouts change between every PHP minor version, so each PHP minor has its own generated definitions and its own branch.

PHPOS / Arch / TSBranchStatus8.5linux-x64-nts`master`🚧 in progress8.4linux-x64 (nts, zts), darwin-x64 (nts, zts), darwin-arm64 (nts, zts), windows-x64 (nts, zts)`8.4`✅ supported8.0linux-x64-nts`8.0`🧊 frozen (legacy)> **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.

Memory safety &amp; long-running PHP
------------------------------------

[](#memory-safety--long-running-php)

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 `$this` and 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](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.

Installation
------------

[](#installation)

```
composer require lisachenko/z-engine
```

There is nothing to initialize: the engine bridge is booted from Composer's autoloader, so `require __DIR__ . '/vendor/autoload.php'` is all a consumer needs. That includes the `opcache.preload` stage, which the bootstrap recognises and serves by publishing the engine definitions for the life of the server rather than for the preload request alone — pointing `opcache.preload` at a script that only requires the autoloader is enough to get the per-request cost down.

`Core::init()` remains public, idempotent and re-invocable, for the cases that want it: booting explicitly at a chosen point, re-booting after `Core::shutdown()` inside a live worker, and turning "the engine is not available here" into its explanation. On a host that cannot run the engine at all — no ext-ffi, `ffi.enable=0`, an unsupported PHP minor or platform — autoloading stays silent and leaves `Core` uninitialized, so static analysis and test suites still load the package; ask `Core::isInitialized()` for the state, or call `Core::init()` to get the reason. Set `ZENGINE_AUTOBOOT=0` to skip the automatic boot entirely.

### Hello, impossible

[](#hello-impossible)

```
