Repository navigation
feat(ext): add FFI bridge with php-src corpus parity - #2233
Open
Guikingone wants to merge 16 commits into
Open
Guikingone wants to merge 16 commits into
Guikingone wants to merge 16 commits into
Conversation
FFI bridge with php-src corpus parity
Guikingone
force-pushed
the
feat/ffi-php-src-compliance
branch
from
November 3, 2026 13:24
998fd46 to
062931e
Compare
FFI bridge with php-src corpus parityFFI bridge with php-src corpus parity
- 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
force-pushed
the
feat/ffi-php-src-compliance
branch
from
November 3, 2026 21:28
d9f0aef to
e25e190
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds PHP's
ext/ffisurface 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-ffiis passed.The branch also commits the php-src
ext/ffifixture 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.rsis#[cfg(test)]transcription/oracle only; production uses the generatedbuild.rsand never parses PHP text.--with-ffi(including forced prelude/staticlib reachability).tests/php_src/ffi/, the generated test cases, andscripts/gen_php_src_ffi_cases.py.docs/php/ffi.mdandexamples/ffi-callbacks/.PHP-visible parity
class_implements(FFI\CData::class)andget_class_methods(FFI\CData::class)are empty; any CData method call raises php-src's catchableError: Object of type FFI\CData does not support method callsat run time;instanceof ArrayAccess/Countable/Iterator/Traversable/Stringableis false;class_implements(FFI::class)andclass_implements(FFI\CType::class)are empty; only php-src's public method lists are exposed forFFIandFFI\CType. Array access,count()andforeachkeep working through the compiler's internal dispatch.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 explicitFFI::free()still releases it.TypeError: Unsupported operand types ...; only pointer/array CData supports+/-, and pointer difference returnsint.Comparison of incompatible C types.(int)/(float)casts warn and yield1;(string)reads a scalar payload and raises php-src's engine error for pointer/aggregate CData;(bool)is true.Traversable:is_iterable()andReflectionClass::isIterable()are false, anditerator_to_array()raises itsTraversable|arrayTypeError. Array access,count()andforeachstill work through internal dispatch.__getproperties such asFFIglobals staymixedinside array literals, so__callargument arrays no longer cast CData to int.foreachover scalar C arrays yields PHP scalars; by-reference loops keep writable CData views.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 amixedslot; the generic string/closure/array path is unchanged.Variadic function closures are not supported.Error: Object of type FFI\CData does not support method callsat 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=0disables every guarded FFI API entry point with php-src'sFFI API is restricted by "ffi.enable" configuration directive;preloadstays CLI-only and1/Onenables 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.++/--uses PHP string/number increment semantics, which fixes$charCData->cdata++.Validation
codegen::ffi::: 93 passed, 3 ignored (external SDL)elephc-ffiunit tests: 10 passedffi_preludeparity tests: 6 passeddebug_info_dumps: 17;mixed_receiver_property_writes: 19--with-ffiand runtime-cache feature-key tests: greenelephc_ffireferences)cargo buildwarning-free;git diff --checkcleanNo
CHANGELOG.mdedit and no Cargo package version bump.