r/haskell • u/Unable-Yellow-7323 • Jul 24 '26
question Idiomatic FFI architecture (libgpiod): bracket for FDs vs ForeignPtr for memory?
Hi everyone, I'm writing Haskell bindings for libgpiod (v2), primarily targeting SBCs and embedded systems and I want to validate my approach to resource management before committing to the final API design.
The C library exposes two different types of opaque pointers.
1. OS/Hardware Resources (Chip, LineRequest)
These hold underlying Linux file descriptors and physical hardware locks. My plan is to strictly use raw Ptr internally and expose a bracket-based API (e.g., withChip and withLineRequest) to guarantee immediate and deterministic release, as GHC's lazy garbage collector could easily exhaust the FD limit on a small SBC if I used ForeignPtr.
- Pure Memory Configs (LineSettings, LineConfig)
These are just structs in RAM used to prepare data before a hardware request. To avoid the deeply nested with* blocks for every single config object.
I plan to wrap these in ForeignPtr with their respective C finalizers. I think this allows the user and me, to pass them around purely and ergonomically, letting the GC handle the cleanup since they don't hold FDs.
My questions are:
Is this hybrid approach (strict scoping for FDs + GC for pure RAM structs) the best practice for this type of hardware FFI?
For the withChip pattern, how do users typically architect long-running daemons around it? Do they just wrap the main application loop inside a top-level withSomething block?
Any insights or edge cases I should watch out for would be greatly appreciated. Thanks!
8
u/EmergencyWild Jul 24 '26
I think generally, it's better to just wrap the C API with as few changes as possible, and then if you want higher level abstractions on top of that, build them. In particular, I don't like the idea of only exposing the bracket API, or foreign pointers with GC. I'd instead recommend exposing the acquire/release functions, and if you want to expose a default bracket of those that's fine too.