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

php.internals

Alexander Lissachenko

20 days ago
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

Bob Weinand

14 days ago
Hey Alexander, On 12.8.2026 15:39:35, Alexander Lisachenko wrote:
> 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 >
I've replied to you on the github issue outlining some thoughts. But yeah, don't rush this, write a RFC, and check what actually feels good to use and read. But yeah, as noted on the issue, I don't think the typemap really solves issues I had in my prior FFI usage. Thanks, Bob