harmont-dev/hsrs

Type-safe Haskell Rust Bindings

Rust

85

130 commits

updated Jul 13, 2026

See the code

README

hsrs

actions status codecov crates.io minimum rustc 1.85 discord

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

AnnotationWhat it doesHaskell result
#[hsrs::data_type]Opaque struct passed by pointerForeignPtr newtype with automatic cleanup
#[hsrs::enumeration]C-compatible enum (repr(u8))Word8 newtype with pattern synonyms
#[hsrs::value_type]Struct passed by value via Borshdata record with Borsh deriving
#[hsrs::function]Method exported over FFIType-safe Haskell wrapper
#[hsrs::module]Groups a data type with its methodsGenerates 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

RustHaskellTransfer
i8, i16, i32, i64Int8, Int16, Int32, Int64Direct (C FFI)
u8, u16, u32, u64Word8, Word16, Word32, Word64Direct (C FFI)
boolCBoolDirect (C FFI)
usize / isizeWord64 / Int64Direct (C FFI)
#[hsrs::enumeration] enumWord8 newtype + patternsDirect (C FFI)
#[hsrs::value_type] structdata recordBorsh
StringTextBorsh
Vec<T>[T]Borsh
Option<T>Maybe TBorsh
Result<T, E>Either E TBorsh

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

Custom license (source-available). Based on the MIT license with an entity exclusion clause. See LICENSE for full terms.

Note: This software was previously dual-licensed under MIT OR Apache-2.0. Versions prior to this change remain available under those terms.

bindgen
binding-generator
bindings
borsh
cabal
cargo
codegen
ffi
haskell
haskell-bindings
interop
macros
proc-macro
rust
rust-bindings
serialization
type-safe

Contributors

markovejnovic

130 commits

harmont-dev/hsrs

Type-safe Haskell Rust Bindings

Rust

85

130 commits

updated Jul 13, 2026

See the code

README

hsrs

actions status codecov crates.io minimum rustc 1.85 discord

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

AnnotationWhat it doesHaskell result
#[hsrs::data_type]Opaque struct passed by pointerForeignPtr newtype with automatic cleanup
#[hsrs::enumeration]C-compatible enum (repr(u8))Word8 newtype with pattern synonyms
#[hsrs::value_type]Struct passed by value via Borshdata record with Borsh deriving
#[hsrs::function]Method exported over FFIType-safe Haskell wrapper
#[hsrs::module]Groups a data type with its methodsGenerates 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

RustHaskellTransfer
i8, i16, i32, i64Int8, Int16, Int32, Int64Direct (C FFI)
u8, u16, u32, u64Word8, Word16, Word32, Word64Direct (C FFI)
boolCBoolDirect (C FFI)
usize / isizeWord64 / Int64Direct (C FFI)
#[hsrs::enumeration] enumWord8 newtype + patternsDirect (C FFI)
#[hsrs::value_type] structdata recordBorsh
StringTextBorsh
Vec<T>[T]Borsh
Option<T>Maybe TBorsh
Result<T, E>Either E TBorsh

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

Custom license (source-available). Based on the MIT license with an entity exclusion clause. See LICENSE for full terms.

Note: This software was previously dual-licensed under MIT OR Apache-2.0. Versions prior to this change remain available under those terms.

bindgen
binding-generator
bindings
borsh
cabal
cargo
codegen
ffi
haskell
haskell-bindings
interop
macros
proc-macro
rust
rust-bindings
serialization
type-safe

Contributors

markovejnovic

130 commits

Languages

Rust

99.1%