C 不再是一种编程语言( 三 )

上面是Aria在Ubuntu 20.04 x64上运行的FFI abi-checker,她在这个相当重要的、表现良好的平台上测试了一些非常无聊的情况 。结果发现,一些整数参数在两个由Clang和GCC编译的静态库之间按值传递失败了!
Aria发现,Clang和GCC甚至不能就Linux x64上_int128的ABI达成一致 。
Aria本来是为了检查rustc中的错误,没想到会在一个重要的、常用的ABI上发现两大主流C编译器的不一致 。

C 不再是一种编程语言

文章插图
试图驯服C
Aria认为,可怕的是对C头文件进行语义解析,只能由该平台的C编译器来完成 。即使C编译器告诉了你类型和如何理解注释,但实际上你仍然不知道所有内容的大小/对齐/惯例 。那如何与这些乱七八糟的东西进行互操作呢?Aria提供了两种选择 。
第一个选择是完全投降,将你的语言与C进行灵魂绑定,这可以是以下任何一种:
  • 用C(++)编写你的编译器/运行时,这样它就可以用C了
  • 让你的 "codegen "直接发出C(++),这样用户无论如何都需要一个C编译器
  • 将你的编译器建立在一个成熟的主要C编译器(Clang或GCC)之上
但上面这些也只能让你走这么远,因为除非你的语言真的暴露了unsigned long long,否则你将继承C的巨大可移植性混乱 。
这就让我们想到了第二个选择:撒谎、欺骗和偷窃 。
如果这一切是无论如何都无法避免的灾难,你还不如开始手工翻译类型和接口定义到你的语言中,基本上就是我们每天在Rust中所做的事情 。比如,人们使用rust-bindgen和friends自动化处理一些事,但很多时候,定义会被检查或手工调整 。因为人们不想浪费时间,去尝试Phantomderp的定制C构建系统可移植地工作 。
在Rust中,Linux x64上的intmax_t是什么?
pub type intmax_t = i64;在Nim中,Linux x64上的long long是什么?
clonglong {.importc: "long long", nodecl.} = int64很多代码已经完全放弃将C保持在循环中,开始对核心类型的定义进行硬编码 。毕竟,它们显然只是平台ABI的一部分!他们要改变intmax_t的大小吗?这显然是一个破坏ABI的变化!
那phantomderp正在研究的又是什么?
我们讨论过为何intmax_t不能被改变,因为如果我们从long long(64位整数)改为_int128_t(128位整数),某个地方的二进制会失控使用错误的调用约定/返回约定 。但有没有一种方法,如果代码选择了它或其他东西,我们可以为较新的应用程序升级函数调用,而让旧应用程序保持不变?让我们编写一些代码,测试一下透明别名可以帮助ABI的想法 。
Aria提出了她的疑问:编程语言如何处理这种变化?如何指定与哪个版本的 intmax_t互操作?如果你有一些C头文件提到intmax_t,它使用的是哪个定义?
在此讨论具有不同ABI的平台的主要机制是目标三元组 。你知道什么是目标三元组吗?你知道基本上涵盖了过去20年里所有主流桌面/服务器Linux发行版的 x86_64-unknown-linux-gnu包括什么吗?现在,虽然表面上可以针对这个目标进行编译,并得到一个在所有这些平台上都能“正常工作”的二进制文件,但Aria不相信有些程序会被编译成intmax_t大于int64_t
任何试图做出这种改变的平台都会成为一个新的x86_64-unknown-linux-gnu2 目标三元组吗?如果任何针对x86_64-unknown-linux-gnu编译的东西都被允许在上面运行,这难道还不够吗?
C 不再是一种编程语言

文章插图
 
在不破坏ABI的情况下更改签名"那又怎样,C永远不会再有进步吗?"不!但也是!因为他们提供了糟糕的设计 。
老实说,进行ABI兼容的修改是一种艺术形式 。这种艺术的一部分就是准备工作 。具体来说,如果你准备好了,做出不破坏ABI的修改就会容易得多 。
正如phantomderp的文章所指出的,像glibc( g  x86_64-unknown-linux-gnu 中的 gnu )早就明白了这一点,并使用符号版本化这样的机制来更新签名和API,同时为任何针对旧版本编译的人保留旧版本 。
因此,如果你有 int32_t my_rad_symbol(int32_t) ,你告诉编译器将其导出为 my_rad_symbol_v1 ,那么任何根据这个头文件进行编译的人,都会在他们的代码中写上


推荐阅读