场景

很多团队不是从零重写,而是把热点模块逐步迁到 Rust。最现实的路径是:

  • C++ 主程序继续跑
  • Rust 提供一个动态库
  • 双方通过 C ABI 通信

核心原则很简单:跨语言只传 C 兼容类型

先定义稳定接口

#[repr(C)]
pub struct CalcResult {
    pub code: i32,
    pub value: i64,
}

#[no_mangle]
pub extern "C" fn calc_sum(a: i64, b: i64) -> CalcResult {
    CalcResult { code: 0, value: a + b }
}
  • #[repr(C)] 保证结构体布局可预测
  • extern "C" 保证调用约定一致
  • #[no_mangle] 让符号名可被 C++ 链接

字符串内存谁分配谁释放

跨边界最容易出问题的是字符串:

  • Rust 分配、C++ 释放,或者反过来
  • 不同分配器混用导致崩溃

推荐做法:统一由一侧分配和释放,并显式提供 free 函数。

错误处理不要 panic 穿透

FFI 层应该“防炸”:

#[no_mangle]
pub extern "C" fn safe_div(a: i64, b: i64, out: *mut i64) -> i32 {
    if out.is_null() {
        return -2;
    }
    if b == 0 {
        return -1;
    }
    unsafe { *out = a / b; }
    0
}

返回错误码是最朴素、也最稳的方式。

性能要看端到端

FFI 的一次调用成本不高,但频繁小调用会拖垮收益。经验是:

  • 把细粒度调用合并成批处理接口
  • 在 Rust 内部完成更多计算
  • 减少跨边界来回搬运

收尾

Rust/C++ 混编的关键不在“语法能不能过”,而在边界是否稳定:

  • ABI 明确
  • 所有权清晰
  • 错误模型统一

边界稳了,性能优化才有意义。