Skip to content

feat(ext): add FFI bridge with php-src corpus parity - #2233

Open
Guikingone wants to merge 16 commits into
illegalstudio:mainfrom
Guikingone:feat/ffi-php-src-compliance
Open

Guikingone wants to merge 16 commits into
illegalstudio:mainfrom
Guikingone:feat/ffi-php-src-compliance

Conversation

@Guikingone

@Guikingone Guikingone commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds PHP's ext/ffi surface to elephc as a Rust bridge over bundled libffi, plus an AST-built prelude (FFI, FFI\CData, FFI\CType, FFI\Exception, FFI\ParserException) that is injected only when FFI is referenced or --with-ffi is passed.

The branch also commits the php-src ext/ffi fixture corpus (77 cases) and a harness that compiles/runs each case with its --INI--, --ENV--, preload and asset setup. The corpus is compiled with the PHP 8.3 profile matching its provenance.

What is included

  • crates/elephc-ffi: C declaration parser, C type layout, libffi calls, CData/CType context, memory helpers, function-pointer callbacks, ownership handling, and a feature-gated php-src fixture shim module.
  • src/ffi_prelude*: synthetic-AST prelude and detection. fragments.rs is #[cfg(test)] transcription/oracle only; production uses the generated build.rs and never parses PHP text.
  • Pipeline/linker wiring for auto-detection and --with-ffi (including forced prelude/staticlib reachability).
  • 77 committed php-src fixtures under tests/php_src/ffi/, the generated test cases, and scripts/gen_php_src_ffi_cases.py.
  • docs/php/ffi.md and examples/ffi-callbacks/.

PHP-visible parity

  • class_implements(FFI\CData::class) and get_class_methods(FFI\CData::class) are empty; any CData method call raises php-src's catchable Error: Object of type FFI\CData does not support method calls at run time; instanceof ArrayAccess/Countable/Iterator/Traversable/Stringable is false; class_implements(FFI::class) and class_implements(FFI\CType::class) are empty; only php-src's public method lists are exposed for FFI and FFI\CType. Array access, count() and foreach keep working through the compiler's internal dispatch.
  • PHP 8.5+ emits Deprecated: Calling FFI::new() statically is deprecated; earlier profiles stay silent. The committed 8.3-era corpus compiles with the matching profile.
  • FFI::new(..., persistent: true) marks the allocation persistent: the CData destructor does not release it, matching php-src's persistent-allocator lifetime, while an explicit FFI::free() still releases it.
  • Scalar CData arithmetic raises php-src's TypeError: Unsupported operand types ...; only pointer/array CData supports +/-, and pointer difference returns int.
  • CData comparisons match the internal handler: pointer/pointer by address, null pointers throw, and every non-pointer/incompatible combination raises Comparison of incompatible C types.
  • (int)/(float) casts warn and yield 1; (string) reads a scalar payload and raises php-src's engine error for pointer/aggregate CData; (bool) is true.
  • CData is not Traversable: is_iterable() and ReflectionClass::isIterable() are false, and iterator_to_array() raises its Traversable|array TypeError. Array access, count() and foreach still work through internal dispatch.
  • Magic __get properties such as FFI globals stay mixed inside array literals, so __call argument arrays no longer cast CData to int.
  • foreach over scalar C arrays yields PHP scalars; by-reference loops keep writable CData views.
  • String-to-char/char[N] assignment follows php-src diagnostics, with numeric-prefix coercion for numeric fields.
  • is_callable() on CData uses the bridge's pointer-to-function check, including when the value is boxed in a mixed slot; the generic string/closure/array path is unchanged.
  • Callback aggregate arguments are owned copies instead of libffi stack views.
  • Struct/array callback returns by value are supported through a staged libffi aggregate dispatcher; variadic function pointers reject PHP closures with php-src's Variadic function closures are not supported.
  • PHP closures, function names, and array callables can be passed directly to a C function-pointer parameter (qsort comparators and similar); the call builder prepares a temporary libffi callback.
  • CData method calls now raise php-src's catchable Error: Object of type FFI\CData does not support method calls at run time (including dynamic and non-null nullsafe forms) instead of failing at compile time; is_callable([$cdata, "method"]) throws the same error.
  • --ini ffi.enable=0 disables every guarded FFI API entry point with php-src's FFI API is restricted by "ffi.enable" configuration directive; preload stays CLI-only and 1/On enables both CLI and web builds.
  • FFI::addr(<temporary>) transfers ownership; reads from pointer fields/elements keep independent copies; FFI::free($array[$i]) still zeroes the originating element.
  • Property ++/-- uses PHP string/number increment semantics, which fixes $charCData->cdata++.

