C 不再是一种编程语言?

Aria和朋友在“对C ABI接口感到非常失望并试图修复上”达成了高度一致 。但在失望的原因上,Aria与朋友各自持不同意见 。那具体产生了哪些分歧呢?为什么会提出C不再是一种编程语言的观点呢?笔者对原文进行了编译:
试图从原生上改善使用C本身作为编程语言的条件,而Aria则希望改善使用C以外的任何语言条件 。
这时候大家就会产生疑问了,这个问题和C有什么关系?
Aria表示:如果C真的是一种编程语言,那就和它无关 。不幸的是,它并不是 。这不是说数十亿种实现方式和失败的层次结构,导致它的定义方式非常糟糕的事实,而是C被提升到一个具有威望和权力的角色,它的统治是绝对和永恒的 。C是编程的通用语言 , 我们都必须学C,因此C不再只是一种编程语言,它成了每一种通用编程语言都需要遵守的协议 。
这实际有点像是关于整个“C是一个不可捉摸的实现定义混乱”。但仅因为它让我们不得不使用这个协议,这就变成了一个更大的噩梦 。
外部功能接口
下面一起来谈谈技术问题 。假如你已经完成了你的新语言的设计,对Bappy Paws//Fins有一流的支持 。这是一种神奇的语言 , 将彻底改变cats、sheep、和的编程方式 。
但现在需要让它真正做一些有用的事情 。比如接受用户的输入,或者输出,或者字面上的任何可观察之类的东西 。如果你想让该语言编写的程序与主流操作系统兼容 , 那就需要与操作系统的界面进行交互 。听说Linux上的一切都“只是一个文件”,所以一起在Linux上打开一个文件吧!
OPEN(2)NAMEopen, openat, creat - open and possibly create a fileSYNOPSIS#include int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);int creat(const char *pathname, mode_t mode);int openat(int dirfd, const char *pathname, int flags);int openat(int dirfd, const char *pathname, int flags, mode_t mode);/* Documented separately, in openat2(2): */int openat2(int dirfd, const char *pathname,const struct open_how *how, size_t size);Feature Test Macro Requirements for glibc (seefeature_test_macros(7)):openat():Since glibc 2.10:_POSIX_C_SOURCE >= 200809LBefore glibc 2.10:_ATFILE_SOURCE
这是,不是C,那Linux的接口在哪里?
你说Linux中没有接口是什么意思?好吧 , 当然是因为这是一种全新的语言,但你会添加一个,对吗?那这时你就会发现,你好像必须使用他们给的东西 。
你将需要某种接口,让语言能够调用外部的函数,就像外部函数接口FFI 。然后你发现Rust也有C FFI,Swift也有,甚至也有 。
你会发现,每个人都必须学会C才能与主流的操作系统对话,然后当需要相互对话时,大家突然都用起了C 。所以…为什么不直接用C来相互对话呢?
现在C就变成了一种编程通用语言,不仅是一种编程语言 , 它还是一种协议了 。
与C对话包括哪些内容?
很明显,基本上每种语言都必须学会与C进行对话 , 而且这种语言绝对是非常明确的 。
"对话 "C是什么意思?它意味着以C头文件的形式获得接口类型和功能的描述 , 并以某种方式:
那么 , 这里就有几个问题:
实际上无法解析一个C头文件
Aria曾断言解析C基本上是不可能的,但有人说其实有很多工具可以读取C头文件,比如rust- 。事实果真如此吗?其实不然 。
使用来解析C和C++头文件 。要修改搜索的方式,请参阅clang-sys文档 。关于如何使用的更多细节,请参阅用户指南 。
任何花费大量时间试图快速解析C(++)头文件的人都会很快放弃,然后让一个C(++)编译器来做这件事 。请记住,有意义地解析C头文件不仅仅是解析:你还需要解决#、和的问题!所以现在不仅要实现所有相关功能,还要实现所有平台的头文件解析逻辑,并且还需要想方设法找到!
就拿Swift来说,它在C互操作和资源方面拥有绝对优势 , 它是由苹果开发的一门编程语言,有效取代了-C,成为在其平台上定义和使用系统API的主要语言 。在这样做的过程中,它比其他任何人都更进一步实现了ABI稳定性和设计概念 。
它也是Aria见过的最支持FFI的语言之一 。它可以本地导入(-)C(++)头文件,并产生一个漂亮的本地Swift接口 , 其类型在边界自动 "桥接 "到它们的Swift对等项(由于类型具有相同的ABI,所以通常是透明的) 。
Swift也是由苹果公司中许多构建和维护Clang和LLVM的人开发 。这些人都是C及其衍生品方面的世界顶级专家 。Doug 就是其中之一 , 他曾表达了对C FFI的看法:
所有这些都是Swift内部使用Clang来处理 C(++) ABI的原因 。这样一来,我们就不会去追着Clang增加的每一个影响ABI的新属性 。
可以看出,即使是Swift也不想花时间解析C(++)头文件 。那么,如果你绝对不想让C编译器在编译时解析和解决头文件,你该怎么做呢?
你需要手工翻译?。炕故切磇64. long…?什么是long?
C实际上没有ABI
好吧,这没有什么好惊讶的:C语言中的整数类型,为了 “可移植性”而被设计成摇摆不定的大小,实际上大小也是不稳定的 。我们可以认为很奇怪,但这也不能帮助我们了解long的大小和对齐方式 。
有人说每个平台都有标准化的调用约定和ABI,确实有,而且它们通常定义了C中关键原语的布局(并且有些不只是用C类型来定义调用约定,这里侧眼于AMD64 SysV) 。
还有一个棘手的问题:架构并没有定义ABI,操作系统也是 。我们必须在一个特定的目标三元组上全力以赴 , 比如 “-pc--gnu”(不要和 "-pc--msvc "混淆) 。经过测试 , 一共有176个三元组 。

