Hotdry.

Article

通过派生宏实现类型安全的 Haskell-Rust FFI 绑定

Hsrs 通过派生宏与 Borsh 序列化,自动生成 Haskell 与 Rust 之间的类型安全 FFI 绑定,解决跨语言接口维护的脆弱性问题。

2026-05-19compilers

跨语言 FFI(Foreign Function Interface)绑定是系统级开发中的常见需求,但手工维护接口定义往往导致脆弱性:Rust 侧修改结构体字段或函数签名后,Haskell 侧的绑定代码需要同步更新,否则会在运行时出现内存布局不匹配或类型转换错误。Hsrs 项目通过派生宏(derive macros)和属性宏(attribute macros)实现了 Haskell 与 Rust 之间的类型安全 FFI 绑定自动生成,将接口契约的维护成本降至最低。

核心设计:注解驱动的代码生成

Hsrs 的工作流程分为三步:在 Rust 代码中添加注解、运行代码生成器、在 Haskell 中直接调用。这种设计将 FFI 绑定的复杂性封装在宏展开阶段,开发者无需手动编写 C 粘合层或维护重复的类型定义。

Rust 侧使用 #[hsrs::module] 标记模块,#[hsrs::value_type] 标记值类型结构体,#[hsrs::data_type] 标记不透明数据类型,#[hsrs::function] 标记需要导出的方法。例如:

#[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-codegen src/lib.rs -o Bindings.hs 后,生成的 Haskell 代码包含完整的类型定义和 FFI 导入声明,开发者只需 import Bindings 即可使用。

类型映射与内存管理策略

Hsrs 对不同类型的数据采用差异化的跨边界传输策略。基本数值类型(i8i64u8u64boolusize/isize)通过 C FFI 直接传递,无需序列化开销。枚举类型标记为 #[hsrs::enumeration] 后,要求使用 repr(u8) 表示,在 Haskell 侧映射为 Word8 newtype 并生成模式同义词(pattern synonyms),保持语义等价。

复杂数据结构采用 Borsh(Binary Object Representation Schema for Hash)序列化协议。标记为 #[hsrs::value_type] 的结构体在跨边界时自动序列化为字节流,Haskell 侧通过 BorshSizeToBorshFromBorsh 派生实例完成反序列化。这种设计使得 String 映射为 TextVec<T> 映射为 [T]Option<T> 映射为 Maybe TResult<T, E> 映射为 Either E T, idiomatic Haskell 代码可以直接操作 Rust 返回的数据。

对于需要保持状态的对象,标记为 #[hsrs::data_type] 的类型在 Haskell 侧封装为 ForeignPtr newtype。Hsrs 自动生成 ForeignPtrFinalizer,当 Haskell 垃圾回收器释放引用时,自动调用 Rust 的析构函数。这种机制避免了手动内存管理的错误风险,同时允许 Rust 的借用检查器在编译期验证生命周期安全。

可落地的工程参数

在实际项目中采用 Hsrs,需要关注以下配置要点:

Rust 侧依赖配置

[lib]
crate-type = ["lib", "staticlib"]

[dependencies]
hsrs = "0.1"

staticlib 是必须的,因为 Haskell 的 GHC 需要链接静态库。Rust 版本要求 1.85 以上。

Haskell 侧依赖配置

build-depends:
    hsrs >= 0.1 && < 0.2

运行时包自动引入 Borsh 序列化依赖,无需额外配置。

类型对照速查表

Rust 类型 Haskell 类型 传输方式
i8/i16/i32/i64 Int8/Int16/Int32/Int64 直接 FFI
u8/u16/u32/u64 Word8/Word16/Word32/Word64 直接 FFI
bool CBool 直接 FFI
#[hsrs::enumeration] Word8 + patterns 直接 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 序列化

平台兼容性限制

usizeisize 被固定映射为 Word64Int64,这与 x86_64 和 aarch64 平台的指针宽度一致。如果目标平台是 32 位架构,大数值可能发生截断,需要在设计阶段评估数值范围。

与 hs-bindgen 的差异定位

Hsrs 与另一项目 hs-bindgen 都致力于 Haskell-Rust FFI 绑定,但技术路径不同。hs-bindgen 使用过程宏生成 C-FFI 粘合层,开发者需要处理 C 头文件和链接配置;Hsrs 则提供类型安全的直接绑定机制,通过 Borsh 序列化规避了 C ABI 的复杂性。对于需要频繁传递复杂数据结构的项目,Hsrs 的自动化程度更高;对于需要与现有 C 库集成的场景,hs-bindgen 可能更灵活。

适用场景与权衡

Hsrs 最适合以下场景:Rust 实现核心算法或性能关键模块,Haskell 提供业务逻辑和 DSL 层,两者需要高频交互且数据结构复杂。自动生成的绑定代码减少了手写 FFI 的错误面,Borsh 序列化保证了跨版本兼容性(字段增删遵循向后兼容规则)。

需要注意的是,Borsh 序列化引入的编解码开销在热路径上可能成为瓶颈。对于高频调用的纯数值计算,建议将批量操作封装在 Rust 侧,通过 #[hsrs::function] 暴露粗粒度接口,减少跨边界调用次数。


资料来源

compilers

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com