Show HN: Hsrs – Type-Safe Haskell Bindings Generator for Rust
Summary
Hsrs is a type-safe FFI bindings generator that allows Rust code to be called from Haskell with automatic memory management, type conversions, and Borsh serialization. It provides annotations in Rust and generates idiomatic Haskell wrappers.
View Cached Full Text
Cached at: 05/19/26, 07:01 AM
harmont-dev/hsrs
Source: https://github.com/harmont-dev/hsrs
hsrs
Call Rust from Haskell with type-safe, automatically generated FFI bindings.
Annotate your Rust types and functions, run the code generator, and get idiomatic Haskell that handles memory management, serialization, and type conversions for you.
Quick start
1. Annotate your Rust code
#[hsrs::module]
mod canvas {
#[hsrs::value_type]
pub struct Point {
pub x: i32,
pub y: i32,
}
#[hsrs::data_type]
pub struct Canvas {
points: Vec<Point>,
}
impl Canvas {
#[hsrs::function]
pub fn new() -> Self { Self { points: vec![] } }
#[hsrs::function]
pub fn add_point(&mut self, p: Point) { self.points.push(p); }
#[hsrs::function]
pub fn count(&self) -> u64 { self.points.len() as u64 }
}
}
2. Generate Haskell bindings
cargo install hsrs-codegen
hsrs-codegen src/lib.rs -o Bindings.hs
3. Use from Haskell
import Bindings
main :: IO ()
main = do
c <- new
addPoint c (Point 10 20)
n <- count c
print n -- 1
That’s it. Memory is managed automatically via ForeignPtr, and complex types like Point are serialized across the boundary with Borsh.
Setup
Rust side — add hsrs to your crate:
[lib]
crate-type = ["lib", "staticlib"]
[dependencies]
hsrs = "0.1"
Haskell side — add the hsrs runtime package:
build-depends:
hsrs >= 0.1 && < 0.2
This pulls in Borsh serialization automatically — no extra dependencies needed.
What you can annotate
| Annotation | What it does | Haskell result |
|---|---|---|
#[hsrs::data_type] | Opaque struct passed by pointer | ForeignPtr newtype with automatic cleanup |
#[hsrs::enumeration] | C-compatible enum (repr(u8)) | Word8 newtype with pattern synonyms |
#[hsrs::value_type] | Struct passed by value via Borsh | data record with Borsh deriving |
#[hsrs::function] | Method exported over FFI | Type-safe Haskell wrapper |
#[hsrs::module] | Groups a data type with its methods | Generates all FFI glue for the type |
Result<T, E> becomes Either E T, Option<T> becomes Maybe T, Vec<T> becomes [T], and String becomes Text — all serialized transparently via Borsh.
Supported types
| Rust | Haskell | Transfer |
|---|---|---|
i8, i16, i32, i64 | Int8, Int16, Int32, Int64 | Direct (C FFI) |
u8, u16, u32, u64 | Word8, Word16, Word32, Word64 | Direct (C FFI) |
bool | CBool | Direct (C FFI) |
usize / isize | Word64 / Int64 | Direct (C FFI) |
#[hsrs::enumeration] enum | Word8 newtype + patterns | Direct (C FFI) |
#[hsrs::value_type] struct | data record | Borsh |
String | Text | Borsh |
Vec<T> | [T] | Borsh |
Option<T> | Maybe T | Borsh |
Result<T, E> | Either E T | Borsh |
Platform notes
usize and isize are mapped to Word64 and Int64 respectively. This matches 64-bit platforms (x86_64, aarch64). If you target 32-bit platforms, be aware that values may be truncated.
Full example
A small VM with enums, value types, Result, and Option
Rust
#[hsrs::module]
mod quecto_vm {
#[derive(Debug, PartialEq, Eq)]
#[hsrs::enumeration]
pub enum Register { Reg0, Reg1, Count }
#[derive(Debug, PartialEq, Eq)]
#[hsrs::value_type]
pub struct Point { pub x: i32, pub y: i32 }
#[derive(Debug, PartialEq, Eq)]
#[hsrs::value_type]
pub struct VmError { pub code: u32 }
#[hsrs::data_type]
pub struct QuectoVm {
registers: [i64; Register::Count as usize],
clock: usize,
}
impl QuectoVm {
#[hsrs::function]
pub fn new() -> Self { /* ... */ }
#[hsrs::function]
pub fn store(&mut self, r: Register, v: i64) { /* ... */ }
#[hsrs::function]
pub fn snapshot(&self) -> Point { /* ... */ }
#[hsrs::function]
pub fn safe_div(&mut self, a: Register, b: Register) -> Result<i64, VmError> { /* ... */ }
#[hsrs::function]
pub fn nonzero(&self, r: Register) -> Option<i64> { /* ... */ }
}
}
Generated Haskell
newtype Register = Register Word8
deriving (Eq, Show, Storable)
deriving (BorshSize, ToBorsh, FromBorsh) via Word8
pattern Reg0 :: Register
pattern Reg0 = Register 0
data Point = Point
{ pointX :: Int32
, pointY :: Int32
} deriving (Generic, Eq, Show)
deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct Point
data VmError = VmError
{ vmErrorCode :: Word32
} deriving (Generic, Eq, Show)
deriving (BorshSize, ToBorsh, FromBorsh) via AsStruct VmError
data QuectoVmRaw
newtype QuectoVm = QuectoVm (ForeignPtr QuectoVmRaw)
new :: IO QuectoVm
store :: QuectoVm -> Register -> Int64 -> IO ()
snapshot :: QuectoVm -> IO Point
safeDiv :: QuectoVm -> Register -> Register -> IO (Either VmError Int64)
nonzero :: QuectoVm -> Register -> IO (Maybe Int64)
License
MIT OR Apache-2.0
Similar Articles
Show HN: Galois connections for composable numeric casts in Rust
A Rust crate that implements Galois connections for lawful, composable numeric type conversions with property-tested invariants and compile-time composition.
hax: A Rust verification tool
hax is a tool for translating Rust code into formal languages like F*, Rocq, and Lean for high-assurance verification.
Show HN: 18KB ls alternative in no_std rust and Libc
A minimal 18KB ls command alternative written in Rust using no_std and Libc.
Show HN: Nectar, a Rust-like React that compiles to WebAssembly
Nectar is a new web framework that compiles Rust-like code to WebAssembly, eliminating JavaScript dependencies with O(1) signal updates and zero-dependency builds.
Show HN: Bun-sqlgen – Type-safe raw SQL for Bun, no ORM
Bun-sqlgen is a type-safe raw SQL library for Bun that uses code generation to check queries against a live Postgres or SQLite schema, producing fully-typed, null-safe results without an ORM.