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.hsimport Bindings
main :: IO ()
main = do
c <- new
addPoint c (Point 10 20)
n <- count c
print n -- 1That'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.2This 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