
hsrs -- PyO3-style bindings generator for Haskell
Hey everyone! I recently needed an ergonomic Haskell bindings generator for Rust code and realized one doesn't really exist, so I decided to build my own! hsrs is an ergonomic bindings generator which will take your Rust code, with hsrs annotations, and generate a Haskell bindings for you.
hsrs allows you to take this code
#[hsrs::module(safety = unsafe)]
mod quecto_vm {
/// CPU register identifiers.
#[derive(Debug, PartialEq, Eq)]
#[hsrs::enumeration]
pub enum Register {
/// First general-purpose register.
Reg0,
/// Second general-purpose register.
Reg1,
}
/// An error produced by the VM.
#[derive(Debug, PartialEq, Eq)]
#[hsrs::enumeration]
pub enum VmError {
/// Division by zero.
DivisionByZero,
}
/// A tiny VM with support for addition.
#[hsrs::data_type]
pub struct QuectoVm { registers: [i64; 2] }
impl QuectoVm {
/// Create a new instance of the VM.
#[hsrs::function]
pub fn new() -> Self { ... }
/// Adds register `b` into register `a` (a += b).
#[hsrs::function]
pub fn add(&mut self, a: Register, b: Register) { ... }
/// Divides register `a` by register `b`, returning an error on division by zero.
///
/// Demonstrates `Result<T, E>` → `Either E T` mapping across the FFI boundary.
#[hsrs::function]
pub fn safe_div(&mut self, a: Register, b: Register) -> Result<i64, VmError> { ... }
}
}
and generate
-- | CPU register identifiers.
newtype Register = Register Word8
deriving newtype (Eq, Show, Storable)
deriving (BorshSize, ToBorsh, FromBorsh) via Word8
pattern Reg0 = Register 0
pattern Reg1 = Register 1
-- | An error produced by the VM.
newtype VmError = VmError Word8
deriving newtype (Eq, Show, Storable)
deriving (BorshSize, ToBorsh, FromBorsh) via Word8
data QuectoVmRaw
-- | A tiny VM with support for addition.
newtype QuectoVm = QuectoVm (ForeignPtr QuectoVmRaw)
-- | Create a new instance of the VM.
new :: IO QuectoVm
new = do
ptr <- c_quectoVmNew
fp <- newForeignPtr c_quectoVmFree ptr
pure (QuectoVm fp)
-- | Adds register `b` into register `a` (a += b).
add :: QuectoVm -> Register -> Register -> IO ()
add (QuectoVm fp) a b = withForeignPtr fp $ \ptr -> c_quectoVmAdd ptr (let (Register a') = a in a') (let (Register b') = b in b')
-- | Divides register `a` by register `b`, returning an error on division by zero.
--
-- Demonstrates `Result<T, E>` → `Either E T` mapping across the FFI boundary.
safeDiv :: QuectoVm -> Register -> Register -> IO (Either VmError Int64)
safeDiv (QuectoVm fp) a b = withForeignPtr fp $ \ptr ->
fromBorshBuffer =<< c_quectoVmSafeDiv ptr (let (Register a') = a in a') (let (Register b') = b in b')
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.
For a full example, I'd recommend you look at the QuectoVM example in the hsrs repo.
Prior Art
hs-bindgen
A 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.
Purgatory
I 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:
- Both
hsrsandPurgatoryuseborshas the underlying serialization scheme for sharing value types across the FFI boundary. hsrs, unlikePurgatory, automatically does Haskell codegen for you from your Rust types.hsrsautomatically emitsexternfunctions and automatically generates binding files. We support automatic.hscodegen and have some nifty features:- Automatic value-type serialization/deserialization.
- Automatic Haddock codegen from your Rust codegen.
- Automatic
Derivepropagation -- things that you marked asEqin Rust automatically getEqin Haskell, etc.
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.
Note: This has been cross-posted on discourse.haskell.org