[RFC] [FFI] Opt-in mapping of C struct types to userland PHP classes

From: Date: Wed, 12 Aug 2026 13:39:35 +0000
Subject: [RFC] [FFI] Opt-in mapping of C struct types to userland PHP classes
Groups: php.internals 
Request: Send a blank email to internals+get-132203@lists.php.net to get a copy of this message
Hi internals, I'd like to check interest in an FFI improvement before writing a formal RFC. I've filed the full proposal with an implementation sketch as a feature request: https://github.com/php/php-src/issues/23229 The problem: every C value FFI produces - a zend_string*, a zval*, a char* - is one and the same final class, FFI\CData. That single opaque type is what makes FFI flexible, but it also means no C struct a binding works with can ever be described to static analysis or an IDE, and CData being final closes off every userland workaround. Today a binding that wants any static typing has to ship a code generator emitting analysis-only stub classes, a .phpstorm.meta.php map, and an analyser extension - and the result is still strictly weaker than the real thing: instanceof can never work and native parameter types can never be enforced. I maintain the lisachenko/z-engine framework, which drives the Zend Engine's own structs through FFI, and I ship exactly that four-part workaround; every FFI binding generator hits the same wall. The proposal: an opt-in, per-scope class map, configured through an options array in the spirit of SoapServer/SoapClient: $ffi = FFI::cdef($code, $lib, options: [ 'classmap' => [ 'zend_string' => \My\Engine\ZendString::class, ], 'typemap' => [ /* C type => marshalling callbacks */ ], ]); final class ZendString extends \FFI\CData { public int $len { get => ...; } // property hooks over raw fields } With a type registered, every handle ext/ffi mints for it - from FFI::new(), FFI::cast(), struct-field reads, function returns - is an instance of the mapped class instead of bare CData. get_class() is truthful, instanceof works, and native parameter/return declarations are enforced by the engine. Implementation-wise this stays local to ext/ffi and is zero-overhead when unused: the object storage remains zend_ffi_cdata with shared handlers, only the ce pointer differs, so GC, clone, field access and lifetime behave byte-for-byte as today. The only relaxation is that FFI\CData becomes extendable for registered classes. Fully opt-in, no BC impact. I'm targeting PHP 8.6 ahead of feature freeze and volunteering to write the implementation PR - z-engine already emulates these exact semantics in userland, so there's a strong real-world test bed for it. The main open design point I'd like feedback on is the typemap callback contract (from_cdata/to_cdata, and when they fire) versus shipping classmap alone in the first iteration. If the feedback here is positive, I'll write this up as a formal RFC on the wiki. Regards, Alexander Lisachenko

« previous php.internals (#132203) next »