跳到主要内容
FFI 是那道永远在重写的缝

FFI 是那道永远在重写的缝

原石
原石

· 阅读约 4 分钟

先把结构体贴上。

typedef struct {
    ffi_type *t;
    void     *val;
} ffi_arg;

一个类型,一个值。就这么多。谁调谁负责把 C 那边的 int、double、char * 塞进 val,谁负责管它的生命周期。

这不是哪个库的真代码,是我脑子里 FFI 该有的样子。现实不长这样。现实是一层类型描述、一层编解码、一层生命周期、一层错误转换,外面再套一层"让用户少写代码"的宏。你只想调一个 foo(int, char *),先读懂三个模块才敢动手。

上周末 RacketCon 开完,我事后把日程过了一遍。Matthew Flatt 那场叫 ffi2。按摘要说,是把 Eli Barzilay 早年的 FFI 工作和 Andy Keep 给 Chez Scheme 写的 ftypes 层合到一起。

两次重做,同一道缝。

FFI 干的事,就是把两个对不上的世界焊在一起。类型、内存模型、错误处理,全都对不上。这几样没有哪一样能靠"设计得好"消掉。

最常见的填法是一张转换表。

/* 每来一个类型,加一行 */
case FFI_INT:    *(int *)out    = va_arg(ap, int);    break;
case FFI_DOUBLE: *(double *)out = va_arg(ap, double); break;
case FFI_PTR:    *(void **)out  = va_arg(ap, void *); break;

这张表能长到两百行。每加一行,你都在替一个新的类型组合做一次决定。int 和 long 在 LP64 上不一样,size_t 和指针同宽但语义不同,结构体是按值传还是按引用传。C 自己的类型系统就到处是平台坑,你把表填满,等于把那门语言的坑抄了一遍。

所以有人想:别手填了,让机器填。从头文件生成。

bindings.rkt: include/foo.h
	c2ffi $< > $@

然后 raco make 一下。你的 build 图里多一个节点,输入是别人的头文件,输出是一份你没读过的代码。头文件一升级,绑定跟着变,变出什么你事前不知道。你只知道它编译过了。

我反对这个。不是生成的代码不好——它可能比我手写的还全。是你付了一样东西:你不再知道你的程序在边界上干了什么。出错的时候,栈里的行号在生成文件里,你打开那个文件,看到一万行没打算让人读的东西。

说句题外话。我手上那个玩具引擎,往外接 C 那一层我也重写过一遍。第一版想做"什么样的 C 函数都能包",第二版砍到只剩我真正要用的七个。砍完,之前攒下的那批 bug 里有小一半自己没了 😅。

扯回来。手写的绑定长这样。

/* 就三个函数,明说谁分配谁释放 */
char *foo_give(void);          /* 调用方 free */
int   foo_take(const char *s); /* s 由调用方管,库不碰 */
void  foo_free(char *p);

土。但你半夜三点调它的时候,知道 foo_give 返回的东西是你的,得你 free。

复杂度不会消失。两个世界对不上,就必然有一处地方在做翻译和记账。你能选的只有把它放在哪:库里、生成器里、还是调用者手上。

放库里,看着最省事,代价是库得替你做最多的假设,你稍微偏一点它就不适用。放生成器里,省的是人手,交出去的是边界的知情权。放调用者手上最土——但每个决定都写在调用它的那几行旁边,谁读谁明白。

我选第三个。

顺带说件比所有新特性都重要的事。Pavel Panchekha 那场讲 Herbie——一个把数学公式改写成更准的浮点表达式的编译器,在 Racket 里写了十年。十年,没换过语言。

一门语言好不好、一套 FFI 好不好,判据不是它第一天多顺手,是第十年它还在不在你的 build 里。Herbie 天天跟 double 打交道,跟 C 的数值库、跟编译器那点浮点语义打交道——这正是最容易让人掀桌子换语言的那种活。它没掀。

但有人掀了。Lucas Myers 讲的 Pille,Rhombus 上的一门低级语言,编译器走 LLVM,明说了是为了绕开 Racket VM 的限制。

我不骂这个选择。当你确定瓶颈就在 VM 本身、修不动、也不该你去修,绕开是最短的路。但你得清楚换走了什么:整个 Racket 那一大摊你没被卡住的库,也一并不要了。换房子不是修缝,是两个动作,别混着算。

真正该骂的是第三种:缝还在,你在缝上又搭一层"通用适配层",理由是以后换 VM 好换。缝没修,债先记上了。

所以规则一条。凡是有人要给一道缝加一层"以后好换",先让他说清楚要换成什么、什么时候换、不换会不会死。答不上来的,那层就是抽象税。

同一道缝修两遍不丢人。丢人的是每修一遍,都想顺手把所有人的假设一次包进来。

原石
原石

把代码当文章写的系统工程师,以源码立论、单线程式拒绝复杂度。

查看主页 →