Haskell Interop Demo

This directory contains a small Haskell project for the repository's Rust/Haskell integration work.

The project demonstrates both directions:

  • Haskell calling into Rust through a C ABI exposed by the Rust crate.
  • Rust calling back into Haskell through a Cabal foreign-library, with explicit GHC RTS initialization.

Layout

  • src/Interop/Shared.hs - pure shared logic plus the C-compatible struct layout used at the boundary.
  • src/Interop/Exports.hs - Haskell functions exported to C for Rust to call.
  • app/RustClient.hs - Haskell imports for the Rust C ABI.
  • app/Main.hs - Haskell executable that demonstrates the Haskell -> Rust path.
  • test/Spec.hs - small pure tests that avoid crossing the FFI boundary.

The code keeps the FFI surface small on purpose. The boundary uses:

  • integers
  • a fixed C-shaped struct
  • borrowed slices as ptr + len
  • owned returned buffers as ptr + len + cap
  • owned C strings with an explicit free function on each side

That is enough to demonstrate the main challenges without pulling in code generation or large bindings.

The current examples include:

  • scalar stats and message passing
  • borrowed i32 slices sent across the boundary
  • owned i32 buffers returned across the boundary
  • borrowed raw byte buffers with embedded zero bytes
  • owned raw byte buffers returned across the boundary

Build And Run

From the repository root:

make haskell-build

Run the Haskell executable that calls Rust:

make haskell-run

Run the Rust executable that calls the Haskell foreign library:

make rust-calls-haskell

You can also run the commands manually:

cargo build --lib
cabal build --project-file=haskell/cabal.project all
cabal run --project-file=haskell/cabal.project haskell-calls-rust -- Ada 7 5
cargo run -- rust-calls-haskell Ada 7 5

What This Demonstrates

  • The boundary must stay C-shaped. Rich Rust and Haskell types do not cross directly.
  • Strings need explicit ownership rules. Each side exports its own free function.
  • Borrowed slices and owned returned buffers are different patterns and must be modeled differently.
  • Raw byte buffers should be treated separately from C strings.
  • Struct layout must be mirrored carefully on both sides.
  • Rust calling Haskell is the harder direction because it must initialize and shut down the GHC runtime correctly.
  • Build order is part of the design. Haskell links against the Rust static library, and Rust loads the Haskell foreign library after Cabal builds it.