Refactor FFI boundary #221
Description
Is your change request related to a problem? Please describe.
When writing FFI bindings for PHP (such as in phpurs-aff), there's currently a pattern inherited from the JavaScript ecosystem that consists of passing a PureScript Record (like ffiUtil) to FFI functions to provide access to PureScript constructors or utility functions.
It's a bit frustrating to have to guess how the phpurs compiler translates these Records internally. In JavaScript, this works flawlessly because Records strictly compile to native JS Objects. However, in PHP, this creates a severe Impedance Mismatch and tight coupling with the compiler.
Currently, the PHP FFI code (e.g., in Effect/Aff.php) is forced to hardcode internal compiler behaviors:
- Data Structure: The FFI assumes the Record is compiled as a
stdClassobject, requiring property access syntax ($ffiUtil->...) rather than an associative array ($ffiUtil['...']). - Name Mangling: Because PHP properties are case-sensitive and the
phpurscompiler applies asafeNamemangling algorithm universally, a field likeisLeftis compiled toismajLeft. The FFI is forced to hardcode this compiler-specific mangling:($ffiUtil->ismajLeft)($res). - Closure Invocation: PHP requires wrapping property closures in parentheses
($obj->func)()which is an awkward syntax to maintain.
If the compiler changes its internal representation of Records or its mangling algorithm tomorrow, the entire FFI layer will silently break.
Describe the solution you'd like
To make the PHP FFI truly robust and agnostic to the compiler's internal choices, we should avoid passing Records across the FFI boundary entirely. Instead, we should flatten the dependencies using Fn.FnX from Data.Function.Uncurried.
Current approach (Fragile & Coupled):
foreign import _makeAff :: forall a. Fn.Fn2 FFIUtil ((Either Error a -> Effect Unit) -> Effect Canceler) (Aff a)$_makeAff = function($ffiUtil, $k) {
// Highly coupled to phpurs internals: we also could have $ffiUtil['isLeft']
// We have been forced to guess what the compiler does!
if (($ffiUtil->isLeft)($res)) { ... }
} Proposed approach (Robust & Decoupled):
foreign import _makeAff :: forall a. Fn.Fn6
(Either a b -> Boolean) -- isLeft
(Either a b -> a) -- fromLeft
(Either a b -> b) -- fromRight
(a -> Either a b) -- left
(b -> Either a b) -- right
((Either Error a -> Effect Unit) -> Effect Canceler) -- k
(Aff a)$_makeAff = function($isLeft, $fromLeft, $fromRight, $left, $right, $k) {
// 100% decoupled, clean PHP syntax
if ($isLeft($res)) {
$exception = $fromLeft($res);
}
}By injecting the exact callbacks as individual arguments, the FFI no longer needs to know how the backend compiles Records, arrays, or how it mangles case-sensitive strings.
Additional context This architectural change is essential for ensuring that FFI bindings remain resilient across any potential future upgrades or optimizations within the phpurs compiler core. It sets a standard best practice for all PHP-targeted PureScript repositories (avoiding stdClass property access in FFI entirely).
Thank you! And do not hesitate to ask for a PR.
Activity
chore(phpurs): add TODO comment linking to upstream issue purescript-…
natefaubion commentedon Jul 7, 2026
I would wager Aff is not the only offender here (have fun with purescript-record's FFI). Part of PureScript's compilation strategy has always been that records are FFI stable, for better or worse. We've only ever recommended against passing data constructors across FFI boundaries. FWIW, in general, I agree with you, and I think PureScript's general laxness (both compiler and ecosystem) around records is a problem for alternative backends. But I'd wager that's something difficult to come back from collectively.
The usual approach here is to fork the library into a backend-specific package-set.
0x000000000000000000001 commentedon Jul 7, 2026
Okay, I understand. I'll do that. Thanks for the quick reply.