Validation

  • 77/77 php-src FFI fixtures
  • codegen::ffi::: 93 passed, 3 ignored (external SDL)
  • elephc-ffi unit tests: 10 passed
  • ffi_prelude parity tests: 6 passed
  • debug_info_dumps: 17; mixed_receiver_property_writes: 19
  • SPL introspection/interfaces and iterator suites: green
  • incdec/property operator and optimizer suites: green
  • CLI --with-ffi and runtime-cache feature-key tests: green
  • XML example and the iOS XML staticlib link: green (the FFI prelude is no longer injected by prelude-internal probes, so a non-FFI staticlib has no elephc_ffi references)
  • Builtins/compatibility docs drift gate: green
  • cargo build warning-free; git diff --check clean

No CHANGELOG.md edit and no Cargo package version bump.

@Guikingone Guikingone added the ci:macos Run the macOS test matrix and iOS links on this PR, not only in the merge queue. label Nov 3, 2026
@github-actions github-actions Bot added area:eir Touches EIR definitions, lowering, validation, or passes. area:runtime Touches runtime helpers, GC, ownership, or bridge runtimes. area:types Touches type checking, inference, or compatibility. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:xl Very large pull request that needs deliberate review planning. target:windows-x86_64 Contains behavior specific to the Windows x86_64 target. type:feature Introduces new user-visible behavior or capabilities. labels Nov 3, 2026
@Guikingone Guikingone changed the title feat: add PHP FFI bridge with php-src corpus parity feat(ex)t: add FFI bridge with php-src corpus parity Nov 3, 2026
@Guikingone Guikingone self-assigned this Nov 3, 2026
@Guikingone
Guikingone force-pushed the feat/ffi-php-src-compliance branch from 998fd46 to 062931e Compare November 3, 2026 13:24
@github-actions github-actions Bot added area:codegen Touches target-aware assembly or backend lowering. and removed area:types Touches type checking, inference, or compatibility. labels Nov 3, 2026
@Guikingone Guikingone changed the title feat(ex)t: add FFI bridge with php-src corpus parity feat(ext): add FFI bridge with php-src corpus parity Nov 3, 2026
- add the elephc-ffi staticlib bridge for libffi calls, CData/CType values,
  callbacks, memory helpers, and value ownership
- build the FFI/FFI\CData/FFI\CType prelude as synthetic AST and wire
  auto-detection plus --with-ffi into the pipeline and linker
- add the committed php-src ext/ffi corpus harness (77 fixtures) and examples
- align CData arithmetic, foreach values, string-to-char assignment,
  is_callable, and callback aggregate ownership with php-src
- gate php-src test-extension symbols behind a test-only bridge feature
- document the FFI surface and its compatibility decisions
- hide FFI CData's internal ArrayAccess/Countable/Iterator/Stringable
  interfaces from instanceof, class_implements and reflection while keeping
  the compiler's internal dispatch
- publish only php-src's method lists for FFI, FFI\CData and FFI\CType
- emit the PHP 8.5 static FFI::new() deprecation for 8.5+ only, and compile
  the committed 8.3-era corpus with the matching profile
- mark FFI::new(..., persistent: true) allocations so the CData destructor
  does not release them, matching php-src's allocator lifetime
- cover the reflection surface, deprecation gate and persistent allocation
  with focused tests
A CData boxed in mixed no longer falls through to the generic public-__invoke
probe. The lowering branches on the runtime FFI\CData class, calls the bridge's
declared-type predicate for CData, and keeps the generic runtime lookup for
strings, closures and arrays. Covers scalar CData (not callable), function
pointers (callable) and the non-CData fallback.
- compare CData like php-src: pointer/pointer by address, null pointers throw,
  and every incompatible/non-pointer combination raises FFI\Exception
- cast scalar CData to string/int/float like php-src, and reject pointer or
  aggregate string casts with the engine error
