{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreia6jczufmvrcsp5ntbvc3nzggax7g6xy4uwfdfjrkvg3gl6nsg4c4",
"uri": "at://did:plc:pi6woz4d47bkuws673w2il2r/app.bsky.feed.post/3mm6ypgdi4r62"
},
"path": "/t/ann-hsrs-ergonomic-haskell-bindings-for-rust/14129#post_1",
"publishedAt": "2026-05-19T03:32:36.000Z",
"site": "https://discourse.haskell.org",
"tags": [
"hsrs",
"hsrs repo"
],
"textContent": "A recent pain-point I’ve had is generating Haskell bindings for calling Rust code. Unfortunately, none of the existing prior art was really as ergonomic as I wanted it to be.\n\nI’ve recently released hsrs – an ergonomic Haskell bindings generator for Rust. The goal of `hsrs` is to mimic the interfaces of `PyO3` and `napi-rs`, and be as ergonomic as possible.\n\n`hsrs` allows you to take this code\n\n\n #[hsrs::module(safety = unsafe)]\n mod quecto_vm {\n\n /// CPU register identifiers.\n #[derive(Debug, PartialEq, Eq)]\n #[hsrs::enumeration]\n pub enum Register {\n /// First general-purpose register.\n Reg0,\n /// Second general-purpose register.\n Reg1,\n }\n\n /// An error produced by the VM.\n #[derive(Debug, PartialEq, Eq)]\n #[hsrs::enumeration]\n pub enum VmError {\n /// Division by zero.\n DivisionByZero,\n }\n\n\n /// A tiny VM with support for addition.\n #[hsrs::data_type]\n pub struct QuectoVm { registers: [i64; 2] }\n\n impl QuectoVm {\n /// Create a new instance of the VM.\n #[hsrs::function]\n pub fn new() -> Self { ... }\n\n /// Adds register `b` into register `a` (a += b).\n #[hsrs::function]\n pub fn add(&mut self, a: Register, b: Register) { ... }\n\n /// Divides register `a` by register `b`, returning an error on division by zero.\n ///\n /// Demonstrates `Result<T, E>` → `Either E T` mapping across the FFI boundary.\n #[hsrs::function]\n pub fn safe_div(&mut self, a: Register, b: Register) -> Result<i64, VmError> { ... }\n }\n\n }\n\n\nand generate\n\n\n -- | CPU register identifiers.\n newtype Register = Register Word8\n deriving newtype (Eq, Show, Storable)\n deriving (BorshSize, ToBorsh, FromBorsh) via Word8\n\n pattern Reg0 = Register 0\n pattern Reg1 = Register 1\n\n -- | An error produced by the VM.\n newtype VmError = VmError Word8\n deriving newtype (Eq, Show, Storable)\n deriving (BorshSize, ToBorsh, FromBorsh) via Word8\n\n data QuectoVmRaw\n\n -- | A tiny VM with support for addition.\n newtype QuectoVm = QuectoVm (ForeignPtr QuectoVmRaw)\n\n -- | Create a new instance of the VM.\n new :: IO QuectoVm\n new = do\n ptr <- c_quectoVmNew\n fp <- newForeignPtr c_quectoVmFree ptr\n pure (QuectoVm fp)\n\n -- | Adds register `b` into register `a` (a += b).\n add :: QuectoVm -> Register -> Register -> IO ()\n add (QuectoVm fp) a b = withForeignPtr fp $ \\ptr -> c_quectoVmAdd ptr (let (Register a') = a in a') (let (Register b') = b in b')\n\n -- | Divides register `a` by register `b`, returning an error on division by zero.\n --\n -- Demonstrates `Result<T, E>` → `Either E T` mapping across the FFI boundary.\n safeDiv :: QuectoVm -> Register -> Register -> IO (Either VmError Int64)\n safeDiv (QuectoVm fp) a b = withForeignPtr fp $ \\ptr ->\n fromBorshBuffer =<< c_quectoVmSafeDiv ptr (let (Register a') = a in a') (let (Register b') = b in b')\n\n\n`hsrs` will generate both the Haskell side and the necessary C FFI bridges in Rust. The way I achieved rich type-semantics across both implementations is through `borsh` which serializes types in the Rust-side of things, and then deserializes it on the Haskell end.\n\nFor a full example, I’d recommend you look at the QuectoVM example in the hsrs repo.\n\n## Quickstart\n\nMark your crate as a static library and add the `hsrs` crate:\n\n\n [lib]\n crate-type = [\"lib\", \"staticlib\"]\n\n [dependencies]\n hsrs = \"0.1\"\n\n\nIn your cabal file, add:\n\n\n build-depends:\n hsrs >= 0.1 && < 0.2\n extra-libraries: your_project\n extra-lib-dirs: path/to/crate/target/release\n\n\nAs part of your compilation process, run the codegen:\n\n\n cargo install hsrs-codegen\n hsrs-codegen src/lib.rs -o YourProject.hs\n\n\nAnd then use the bindings in your haskell code:\n\n\n import qualified YourProject\n main :: IO ()\n main = do\n vm <- YourProject.new\n someResult <- YourProject.exampleFunction vm\n print someResult\n\n\n## Prior Art\n\n### hs-bindgen\n\nA relatively popular project is hs-bindgen, `https://github.com/yvan-sraka/hs-bindgen`. My understanding for this crate is that only primitive C types are supported, which did not suit my ergonomics requirements. `hsrs` supports serializable value types, mapping between `String` and `Text`, `Vec<T>` ↔ `[T]`, `Result<T, E>` ↔ `Either E T`, etc.\n\n### Purgatory\n\nI stumbled upon Calling Purgatory from Heaven – `https://well-typed.com/blog/2023/03/purgatory/` – after writing `hsrs`, which describes a similar approach to what `hsrs` employs. The system described in that article outlines two packages – foreign-rust, `https://github.com/BeFunctional/haskell-foreign-rust`, and haskell-ffi, `https://github.com/BeFunctional/haskell-rust-ffi`. From now, I will refer to these two packages as `Purgatory`. Similar ideas and differences are:\n\n * Both `hsrs` and `Purgatory` use `borsh` as the underlying serialization scheme for sharing value types across the FFI boundary.\n * `hsrs`, unlike `Purgatory`, automatically does Haskell codegen for you from your Rust types. `hsrs` automatically emits `extern` functions and automatically generates binding files. We support automatic `.hs` codegen and have some nifty features:\n * Automatic value-type serialization/deserialization.\n * Automatic Haddock codegen from your Rust codegen.\n * Automatic `Derive` propagation – things that you marked as `Eq` in Rust automatically get `Eq` in Haskell, etc.\n\n\n\n## Future Work\n\nThere are a couple things I see `hsrs` scaling into:\n\n * `async` – My current needs are exclusively synchronous, but I do see `hsrs` growing into adding support for `async`.\n * `hsrs` does not support stack allocation. Even for fixed-size types, `hsrs` allocates on the heap. This could be better improved for types of known sizes – the haskell side can allocate a buffer that `hsrs` can write into, if the wire structure is `Sized`.\n\n\n\n**Feedback is very welcome** – I want `hsrs` to solve for your needs as well as it does for mine. I commit to supporting this project for the next year, or so, to the best of my abilities.",
"title": "[ANN] hsrs -- Ergonomic Haskell Bindings for Rust"
}