C 不再是一种编程语言?

文章插图
> rustc --print target-listaarch64-apple-darwinaarch64-apple-iosaarch64-apple-ios-macabiaarch64-apple-ios-simaarch64-apple-tvos...armv7-unknown-linux-musleabiarmv7-unknown-linux-musleabihfarmv7-unknown-linux-uclibceabihf...x86_64-uwp-windows-gnux86_64-uwp-windows-msvcx86_64-wrs-vxworks>_
这实在是有太多ABI了,因为测试中甚至没有用到所有不同的调用约定,如 vs 或aapcs vs aapcs-vfp 。
但至少所有这些ABI和调用约定之类的东西,都可以一种方便使用的机器可读格式获得 。至少主流的C编译器在特定目标三元组的ABI上达成了一致! 当然有一些奇怪的jank C编译器,但Clang和GCC不是:
> abi-checker --tests ui128 --pairs clang_calls_gcc gcc_calls_clang...Test ui128::c::clang_calls_gcc::i128_val_in_0_perturbed_smallpassedTest ui128::c::clang_calls_gcc::i128_val_in_1_perturbed_smallpassedTest ui128::c::clang_calls_gcc::i128_val_in_2_perturbed_smallpassedTest ui128::c::clang_calls_gcc::i128_val_in_3_perturbed_smallpassedTest ui128::c::clang_calls_gcc::i128_val_in_0_perturbed_bigfailed!test 57 arg3 field 0 mismatchcaller: [30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 3A, 3B, 3C, 3D, 3E, 3F]callee: [38, 39, 3A, 3B, 3C, 3D, 3E, 3F, 40, 41, 42, 43, 44, 45, 46, 47]Test ui128::c::clang_calls_gcc::i128_val_in_1_perturbed_bigfailed!test 58 arg3 field 0 mismatchcaller: [30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 3A, 3B, 3C, 3D, 3E, 3F]callee: [38, 39, 3A, 3B, 3C, 3D, 3E, 3F, 40, 41, 42, 43, 44, 45, 46, 47]...392 passed, 60 failed, 0 completely failed, 8 skipped
上面是Aria在 20.04 x64上运行的FFI abi- , 她在这个相当重要的、表现良好的平台上测试了一些非常无聊的情况 。结果发现,一些整数参数在两个由Clang和GCC编译的静态库之间按值传递失败了!
Aria发现,Clang和GCC甚至不能就Linux x64上的ABI达成一致 。
Aria本来是为了检查rustc中的错误,没想到会在一个重要的、常用的ABI上发现两大主流C编译器的不一致 。
试图驯服C
Aria认为,可怕的是对C头文件进行语义解析,只能由该平台的C编译器来完成 。即使C编译器告诉了你类型和如何理解注释 , 但实际上你仍然不知道所有内容的大小/对齐/惯例 。那如何与这些乱七八糟的东西进行互操作呢?Aria提供了两种选择 。
第一个选择是完全投降,将你的语言与C进行灵魂绑定,这可以是以下任何一种:
但上面这些也只能让你走这么远 , 因为除非你的语言真的暴露了 long long,否则你将继承C的巨大可移植性混乱 。
这就让我们想到了第二个选择:撒谎、欺骗和偷窃 。
如果这一切是无论如何都无法避免的灾难,你还不如开始手工翻译类型和接口定义到你的语言中,基本上就是我们每天在Rust中所做的事情 。比如,人们使用rust-和自动化处理一些事,但很多时候,定义会被检查或手工调整 。因为人们不想浪费时间 , 去尝试的定制C构建系统可移植地工作 。
在Rust中,Linux x64上的是什么?
pub type intmax_t = i64;
在Nim中,Linux x64上的long long是什么?
clonglong {.importc: "long long", nodecl.} = int64
很多代码已经完全放弃将C保持在循环中,开始对核心类型的定义进行硬编码 。毕竟,它们显然只是平台ABI的一部分!他们要改变的大小吗?这显然是一个破坏ABI的变化!
那正在研究的又是什么?
我们讨论过为何不能被改变,因为如果我们从long long(64位整数)改为(128位整数),某个地方的二进制会失控使用错误的调用约定/返回约定 。但有没有一种方法,如果代码选择了它或其他东西 , 我们可以为较新的应用程序升级函数调用,而让旧应用程序保持不变?让我们编写一些代码 , 测试一下透明别名可以帮助ABI的想法 。
Aria提出了她的疑问:编程语言如何处理这种变化?如何指定与哪个版本的互操作?如果你有一些C头文件提到,它使用的是哪个定义?
在此讨论具有不同ABI的平台的主要机制是目标三元组 。你知道什么是目标三元组吗?你知道基本上涵盖了过去20年里所有主流桌面/服务器Linux发行版的--linux-gnu包括什么吗?现在,虽然表面上可以针对这个目标进行编译,并得到一个在所有这些平台上都能“正常工作”的二进制文件,但Aria不相信有些程序会被编译成大于 。
任何试图做出这种改变的平台都会成为一个新的--linux-gnu2目标三元组吗?如果任何针对--linux-gnu编译的东西都被允许在上面运行 , 这难道还不够吗?
在不破坏ABI的情况下更改签名
"那又怎样,C永远不会再有进步吗?"不!但也是!因为他们提供了糟糕的设计 。
老实说,进行ABI兼容的修改是一种艺术形式 。这种艺术的一部分就是准备工作 。具体来说,如果你准备好了 , 做出不破坏ABI的修改就会容易得多 。
正如的文章所指出的,像glibc(g是--linux-gnu中的gnu)早就明白了这一点,并使用符号版本化这样的机制来更新签名和API,同时为任何针对旧版本编译的人保留旧版本 。
因此,如果你有 (),你告诉编译器将其导出为,那么任何根据这个头文件进行编译的人,都会在他们的代码中写上,但针对链接 。
然后当你决定实际上应该使用时 , 你可以把 ()作为,但保留旧的定义作为。任何针对较新版本头文件进行编译的人都会高兴地使用v2符号 , 而针对旧版本进行编译的人则继续使用v1!
但是你仍然有一个兼容性的问题:任何用新头文件编译的人都不能与库的旧版本进行链接,库的V1版本根本没有V2符号!因此,如果你想获得热门的新功能,你就要接受与旧系统的不兼容 。
不过这并不是什么大问题,它只是让平台供应商感到难过 , 因为没有人能够立即使用他们花了这么多时间做的东西 。你不得不推出一个闪亮的新功能 , 然后让大家等待它变得足够普遍和成熟 。但为了人们愿意依赖它并中断对旧平台的支持(或者愿意为它实施动态检查和回退)时 , 你必须坐等几年 。
如果你真的想让人们立即升级,那就要谈论向前兼容的问题 。这让旧版本的东西以某种方式与他们没有概念的新功能一起工作 。
在不破坏ABI的情况下更改类型
那除了可以改变一个函数的签名,还可以改变类型布局吗?Aria表示,这取决于你是如何暴露类型的 。
C真正奇妙的一个特点是,它可以让你区分一个已知布局的类型和一个未知布局的类型 。如果你只在C头文件中前向声明一个类型,那么任何与之交互的用户代码都不被“允许”知道该类型的布局,并且必须一直在指针后面不透明地处理它 。
所以你可以做一个像* ()和(*)的API,然后使用同样的符号版本技巧来暴露和符号,任何时候你想改变这个布局,你就在所有与该类型交互的东西上增加版本 。类似地,你在、和一些类型定义中保留了一些 , 以确保人们使用“正确”的类型 。这样就可以在不同的版本之间改变类型的布局 。
如果多个东西建立在你的库之上,然后开始用不透明类型相互交谈,坏事就会发生:
如果lib1和lib2针对库的不同版本进行了编译,那么就会被输入到中!你有两个选择来处理这个问题:
1.说这是被禁止的,责备那些无论如何都要这么做的人 , 然后伤心
2.以一种向前兼容的方式设计,这样混合就可以了
常见的前向兼容技巧包括:
案例研究:
微软是这种向前兼容的大师 , 甚至可以实现在架构之间保持布局兼容 。Aria最近正在处理的一个例子是.h中的 。
这个API描述了一个有版本的值列表 。该列表以这种类型开始:
typedef struct _MINIDUMP_HANDLE_DATA_STREAM {ULONG32 SizeOfHeader;ULONG32 SizeOfDescriptor;ULONG32 NumberOfDescriptors;ULONG32 Reserved;} MINIDUMP_HANDLE_DATA_STREAM, *PMINIDUMP_HANDLE_DATA_STREAM;
其中:
而事实上 , 微软实际上有理由使用这种版本方案,并定义了两个版本的数组元素:
typedef struct _MINIDUMP_HANDLE_DESCRIPTOR {ULONG64 Handle;RVA TypeNameRva;RVA ObjectNameRva;ULONG32 Attributes;ULONG32 GrantedAccess;ULONG32 HandleCount;ULONG32 PointerCount;} MINIDUMP_HANDLE_DESCRIPTOR, *PMINIDUMP_HANDLE_DESCRIPTOR;typedef struct _MINIDUMP_HANDLE_DESCRIPTOR_2 {ULONG64 Handle;RVA TypeNameRva;RVA ObjectNameRva;ULONG32 Attributes;ULONG32 GrantedAccess;ULONG32 HandleCount;ULONG32 PointerCount;RVA ObjectInfoRva;ULONG32 Reserved0;} MINIDUMP_HANDLE_DESCRIPTOR_2, *PMINIDUMP_HANDLE_DESCRIPTOR_2;// The latest MINIDUMP_HANDLE_DESCRIPTOR definition.typedef MINIDUMP_HANDLE_DESCRIPTOR_2 MINIDUMP_HANDLE_DESCRIPTOR_N;typedef MINIDUMP_HANDLE_DESCRIPTOR_N *PMINIDUMP_HANDLE_DESCRIPTOR_N;
这些结构的实际细节不是很有趣,除了:
这是一个坚不可摧的向前兼容的庞然大物 。它们对填充非常小心,它甚至在32位和64位之间有相同的布局 (这实际上是非常重要的,因为你希望一个架构上的处理器能够处理来自每个架构的) 。
案例研究:
Aria对这种情况不是很熟悉,但在研究历史上的glibc中断时,她在LWN上看到了一篇很棒的文章:《glibc s390 ABI中断》,她假设它是准确的 。
事实证明 , glibc曾经破解过类型的ABI,至少在s390上 。根据这篇文章的描述,它是混乱的 。
特别是他们改变了/使用的保存状态类型的布局,即 。现在,他们知道这是一个破坏ABI的变化,所以他们做了负责任的符号版本化的事情 。
但并不是一个不透明的类型,其他东西都在内联地存储这个类型的实例,比如Perl的运行时间 。不用说,这个相对晦涩的类型已经渗透到许多二进制文件中去了,最终的结论是 , 的所有东西都需要重新编译!
这篇文章甚至讨论了将libc版本升级以应对这种情况的可能性:
在像这样的混合ABI环境中,SO名称碰撞导致两个libc被加载并争夺相同的符号命名空间,而解析(以及因此选择ABI)则由ELF插值和范围规则决定 。这真是一场噩梦 。这可能是一个比告诉大家重建并继续生活更糟糕的解决方案 。
真的能改变吗?
在Aria看来,不完全是 。就像一样,它不是一个不透明的类型,这意味着它被内联到大量的随机结构中,被认为具有大量其他语言和编译器的特定表示,并且可能是大量公共接口的一部分 。而这些接口并不在libc、Linux,甚至不在发行版维护者的控制之下 。
当然,libc可以适当地使用符号版本技巧来使其API与新的定义兼容,但改变像这样的基本数据类型的大?。窃谝桓銎教ǖ拇笊低持醒扒蠡炻?。
Aria希望被证明自己是错误的,但据她所知,做出这样的改变需要一个新的目标三元组 , 并且不允许任何为旧ABI构建的二进制/库在这个新三元组上运行 。当然有人可以做这些工作,但Aria并不羡慕任何这样做的发行版 。
即使如此,面临的还有x64的int问题:这是一个非常基本的类型 , 而且长期以来一直是这种大小 , 无数的应用程序可能对它有奇怪的无法察觉的假设 。这就是为什么int在x64上是32位的,尽管它应该是64位的:int是32位的时间太长了,以至于完全无望将软件更新到新的大?。?尽管它是一个全新的架构和目标三元组 。
Aria再次希望自己是错的,但是人们有时犯的错误如此严重,以至于根本无法挽回 。如果C语言是一种独立的编程语言?当然可以去做 。但它不是 , 它是一个协议,还是我们必须使用的糟糕的协议 。
就算C征服了世界,但也许它再也得不到好东西了 。
???????????????? END ???????????????
关注我的微信公众号,回复“加群”按规则加入技术交流群 。
点击下面图片,有星球具体介绍,新用户有新人优惠券,老用户半价优惠,期待大家一起学习一起进步 。
【C 不再是一种编程语言?】点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看 。