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