【FFI】 内存管理——跨语言边界最难的问题
跨语言调用的核心矛盾只有三个:数据类型怎么传、函数怎么调、内存怎么管。前两个靠 ABI 和绑定工具解决,第三个是最难的——GC 语言和手动管理语言各有自己的内存世界观,边界上谁说了算?
两个世界,一个边界
Rust 说:每个值有且仅有一个 owner,owner 离开作用域就 drop。Python 说:引用计数归零就 __del__。C 说:你自己 malloc 的,自己记着 free。
当一段数据从 Rust 传到 Python,该谁释放?什么时候释放?
三个典型场景:
场景 1:Rust 分配,Python 使用,Rust 释放。 简单,传裸指针过去,用完 Rust 侧自己 drop。前提是 Python 侧不再持有引用。
场景 2:Rust 分配,Python 需要长期持有。 PyO3 的做法是把 Rust 结构体包进 Python PyObject,Python 的 GC 管生命周期,__del__ 时回调 Rust 的 Drop。两边的内存模型对接上了。
场景 3:Python 分配,Rust 读取完就走。 传引用过去,Rust 不持有所有权。PyO3 用 &PyAny 表示借来的引用,编译期保证不比你活得长。
这三个场景的核心是同一个问题:谁分配谁释放? C ABI 不管这个,libffi 也不管,全靠绑定层设计。
跨语言内存的三种模式
模式 1:传所有权。 你分配,我接管,我来释放。Rust 把 Vec<u8> move 给 C,C 拿到 char* 要自己 free。但直接调 free 可能不对——Rust 用 jemalloc 还是系统 allocator?解法是暴露一个 rust_free_buffer 函数:String 先 into_raw_parts,传递指针,接收方用配套的 rust_free 释放。
模式 2:传引用。 你持有,我只看。PyO3 的 &str 参数就是:Python 把 str 的内部 buffer 指针传给 Rust,Rust 读完就松手。危险在 Python 侧可能会在 Rust 还在用的时候 del 掉那个字符串,所以 GIL 在这里充当了事实上的生命周期保证。
模式 3:共享所有权。 两边都要用,最后一个人关门。Arc/Rc 是最直接的方案:Rust 侧包一层 Arc<T>,把 clone 的 Arc 指针传给 C,C 回调 Rust 时再拿着操作。谁都不需要管释放,引用计数归零自动 drop。
为什么内存问题是 FFI 里最难的
类型映射有工具帮你生成,调用约定编译器和 libffi 帮你搞定。但内存管理没有通用解。
C 的 char* 是裸指针,Python 的 str 背后有引用计数,Rust 的 Vec 有 allocator 绑定。把这三个串一起,malloc/free、GC、Drop 可能互相踩脚。
最容易犯的错误是双重释放:C 侧 free 了一次,Rust Drop 又释放了一次。其次是use after free:Python 侧 del 了对象,C 侧还拿着野指针。
Panama 的 Arena 和 PyO3 的 #[pyclass] 本质上都在解决同一个问题:让分配方和释放方一致,让生命周期在编译期或 API 层面就确定下来。
寒蝉 Hancic

