【FFI】 FFI 为什么存在——跨语言调用的根本问题
写代码几十年了,没有哪个语言能统一所有场景。C/C++ 统治系统编程,Java 统治企业后端,Python 统治数据科学。每种语言都有它擅长的生态,也有它够不着的角落。
FFI(Foreign Function Interface)的存在,本质上是为了两件事:
借用性能。 Python 写数据管道很顺手,但跑数值计算就慢得离谱。一个百万次的矩阵乘法循环,纯 Python 要跑几分钟,调 C 的 BLAS 库只要几毫秒。你要的不是 C 语言本身,是它直接操作硬件的能力。类似地,Java 要调 SIMD 指令做向量化,也得走 JNI。
借用生态。 Rust 想用 Python 的 ML 训练框架(PyTorch),Go 想用 C++ 的游戏引擎(Unreal),都不可能自己重写一遍。FFI 让你把现成的库拿过来用,语言只是载体。
这两个动机的副产品是"老系统不用重写"——不是因为你想保留旧代码,而是因为你能直接调用它,重写也就没必要了。
但跨语言调用不是"调个函数"那么简单。两个语言的运行时本质上是两套不同的世界,中间隔着三个鸿沟:
第一,函数怎么调。 两个语言有不同的调用约定——参数放寄存器还是压栈?返回值放哪里?谁清栈?一个统一的起点是:几乎所有操作系统都定义了 C 的 ABI(Application Binary Interface)。所以不管你是 Python 调 Rust,还是 Java 调 Go,中间都要先"翻译"成 C 的调用约定。libffi 就是这个翻译层的底层实现——它在运行时动态构造调用栈,让你不需要在编译期就知道目标函数的签名。
第二,数据怎么传。 两边的类型系统不是一一对应的。int、float 没问题,但 struct 的内存对齐、union 的判别、函数指针的表示方式完全不同。更头疼的是像 Go 的 slice(指针+长度+容量三元组)和 C 的数组(裸指针),看着像,实际不是一个东西。你得做 marshal:把数据从一种表示翻译成另一种表示。这也是 FFI 调用的主要开销来源。
第三,内存谁管。 这是坑最多的。GC 语言(Java、Python、Go)和手动管理语言(C/C++)混在一起时,一个指针从 C 传到 Java,Java 的 GC 管不了它;如果 Java 这边把它 free 了,C 那边可能还在用。反过来,如果 C 直接在 Java 的 GC 堆上写数据,GC 下次挪动对象时,C 手里的指针就变野指针了。
三个鸿沟,FFI 就是填补它们的那层胶水。它不是某个具体技术,而是一类技术的统称。各语言的 FFI 方案本质上都是在解决这三个问题,只是策略和取舍不同。
寒蝉 Hancic

