一种特殊的地狱:C和C ++中的intmax_t

2020-12-06 03:54:35

C和C ++作为语言有一些东西使它们彼此分开,主要是在它们的详细信息上,偶尔还有较大的功能集,例如指定的初始化程序。但是,令人烦恼的是,大量的C ++可以简单地完成C的工作,远胜于C,包括解决C和C ++的发展面临的一些最大问题时。

让我们来解决一个同时困扰C和C ++的当代问题,它影响了从标准库维护人员到项目开发人员的所有人,过去20年来一直如此:intmax_t。

intmax_t背后的概念很简单:它是您的实现及其标准库共同支持的最大整数类型。这是实现内的intmax_t控件的一些内容:

它是可移植打印的最大位数,例如用printf("%j&#34 ;,(intmax_t)value)(C和C ++);

intmax_t是std :: numeric_limits适用的最大类型,包括直到该类型并包括该类型的大多数类型(仅C ++);

intmax_t支持std :: chrono的强制转换和类似操作(例如,在系统内外转换期间不会丢失任何信息)(仅适用于C ++);

并且,标准库提供了一组整数运算(如绝对值和商/余数运算),可以用实现中可用的最大位精度来完成(C和C ++)。

这些属性构成了intmax_t目的的基础。依靠这种类型的隐式契约,可以实现无损存储,传递操作等。由于它是类型定义,因此它下面的“实数”整数类型可以换出,依赖它的人们可以无缝升级!

C对不破坏旧代码并保持“开发人员靠近机器”的承诺更高。对于大多数应用程序二进制接口,这实际上是非常简单的“名称修改”方案(即,没有),弱链接程序和其他恶作剧。最终结果是,我们使C开发人员暴露于平台细节,这些细节成为其代码的不可见依赖项,必须不惜一切代价保留它们。例如,让我们来看一个使用intmax_t,imaxabs的C标准函数:

并且,让我们尝试找出如何在不严重破坏其代码的情况下将某人升级为这种用法。我们将尝试在C和C ++中修复此问题。

以intmax_t为例,我们不费吹灰之力地进行使用:调用具有intmax_t输入和返回类型的函数。语法和用法最终看起来像这样:

#include< inttypes.h> int main(){intmax_t original =(intmax_t)-2; intmax_t val = imaxabs(val); return(int)val; }

很简单!但是,根据代码的编译方式,这里也存在隐藏的依赖关系。虽然许多人将其C标准库编译为静态库,并且仅生成最终二进制代码以用作拥有“自包含”二进制文件的代码,但绝大多数共享生态系统都依赖于共享库/动态链接库来实现。标准。这意味着,在程序启动时通过操作系统铣削程序时,“加载程序”将运行以在某些系统库中找到符号imaxabs(例如,/lib/x86_64-linux-gnu/libc-2.27.so “ amd64”系统)。足够无害了吧?嗯,实际上这是一个问题,因为imaxabs就是C语言中用来弄清楚在某些共享库中要使用的子例程的名称,

glibc维护者决定,只要大多数平台支持intmax_t,它们就将从长期更改为__int256_t,因为大多数平台都支持它,并且有很多客户要求它。

他们将libc升级到适用于各种Linux发行版的下一个版本,并且每个人都在寻找默认的libc时链接到它。

您有一个应用程序。您的代码未更改或更新,因此未重新编译。它称为imaxabs。它传递的参数很长,因为这是您上次编译和交付软件时的类型。

用于查找要调用的函数的imaxabs在新libc中查找采用__int256_t的版本。

libc二进制文件中使用了不同的寄存器来传递和返回imaxabs函数调用所期望的函数值,这是因为您的应用程序处于long long模式,但是glibc期望使用__int256_t。

这是所谓的“应用程序二进制接口(ABI)中断”的体现之一。通常,ABI中断是在程序运行时发生的不可检测的无提示中断,它会完全破坏您的程序对该功能的依赖性,以确保准确性。它通常在以下情况发生:细微的细节-用于在共享库及其应用程序之间协商较大整数值的寄存器,结构在特定版本上可能具有的填充量,类成员的排序和布局,位的解释即使类型的布局或传递约定从不改变,甚至更大。

不必要。 C是一种简单的语言,它既可以推销自己,也可以以此为荣。如此之多,以至于它甚至是该语言滚动宪章的一部分。几乎没有名称混乱,因为没有重载。如果您需要“虚拟功能”,则需要手工制作虚拟表结构并自己进行初始化。几乎没有任何查找或实体协商:您所写的内容(无论多么令人恐惧或被诅咒)是您所获得的一般意义上的东西。 (不,这不是“便携式程序集”。编译器将C代码拆开,使其比人们所填充的代码效率更高。它甚至不再是计算机的直接模型:仅仅是一个抽象模型。)

尽管如此,有时甚至C也无法摆脱它。函数imaxabs恰好与一个实体有关,由于历史原因,该实体被固定到采用并返回long long的函数上。升级意味着要处理用户期望的值(升级后的intmax_t并可以打印__int128_t / __int256_t)与保持旧不变式(长long,64位数字)的旧的未重新编译的代码之间的这种分裂。

好的,因此可以在导致ABI中断的库版本之间重新使用符号。用C语言捍卫这样一个世界的方式是什么?

…作为提供imaxabs功能的一种方式。它有点像手工的,手工制作的,自由范围的和自然的ABI版本控制(或者,正如我亲切地称呼它:个人受虐狂以弥补语言失败)。这通常可以工作,直到……不行!

这是C标准中的“保证ABI中断”部分。它的真名是“第7.1.4节库函数的使用”。下面转载的是谴责我们的相关文章,重点是我的:

标头中声明的任何函数都可以另外实现为标头中定义的类函数宏,因此,如果在包含标头时显式声明了库函数,则可以使用以下所示的一种技术来确保不声明该函数受这样的宏影响。通过在函数名称中用括号括起来,可以在本地抑制函数的任何宏定义,因为该名称之后不带有表示宏函数名称扩展的左括号。出于相同的语法原因,即使将库函数也定义为宏,也可以采用该函数的地址。使用#undef删除任何宏定义也将确保引用实际函数。

用户不仅可以通过使用< windows.h>上的相同技巧来抑制类函数的宏调用。类似于(max)(value0,value1),但C标准库允许它们也取消定义函数名称:

//用户代码:main.c #include< inttypes.h> #undef imaxabs // awh geez int main(){intmax_t val =-1; intmax_t absval = imaxabs(val); // awH GEEZ return(int)absval; }

好吧,C标准基本上可以装载双枪管,并将我们唯一的标准化缓解策略带回到谷仓后面。还剩什么?好吧,针对实施的精神错乱,那就是:

这是伪代码。但是,您不相信吗,某些实现实际上可以执行与此类似的操作来解决这些问题!他们所做的事情涉及更多,例如实际下降到链接器级别并创建符号映射和其他非常痛苦的解决方法。 sed脚本,awk脚本和bash开始问世,人们正在做大量文本处理以获取符号名称并将其与版本化的符号名称匹配...

尽管如此,考虑到混乱,它确实使我们摆脱了问题。在C代码中,您可以按我们的主人和救主的意图使用“真实名称” imaxabs,将二进制文件链接到___glibc228_imaxabs,每个人都很高兴。这种修复方法只有一个问题……

QoI非常适合纯粹的理论标准。我们开始用C标准编写性感的叙述,并将其称为“推荐做法”,用小脚注暗示着更美好的世界,同时诱人地为我们所在地区的年轻新手吸引着眉毛。快来吧,它会很棒,我们将获得很多乐趣,只需在那儿执行那个可爱的小实现,您就可以取得如此出色的进步,尽情享受并很快回来,我那纯洁的软件工程师! 〜

然后,您会在小巷中醒来,抢走您的零钱包,并且违反了前提条件。您的头脑从未指明的行为中冒出来,事实证明该实现:

从size_t中学到了0课,要求整个行业为64位设置不同的二进制文件,以更改其定义;

伤害,困惑,您来找我们标准委员会。您告诉我们,您感到困惑不解,这个名为TenDRA的出色实现说您可以做这些令人惊奇的事情,可以看到符合标准的爆炸和烟火,而且值得推荐……

我们向您解释,TenDRA所做的并非违法。不,您不会自动访问他们的专有脚注。我们必须为实际上并不想执行推荐做法的实施提供服务,为什么您不查看实施的文档,为什么不联系维护者呢?真的,这里我们无能为力,您可以自由选择,为什么不选择更好的实施方案呢?如果您的想法真的很流行,为什么没有按照您的期望来实现它们,是吧?为什么不将TenDRA推向左侧-

作为管理机构,WG14将认真对待建议的实施方案。我们还将认真对待DeathStation 9000。毕竟,这是合规的。这不是实现的错:

为了拼命地为自己制定规则并确保永远用C语言编写的所有代码永远继续编译,我们竭尽全力,成为标准委员会。对于intmax_t和printf中的ABI问题,我们感到沮丧,但老实说,就是我们要求这样做。

该错误报告将发布到C和C ++实现中,这些古怪的补丁程序开始了。为什么__int128_t是pariah,即将出现的__int256_t支持在哪里,为什么我们不能将__int512_t与numeric_limits一起使用,为什么_ExtInt(1024)不是“真正的扩展整数类型”,等等?我们想归咎于ABI,但这是我们自己的错:我们设计了一个世界,其中名字永远存在,抽象是邪恶的,宏是坏的,现在我们为链接C世界的复杂性而制定的规则现在邪恶的笑容使我们凝视在脸上。我们重视简单性,以至于自我模仿,并在某种程度上自我毁灭。

对于C?没有。我什至没有开玩笑:在这个主题上,不少于5篇论文在3-4个标准会议上讨论过。没有ISO标准C功能的组合可以解决此问题而又不会破坏别人。甚至C语言中即将出现的关于lambdas和auto的想法(请参见§6.5.2.6Lambda表达式)也无法解决此问题:

int main(){typedef(intmax_t)(f_t)(intmax_t); f_t * f_ptr0 = imaxabs; //好,函数指针衰减f_t * f_ptr1 =& imaxabs; // oops返回0; }

通过lambda的定义方式,它返回对象指针,而不是函数指针。这意味着使用此语法的旧代码已损坏。这也使人怀疑该函数指针的地址是什么?需要将一个功能设计为“ lambda”,但您可以控制“操作者的地址”返回的内容。当然,只要写上这句话,一万名C开发人员就会大喊:“不,C必须简单,操作员重载是DEVIL的缺点!”

今天,C ++可以根据自己的库规则解决其ABI问题。我们只需要挖掘一点辛辣的味道:

命名空间std {extern" C" __int128_t __i128abs(__int128_t v2)noexcept;使用intmax_t = __int128_t; struct __imaxabs {private:using __imaxabs_ptr = intmax_t(*)(intmax_t)noexcept; public:constexpr __imaxabs_ptr运算符& ()const noexcept {return& __i128abs; } constexpr运算符__imaxabs_ptr()const noexcept {return& __i128abs; };内联constexpr const __imaxabs imaxabs = __imaxabs {}; }

不幸的是,我们将C ++本身与C兼容性联系在一起。这意味着我们将永远不会采取这样的步骤,因为这意味着您系统上C库对intmax_t的定义将与C ++库不同。避免使用shenanigans等。请注意,由于以下措辞,C ++仍然可以做到这一点:

C ++标准库还提供了经过适当调整以确保静态类型安全的C标准库的设施。— C ++工作草案,§16.2C标准库([library.c / 1])

就像我们可以使用上面的方法来避免保持一致一样,实现非常希望imaxabs(value)与std :: imaxabs(value)具有完全相同的含义。所以,是的,也许是……

当涉及到与C相同的功能集时,我们明确地不会超越极限或超出我们的限制。我们将自豪地装载脚枪,从根本上摆脱困境,然后迫使好心的开发人员陷入实施定义的地狱之火。兼容性是一项功能,绝对值得!

实际解决此问题的唯一方法是定义自己的C库实现,该实现从实现中获得了积极的符号版本支持,并投资了向前兼容性。老实说,在这个时代,谁能做到如此疯狂?