integrations/haskell/notes/001-garnet.md
2026-03-24 13:33:16 +01:00

5.1 KiB

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.