- warn and return 1 for object-to-int/float casts, including boxed CData
- make CData non-Traversable for is_iterable/ReflectionClass and make static
  iterator_to_array raise the Traversable|array TypeError
- keep magic __get properties such as FFI globals typed as mixed inside array
  literals, fixing the __call argument array cast
- hide CData's internal methods from the runtime callable table
- cover comparison, cast, iterable and callable surfaces with focused tests
php-src's CData exposes no PHP methods; every user method call raises
Error: Object of type FFI\CData does not support method calls. The checker now
rejects public internal adapters with that message while leaving the prelude's
Internal-mode dispatch and private-method visibility diagnostics untouched.
- add an aggregate callback dispatcher: PHP returns a CData, the bridge stages
  its bytes, and the libffi trampoline copies them into the C return buffer
- keep the staged aggregate alive until the trampoline consumes it, so the
  dispatcher's CData release cannot free the struct first
- reject variadic function pointers with php-src's exact
  'Variadic function closures are not supported' error and drop the PHP-only
  closure fallback so allocation/preparation failures throw like php-src
- add a test-only struct-returning callback fixture and regression tests for
  aggregate returns and variadic rejection
The FFI prelude is injected for introspection and opaque eval, but the bridge
was only linked when the main checker saw an extern call. Push elephc_ffi into
the required libraries whenever the prelude was injected, and mirror that in the
codegen harness and the LFC eval link requirements. Regenerate the symbol
registry for the new FFI classes.
Bake php-src's ffi.enable tri-state into the generated FFI surface:
preload keeps the CLI enabled and web disabled, boolean/atoi spellings
override it, and every guarded API entry point throws the exact
'FFI API is restricted by "ffi.enable" configuration directive'
exception. Add unit coverage for the directive parsing and an end-to-end
regression for --ini ffi.enable=0.
Prepare a temporary libffi callback when a PHP closure, function name, or
array callable is passed directly to a C function-pointer parameter, matching
php-src's qsort/signal/pthread pattern. Gate callback conversion on the
declared parameter type so a string for char* stays a string, and preserve
php-src's invalid-callback diagnostics for non-callable values.
- keep CData method calls type-checkable and lower them to the catchable
  Error php-src throws, including dynamic and non-null nullsafe forms
- let the synthetic __invoke/__elephc_invoke_array and foreach iterator
  helpers bypass the user-facing guard so CData invocation and by-ref
  iteration keep working
- make is_callable([$cdata, "method"]) throw for statically typed and mixed
  receivers instead of returning false
- update the destructor regression and add coverage for call timing,
  argument skipping, nullsafe dispatch, and is_callable arrays
Branch on the runtime class before the ordinary dynamic callable-array
dispatch so a mixed value holding FFI\CData raises php-src's catchable
method-call Error instead of the generic unresolved-callable fatal.
Evaluate both array elements before raising the CData method-call Error, and
use the checker's function return types so a function returning CData or mixed
takes the FFI-aware is_callable path. Add ordering coverage.
Prefix-injected preludes contain dynamic calls and probes of their own. Scanning
them made XML/DOM programs look like they introspect the symbol table, which
injected the FFI prelude and its bridge references into programs that never use
FFI. Scan only non-internal statements so a user's eval/reflection still opts in,
and cover both directions in the prelude tests.
Bring the current comparison generator and catalog from main, then regenerate
both compatibility outputs with the FFI classes included (5/5 classes, 190/329
overall). This keeps the docs drift gate green on the merge commit.
@Guikingone
Guikingone force-pushed the feat/ffi-php-src-compliance branch from d9f0aef to e25e190 Compare November 3, 2026 21:28
Opaque eval fixtures inject the FFI prelude, so their codegen shards need
libelephc_ffi.a. Building it in the online bridge step fetches libffi's
bitflags dependency into the offline test cache and keeps the per-fixture
rebuild from failing closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:codegen Touches target-aware assembly or backend lowering. area:eir Touches EIR definitions, lowering, validation, or passes. area:runtime Touches runtime helpers, GC, ownership, or bridge runtimes. ci:macos Run the macOS test matrix and iOS links on this PR, not only in the merge queue. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:xl Very large pull request that needs deliberate review planning. target:windows-x86_64 Contains behavior specific to the Windows x86_64 target. type:feature Introduces new user-visible behavior or capabilities.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant