{
  "$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"
}