integrations/haskell/notes/001-garnet.md

154 lines
5.1 KiB
Markdown
Raw Normal View History

2026-03-24 09:50:06 +01:00
# Garnet findings
Date: 2026-03-24
Path inspected: `./tmp/garnet`
## Plain-English summary
`./tmp/garnet` is a small experiment for calling Rust from Haskell.
The flow is:
1. Define Rust functions and data types.
2. Export them as a C ABI.
3. Generate a C header with `cbindgen`.
4. Generate Haskell-side low-level bindings with `hs-bindgen`.
5. Add hand-written Haskell wrappers on top.
6. Run a demo executable to show the setup works.
This looks like a prototype or reference repo, not a finished product.
## What it is
This is a mixed Haskell/Rust FFI demo.
It contains:
- a Cabal package named `garnet`
- a Rust static library in `./tmp/garnet/rust`
- Haskell modules for raw bindings and higher-level wrappers
- a small executable that exercises the API
- a shell build script
- a Nix flake for the development environment
## What it is trying to prove
The repo appears to be testing whether this toolchain is practical:
Rust -> `cbindgen` -> C header -> `hs-bindgen` -> Haskell wrappers
It also tests whether the approach can handle more than trivial examples, including:
- C strings
- plain structs
- enums
- simple arithmetic
- recursive tree-shaped data
## Key files
- `./tmp/garnet/rust/lib.rs`
- Rust definitions for exported FFI functions and example types
- `./tmp/garnet/rust/build.rs`
- generates `garnet_rs.h` and patches it for compatibility with `hs-bindgen`
- `./tmp/garnet/lib/GarnetRs/Raw.hs`
- drives low-level binding generation from the generated header
- `./tmp/garnet/lib/GarnetRs/Wrapped.hs`
- adds Haskell-friendly wrapper types and conversion code
- `./tmp/garnet/exe/Main.hs`
- demo executable using the wrapper API
- `./tmp/garnet/build`
- custom build script for Rust + Cabal coordination
## Easy reading of the design
### Rust side
The Rust library exports a handful of example FFI functions:
- `hello`
- `hello_struct`
- `hello_shape`
- `add`
- `sum_tree`
These are not a product API. They are examples chosen to stress different interop cases.
### Haskell side
The Haskell code is split into two layers:
- `GarnetRs.Raw`
- low-level generated bindings
- `GarnetRs.Wrapped`
- nicer Haskell-facing types and functions
This separation is a good design choice. It keeps generated code concerns away from the public API.
## Critical assessment
This repo is technically promising, but clearly unfinished.
What looks good:
- It demonstrates the full end-to-end workflow.
- It goes beyond the easiest possible FFI examples.
- It keeps raw and wrapped APIs separate.
- It includes a dev shell, which improves reproducibility.
What looks weak:
- The build flow is manual and somewhat brittle.
- The project depends on pinned git sources and alpha-stage tooling.
- The Rust build script contains a workaround for upstream tooling issues.
- The Haskell wrapper layer still requires manual boilerplate.
- There is no obvious sign of tests, CI, or a stability story.
## Pros of the approach
- **Clear architecture**: raw bindings and ergonomic wrappers are separated well.
- **Good exploration value**: useful for learning the actual friction points in Rust/Haskell interop.
- **Covers realistic cases**: includes enums and recursive data, not just simple integers.
- **Small and understandable**: the repo is compact enough to inspect quickly.
- **Useful as a reference**: someone exploring `hs-bindgen` could learn from it.
## Cons of the approach
- **Custom build glue**: the shell script and symlink step suggest the toolchain is not yet smooth.
- **Dependency fragility**: pinned git dependencies increase maintenance burden.
- **Tooling immaturity**: the repo already needs local workarounds for upstream issues.
- **Manual wrapper overhead**: generated bindings do not remove the need for hand-written API cleanup.
- **Potential safety complexity**: FFI, pointer conversions, and `unsafe` code are easy to get wrong.
- **Weak production story**: this setup does not yet look ready for long-term team use.
## Status of the work
My assessment of current status:
- **Stage**: proof of concept / exploration
- **Scope**: enough code exists to show the full path works
- **Maturity**: decent as an experiment, weak as a reusable foundation
- **Stability**: uncertain because of upstream pins and workarounds
- **Readiness**: not production-ready
In simple terms: this looks like “we got it working” rather than “we finished the system.”
## Likely next problems if this grows
If someone tried to turn this into a serious integration layer, the next hard problems would likely be:
- making the build process less custom
- reducing wrapper boilerplate
- keeping generated headers and bindings in sync
- handling more complex Rust types safely
- upgrading upstream tooling without breakage
- adding tests that catch FFI regressions early
## Bottom line
`./tmp/garnet` is a useful and interesting interop prototype.
It succeeds as a demonstration that Rust -> C header -> Haskell bindings -> Haskell wrapper can work. But it also shows the current cost of that path: manual build glue, unstable dependencies, wrapper code, and reliance on tool-specific workarounds.
Overall: good experiment, useful reference, not mature infrastructure yet.