[FFI] Opt-in mapping of C struct types to userland PHP classes (typed CData handles)
Les mainteneurs répondent en général sous 1 jour
Personne n'a encore pris cette issue.
Évaluation
- Difficulté
- 5/5
- Temps estimé
- Plus d'une semaine
- Accessibilité débutants
- 38/100
Piste de recherche
Commencez dans ext/ffi en lisant zend_ffi_cdata_to_zval() et les handlers de FFI::new()/FFI::cast(), puis suivez la création des scopes via cdef/load et la matérialisation du preload par requête. L’implémentation nécessiterait une conception convenue de classmap/typemap, des instances de CData mappées et un comportement inchangé pour les scopes sans options.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Description
Description
Feature request
PHP FFI represents every C value — a struct zend_string*, a zval*, a
char*, an int — as one and the same final class, FFI\CData. That single
opaque type is what makes FFI so flexible, but it also means no C struct a
binding works with can ever be described to static analysis or an IDE. There
is no way to say "this handle is a zend_string, these are its fields", and no
way to make $handle instanceof ZendString true. FFI\CData being final
closes off every userland workaround.
This proposes an opt-in, per-scope class map: when you create an FFI scope
you may declare that C type X should be represented as instances of your class
\My\X (class extending FFI\CData), so that FFI::new('X'),
FFI::cast('X', …), struct-field reads and function returns all produce
\My\X instances. Nothing changes for anyone who does not ask for it.
Motivation — a concrete, load-bearing case study
z-engine drives the Zend Engine's own
internals through FFI. It dereferences dozens of engine structs —
zend_string, zend_function, zend_class_entry, zval, zend_op_array, …
— and every one of them is FFI\CData. To recover any static typing and IDE
autocompletion the project currently has to ship all of the following:
- a code generator that slices each struct out of the PHP headers via clang and
emits one analysis-only PHP stub class per struct (with@property/typed
properties mirroring the C fields), https://github.com/lisachenko/z-engine/blob/8.4/stubs/zend-engine-structs.php - a
.phpstorm.meta.phpmap so PhpStorm resolves the FFI entry points, https://github.com/lisachenko/z-engine/blob/8.4/.phpstorm.meta.php - a PHPStan dynamic-return extension so the analyser resolves them too, https://github.com/lisachenko/z-engine/blob/8.4/tools/phpstan/TypedEntryPointReturnExtension.php
- a hand-maintained convention that every one of those stub classes is
never loaded at runtime (they exist only for the analyser), because they
cannot actually back theCDatahandles.
That is four moving parts, per project, to emulate one feature the runtime
could provide directly - and it is strictly weaker than the real thing: the
stub classes can never make instanceof work, can never enforce a parameter
type, and drift from the real ABI unless regenerated. Every FFI binding
generator (SWIG-style wrappers, FFIMe, hand-written bindings over libgit2,
libsodium, SDL, …) hits the same wall. A native class map solves it once, for
everyone, in ~the same amount of C code these projects spend working around it.
Proposal
An optional class map attached to an FFI scope, mapping C struct/union type
names to userland classes:
The scope takes an optional array $options configuration, in the spirit of
SoapServer/SoapClient (which accept a classmap, and SoapClient also a
typemap). Two keys are recognised — classmap (C type → userland class) and
typemap (C type → conversion callbacks):
$ffi = FFI::cdef($cCode, $lib, options: [
'classmap' => [
'zend_string' => \My\Engine\ZendString::class,
'zend_value' => \My\Engine\ZendValue::class,
],
'typemap' => [
// C type name => how to marshal it to/from PHP (for types that should
// surface as something other than a raw CData handle)
'zend_bool' => [
'from_cdata' => fn(FFI\CData $c): bool => $c->cdata !== 0,
'to_cdata' => fn(bool $v, FFI\CData $c): void => $c->cdata = $v ? 1 : 0,
],
],
]);
final class ZendString extends \FFI\CData
{
// Fields may be exposed as typed property hooks over the raw CData, and the
// class may carry ordinary methods.
public int $len { get => $this->readUint32('len'); }
public function toPhpString(): string { /* ... */ }
}
Rules for a classmap class:
- it must extend
FFI\CData, - it may declare typed property hooks whose bodies read/write the underlying
C fields through the raw CData, and it may declare methods ; the object's storage stays ext/ffi's
zend_ffi_cdata, so a hook body operates on the raw structure rather than on a
real backing store.
Given the map, every handle ext/ffi mints for a mapped C type — from
FFI::new(), FFI::cast(), FFI::addr(), a struct-field read that yields a
nested struct/pointer, or a function return value — is created as an instance of
the mapped class instead of the bare FFI\CData. get_class() is truthful,
instanceof works, and native parameter/return type declarations
(function f(ZendString $s)) are enforced by the engine. Field access, casting,
FFI::sizeof(), garbage collection and every other behaviour are byte-for-byte
identical to today — the object still is a zend_ffi_cdata, only its ce
differs.
Implementation sketch
The change is localized to ext/ffi and is zero-overhead when unused:
- Registry. Each
zend_ffiscope gains aHashTable *class_mapkeyed on the
resolvedzend_ffi_type *(populated fromoptions['classmap']atcdef/load
time by resolving each declared type name to itszend_ffi_type, and validating
the target class extendszend_ffi_cdata_ce), plus an optional parallel
typemaptable of conversion callbacks. Both areNULL/empty for every
existing user. - Minting. Today every cdata is created with
object_init_ex(&zv, zend_ffi_cdata_ce)(inzend_ffi_cdata_to_zval()and
theFFI::new/FFI::castmethod handlers). Wrap that single choice: when the
active scope'sclass_mapis non-empty, look up the value's
zend_ffi_type *, and if a class is registered use it instead of
zend_ffi_cdata_ce. One hash lookup, guarded byclass_map != NULL, so the
common path is unchanged. - Layout & lifetime. The allocated object stays
zend_ffi_cdata; only the
std.cepointer changes. Allzend_ffi_cdata_handlersare shared, so GC,
free, clone, and the read/write paths need no changes — this is what keeps the
patch small and safe. - Preloading. For
opcache.preloaded scopes the map must be re-resolved per
request (thezend_ffi_type *pointers are request/persistent-scoped); the
natural place is alongside the existing per-request scope materialization. - Struct classes carrying methods / property hooks. Because the mapped class
is an ordinaryce(only the object storage iszend_ffi_cdata), methods and
typed property hooks work with no extra machinery — a hook body just reads or
writes the underlying C field through the raw CData. - Unchanged: serialization stays forbidden (as for any cdata).
Backward compatibility
Fully opt-in and additive. No existing FFI program changes behaviour; the new
options array (with its classmap/typemap keys) is the only surface, and it
defaults to "no mapping". The only relaxation is that FFI\CData becomes
extendable for registered classes only — a normal class X extends FFI\CData
without registration can stay an error (or be allowed as an inert never-minted
class, whichever the RFC prefers).
Target & offer
I'd like to target PHP 8.6, ahead of feature freeze, and I'm volunteering to
write the implementation PR. I'd welcome feedback on the proposal
- Langage dominant
- C
- Étoiles
- 40.4k
- Forks
- 8.2k
- Merge moyen
- 2 j 3 h
- PR mergées (30 j)
- 151
Préparer son environnement
- Aucun Dockerfile ni fichier Docker Compose
- Aucun modèle de pull request
- Lire le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de php/php-src
-
Bug Status: Needs Triage
Difficulté 2/5 1-3 heures Accessibilité débutants 74/100
php/php-src#24121 · 1 commentaire ·
Les mainteneurs répondent en général sous 1 jour
-
Variant analysis: 1 unfixed sibling safety gap in php-srcPeut-être pris @kamil-tekiela l’a pris il y a 6 jours. Ouverte
Difficulté 2/5 1-3 heures Accessibilité débutants 72/100
php/php-src#23958 · 1 personne assignée ·
Les mainteneurs répondent en général sous 1 jour
-
sapi_lsapi_ub_write does not return bytes written in lsapi modePeut-être pris Une pull request liée à cette issue est ouverte ou déjà fusionnée. OuverteBug Status: Needs Triage
Difficulté 1/5 Moins d'une heure Accessibilité débutants 90/100
Les mainteneurs répondent en général sous 1 jour
-
Bug Status: Needs Triage
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
Les mainteneurs répondent en général sous 1 jour
-
Flaky hrtime.phpt testPeut-être pris @veksa l’a pris il y a 62 jours. OuverteBug Category: Tests Status: Verified
Difficulté 2/5 1-3 heures Accessibilité débutants 68/100
Les mainteneurs répondent en général sous 1 jour
Toutes les issues de php/php-src
Issues similaires
-
Difficulté 2/5 1-3 heures Accessibilité débutants 72/100
libsdl-org/SDL#16444 ·
Les mainteneurs répondent en général sous 1 jour
-
bug Component component: net
Difficulté 1/5 Moins d'une heure Accessibilité débutants 90/100
RT-Thread/rt-thread#11852 · 1 commentaire ·
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 68/100
MiSTer-devel/ao486_MiSTer#243 ·
-
Dropped last row with parallel scan of attached SQLite tables if the rowid range is a multiple of 122,880Peut-être pris @staticlibs l’a pris aujourd’hui. Ouverte
Difficulté 2/5 1-3 heures Accessibilité débutants 82/100
duckdb/duckdb-sqlite#240 · 1 commentaire ·
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 66/100
siderolabs/pkgs#1710 ·
Les mainteneurs répondent en général sous 1 jour