Release 8.4.2
⚡ Z-Engine 8.4.2 — "The engine speaks its own language"
126 commits. 154 files. +6,400 / −3,000 lines.
A patch number that undersells it: the library that hands you PHP's newest internals is now written in them — plus a pluggable allocator seam that opens Z-Engine to memory it doesn't own. 🚪
composer require lisachenko/z-engine:^8.4🔢 PHP 8.4 only.
8.4.xstays byte-exact for PHP 8.4. Everything here also cascades upward tomasterfor the 8.5 line.
🧬 The modernization sweep
Z-Engine exposes PHP 8.4's newest internals. It was still written like it was 2019. A sustained refactor pass fixed that — the library now uses the language it exposes:
- 🔐 Asymmetric visibility (
private(set)) guards the engine views —Core::$executor,Core::$compilerand friends are readable but no longer reassignable from outside - 🏷️
#[\Override]across the reflection and hook layers — the compiler now catches a renamed parent method, instead of a test suite catching it much later - 🎯 First-class callable syntax for trampoline installation and resolved closures
- 🔢 Backed enums for the constant groups Z-Engine owns —
CastType,PropertyPurpose,SendMode,DependencyType,VersionRelation,DescriptorSlot. Raw magic integers at hook boundaries are gone. - 🔎 PHP 8.4 array predicates replacing hand-rolled search loops
- 🏗️ Named arguments reconstructing opcache cache headers
- 🧱 Typed class constants,
matchover the opcode-node value lookup, modernized string / class-name / coalesce idioms - 🏭 Named constructors for exceptions throughout the Memory, Type, module and opcode-table layers — every failure explains itself instead of arriving as a generic message
Concrete cleanups underneath: hook classes sealed, the flag-toggle and visibility-setter dances extracted, engine-struct access routed through owning classes, and the hook layer typed with the generated struct stubs from 8.4.1. Bridged-iterator state now travels in a typed object, and every proceed() routes through getOriginalCallable().
🔤 One rename worth noting: StringEntry::copy() is now StringEntry::addReference(). It always was a refcount bump — now the name says so.
🧠 The allocator seam
The most forward-looking addition here, and the groundwork for fork-shared and coroutine-scoped memory.
Every persistent primitive — PersistentObjectFactory's object clones, StringEntry::persistent()'s interned blocks, a PersistentHashTable's struct — used to allocate from exactly one place: the malloc-backed FFI allocator. Now they allocate through an Allocator interface you can implement:
interface Allocator
{
public const int DEFAULT_ALIGNMENT = 16;
public function allocate(int $size, int $alignment = self::DEFAULT_ALIGNMENT): int;
public function ownsAllocations(): bool;
// ...
}Three deliberate decisions make it usable from outside the framework:
- 📮 It speaks in addresses, not
CData. A public Z-Engine API never leaks engine handles — so an implementation living in your package can be written against nothing but integers, binding its ownmmap/shmprimitives withFFI::cdefand returning the address it computed. - 🛑
ownsAllocations()is part of the contract. Z-Engine must neverfree(3)a block inside somebody else's arena —destroy()refuses rather than corrupting it. - 🔄 Passing nothing changes nothing. The default
EngineAllocatoris the malloc-backed allocator Z-Engine has always used.
The target sinks are the ones the process heap can't serve: a fork-shared mmap arena, a shared-memory segment, a preallocated slab. Paired with the new external arData install API, that means persistent engine structures living in memory Z-Engine doesn't own. 🧵
🌳 A consumer-safe compiler surface
Installing an AST hook meant reaching for internals and hoping. Now there's a bracket for it:
$result = Core::$compiler->processInCompilationMode(true, function (): mixed {
// compile-time-only work, with the flag restored no matter how this exits
});Consumer-safe file-name access for AST hooks lands alongside it — and the object-store and class-table operations consumers actually reach for are now named public API rather than something to rediscover from the source each time. ObjectEntry encapsulates its own store registration. 📇
HashTable also grows bucket-key lookup and replacement, and the Type layer exposes property slots and uncounted payload writes on the owning classes. 🪣
🩹 Fixes
- 🎛️ Opline operands are read off the
znode_opunion instead of cast through it — the old approach worked by luck of layout, not by contract - 🔥
EG(fake_scope)is restored even when the original handler throws — an exception mid-hook no longer strands the engine in a foreign scope - 🐌
ObjectEntryresolves the dynamic-properties table lazily instead of eagerly on every wrap - 🛡️
addInterfaces()guards the class-table lookup - 🗑️ Every
pDestructor-disabled delete routed throughHashTable - 🧮 AST child indexes reject negative values through the plain bounds guard
- 🧵 The materialized alias is dropped before a registered clone is freed
- 📛
detectModuleName()kept off the null path ofpreg_replace_callback
⚡ Plus real performance work: loop-invariant struct sizes hoisted out of engine walk/copy loops, the executing frame resolved straight from execute_data in OpCodeHook, instances minted via native reflection with redundant casts dropped, and the last sizeof(type(...)) call sites migrated to Core::sizeOfType().
🏗️ Quality, tests & CI
This is where a surprising amount of the 126 commits went — and it's why the refactor above could be trusted:
- 🧪 The test suite is typed, and now fails on swallowed deprecations and notices instead of passing quietly through them
- 📊 The performance group runs in CI, and the AST node wrappers finally have coverage
- 🧹 One PHPStan configuration instead of several, with PHPUnit taught to it, dead code and broken phpdoc parked in the baseline resolved, and baseline entries pruned as the refactors freed them
- 🔒 Hardened workflows, third-party actions pinned to version tags, static-analysis tooling cached, Docker layer caches keyed on the base image, and Dependabot now watching actions and Docker images as well as Composer
- 📋 A security policy and a pull request template
- 📜 A uniform license header across the whole linted corpus, root-level scripts included
⚠️ Unchanged: still experimental
Z-Engine operates on raw engine memory. Segfaults are a feature of the territory, not a bug in your code. Match the branch to your PHP minor, keep the JIT off for the debugger primitives, and develop against a debug build.
💙 Thank you
A 154-file refactor of a library built on byte-exact struct offsets, landed without breaking it. Thanks to Claude for making this one possible. 🙏