A Zig framework for building PHP extensions with PHP C API bindings.
ℹ️ Note: phpz already covers most PHP extension use cases, but the API is still evolving. Pin to a specific commit if you need stability.
- zig: 0.17.0
- php: 8.2-8.5 (NTS/ZTS) on Linux (x86_64/aarch64), macOS (x86_64/aarch64), Windows MSVC (x64)
- Modules — extension metadata, lifecycle hooks, phpinfo, INI, globals, observers, and exports.
- Stub integration — arginfo, function entries, constants, attributes, class metadata, and arginfo checks.
- Functions and methods — typed
Ctxarguments, returns, nullable values, raw zvals, and PHP argument errors. Scalar parameters follow ZPP exactly: weak-mode coercion,strict_types, int-to-float widening, null deprecations and Zend's own error messages. - Classes and OOP — bind Zig
structlifecycles to PHP objects, with class entries, methods, inheritance, interfaces, enums, exceptions, handlers, and optional operator overloading and comparison. - Runtime state — typed module globals, PHP/Zend globals, and superglobal access for NTS/ZTS builds.
- Zval and Zend APIs — zval conversions, ownership helpers, arrays (including
ListBuilderforZEND_HASH_FILL_PACKED-style construction andsymtable*lookups with PHP key semantics), strings, objects, callables, functions, and resources. - Native closures — create PHP
Closurevalues withFunction.createClosure(),closure.createFromHandler(), orclosure.createFromFn(). - INI, phpinfo, and errors — typed INI values, phpinfo helpers, PHP errors, exceptions, and bailout-safe cleanup.
- Observers — function-call, error, and exception hooks for profiling and monitoring.
- Memory — PHP-backed Zig allocator for any alignment, with optional debug leak traces that are safe under ZTS.
- Build integration — PHP C translation, extension linking, and platform-specific setup.
- PHP build system — optional support for built-in and shared extensions. Pass
--with-php-build-systemtophpz_skel.php; see the skeleton guide. - Cross-compilation — build Windows x86_64 (MSVC) extensions from Linux; see
scripts/build-windows.sh.
Generate a PHP extension skeleton:
# Generate a new PHP extension skeleton
# PHP >= 8.3:
curl -fsSL https://raw.githubusercontent.com/happystraw/phpz/dev/tools/phpz_skel.php \
| php -- --ext my_php_extension
# PHP 8.2:
curl -fsSLO https://raw.githubusercontent.com/happystraw/phpz/dev/tools/phpz_skel.php
php phpz_skel.php --ext my_php_extension
# Run tests (Optional)
cd my_php_extension
zig build run-testsFor a complete minimal project, see
examples/skeleton.
zig fetch --save git+https://github.com/happystraw/phpzA phpz extension usually contains:
build.zig.zon— declares thephpzdependencybuild.zig— initializesPhpzand callsphpz.addExtension<name>.stub.php— PHP API declarations<name>_arginfo.h— generated by PHP'sgen_stub.php<name>.h— includesphpz.hand the generated arginfo headersrc/root.zig— registers functions, classes, and the module
php /path/to/php-src/build/gen_stub.php <name>.stub.phpzig build -Dphp-include-dir="$(php-config --include-dir)"For the complete build.zig, stub.php, header, Zig module, and PHPT tests, use examples/skeleton as the reference.
Check out the examples/ directory for complete working examples.
Build-time Compile-time Runtime
────────── ──────────── ───────
stub.php Zig source
│ │
│ php-src/build/gen_stub.php │
▼ │
arginfo.h │
[ext_functions] │
[register_class_*] │
[register_{name}_symbols] │
│ │
│ translate-c (zig build) │
▼ ▼
PHP C bindings ─────────────► phpz comptime:
(php_c module) ├─ phpz.function()/functions() → zif_* export
├─ phpz.Class() → wrapper + zim_* exports
└─ phpz.module() → get_module() export
│
├─── PHP loads .so
│
▼
module_startup [auto]
├─ register_{name}_symbols (constants)
├─ ini_entries
└─ user hook
└─ Class.register() (classes) [manual]
-
Build-time —
gen_stub.phpgeneratesarginfo.hfrom.stub.php, containing function metadata andregister_*symbols.translate-cconverts PHP C headers into Zig bindings. -
Compile-time —
phpz.function()andphpz.functions()exportzif_*wrappers,phpz.Class()creates wrapper types, andphpz.module()exportsget_module()for the dynamic loader. -
Runtime — PHP loads
.so→get_module():What Registered by When How Functions ext_functionsModule init Auto: set in module entry struct Constants register_{name}_symbolsMINIT Auto: called by module_startup_funcClasses register_class_*MINIT Auto: .classesofphpz.module, in the given orderFunctions are resolved from the module entry when the extension loads; constants are registered during MINIT via the auto-generated
register_{name}_symbols. Classes listed in.classesare registered in order beforemodule_startup_fn, so list parents and interfaces first; use a class's.registeroption to supply them.
Set .tsrmls_cache_tcb_fast_path = true in the options passed to Phpz.init to enable
direct TCB access to PHP's TSRM cache on Linux x86_64/aarch64 ZTS builds. It defaults
to false, which disables both TCB offset detection and direct thread-pointer reads.
When disabled or unavailable, access uses the static cache by default, or runtime
lookup with .static_tsrmls_cache = false. The two options are independent of LTO;
NTS builds ignore both TSRM options.
Zend APIs, zvals, php_allocator and request memory belong to the PHP thread that is running the request.
Native worker threads must not touch them, under NTS or ZTS.
A Zend bailout uses longjmp: Zig defer and errdefer
between the bailout and the boundary that catches it do not run.
php_allocatoruses PHP's fallible allocation API when available, or catches the immediate OOM bailout on older PHP versions. Both paths returnerror.OutOfMemory, allowing the caller's Zig cleanup to run. Propagate the error: phpz handlers process a queued OOM or resume the earlier bailout. With delayed OOM, PHP leavesUnwindExitpending when an execution frame is available; otherwise it bails out.- The
try*APIs (tryCall,tryNew,tryEval, ...) convert an escaping bailout toerror.ZendBailout. Cleanup outside that boundary can run; cleanup skipped inside it is not replayed. These APIs can also returnerror.UnwindExitfor exit/die or delayed OOM. Propagate these errors after native cleanup; standalone hosts must finish request shutdown. - Other Zend calls can bail out directly. Keep native resources that must be released on bailout in a
GuardCtxscope, or in objects PHP destroys at request shutdown. - An
error.OutOfMemorywithout an existing PHP OOM, such as a native allocation failure, becomes a PHP fatal "Out of memory" error. - On PHP versions with a hard memory limit, a non-fallible allocation that would exceed it terminates the process directly, without Zig cleanup or PHP request shutdown.
phpz was inspired by ext-php-rs.