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

- Thread: https://externals.io/message/132203
- Original archive: https://news-web.php.net/php.internals/132203
- Started by Alexander Lisachenko on 2026-08-12T13:39:35+00:00
- Messages: 2
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #132203 — Alexander Lisachenko — 2026-08-12T13:39:35+00:00

URL: https://externals.io/message/132203#132203

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

---

## #132246 — Bob Weinand — 2026-08-18T03:00:52+00:00

URL: https://externals.io/message/132203#132246 · In reply to: #132203

Hey Alexander,


> 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
