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

[email protected] (Alexander Lisachenko)
Newsgroups php.internals
Message-ID <CAEbg-4Y-=neNdXXDXZ2iSqCb_dyx0ebi4LDaXyj=82+=x_41ow@mail.gmail.com>
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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.