Show HN: Hsrs – Type-Safe Haskell Bindings Generator for Rust

Hsrs is a new tool that automates the creation of type-safe Haskell bindings for Rust code, handling memory management and data serialization seamlessly.
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.
#[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 }
}
}
cargo install hsrs-codegen
hsrs-codegen src/lib.rs -o Bindings.hs
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.
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.
| 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.
| 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 |
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.
A small VM with enums, value types, Result, and Option
#[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> { /* ... */ }
}
}
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)
MIT OR Apache-2.0
Source: Hacker News

















