Linux内核的__is_constexpr宏

Linux Kernel的__is_constexpr(x)宏如何工作? 它的目的是什么? 什么时候介绍? 为什么要介绍?

 /* * This returns a constant expression while determining if an argument is * a constant expression, most importantly without evaluating the argument. * Glory to Martin Uecker  */ #define __is_constexpr(x) \ (sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8))) 

有关解决相同问题的不同方法讨论 ,请参阅: 检测宏中的整数常量表达式

Linux内核的__is_constexpr

介绍

__is_constexpr(x)宏可以在Linux内核的include / kernel / kernel.h中找到 :

 /* * This returns a constant expression while determining if an argument is * a constant expression, most importantly without evaluating the argument. * Glory to Martin Uecker  */ #define __is_constexpr(x) \ (sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8))) 

它是在Linux Kernel v4.17的合并窗口中引入的,在2018-04-05 提交3c8ba0d61d04 ; 虽然围绕它的讨论开始于一个月前。

该宏值得注意的是利用C标准的细微细节: 条件运算符确定其返回类型的规则(6.5.15.6)和空指针常量的定义(6.3.2.3.3)。

此外,它依赖于允许的sizeof(void) (并且与sizeof(int) ),这是GNU C扩展 。


它是如何工作的?

宏的身体是:

 (sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8))) 

让我们关注这一部分:

 ((void *)((long)(x) * 0l)) 

注意: (long)(x) u64 旨在允许x具有指针类型并避免在32位平台上对u64类型发出警告。 但是,这个细节对于理解宏的关键点并不重要。

如果x 整数常量表达式 (6.6.6),那么((long)(x) * 0l)是值0整数常量表达式 。 因此, (void *)((long)(x) * 0l)空指针常量 (6.3.2.3.3):

值为0的整型常量表达式或类型为void *的表达式称为空指针常量

如果x 不是 整数常量表达式 ,则(void *)((long)(x) * 0l)不是空指针常量无论其值如何

知道了,我们可以看到之后会发生什么:

 8 ? ((void *)((long)(x) * 0l)) : (int *)8 

注意:第二个8字面旨在避免编译器警告有关创建指向未对齐地址的指针。 8文字可能只是1 但是,这些细节对于理解宏的关键点并不重要。

这里的关键是条件运算符返回一个不同的类型,具体取决于其中一个操作数是否为空指针常量 (6.5.15.6):

[…]如果一个操作数是空指针常量,则结果具有另一个操作数的类型; 否则,一个操作数是指向void的指针或void的限定版本,在这种情况下,结果类型是指向适当限定版本的void的指针。

因此,如果x 一个整型常量表达式 ,那么第二个操作数是空指针常量 ,因此表达式的类型是第三个操作数的类型,它是指向int的指针。

否则 ,第二个操作数是指向void的指针,因此表达式的类型是指向void的指针。

因此,我们最终有两种可能性:

 sizeof(int) == sizeof(*((int *) (NULL))) // if `x` was an integer constant expression sizeof(int) == sizeof(*((void *)(....))) // otherwise 

根据GNU C扩展 , sizeof(void) == 1 。 因此,如果x整数常量表达式 ,则宏的结果为1 ; 否则, 0

此外,由于我们只是比较两个sizeof表达式的相等性,结果本身是另一个整数常量表达式 (6.6.3,6.6.6):

常量表达式不应包含赋值,递增,递减,函数调用或逗号运算符,除非它们包含在未评估的子表达式中。

整数常量表达式应具有整数类型,并且只能具有整数常量的操作数,枚举常量,字符常量,结果为整数常量的sizeof表达式,以及作为强制转换的直接操作数的浮点常量。 整数常量表达式中的转换运算符只能将算术类型转换为整数类型,除非作为sizeof运算符的操作数的一部分。

因此,总之,如果参数是整数常量表达式, __is_constexpr(x)宏将返回值为1的整数常量表达式。 否则,它返回值为0整数常量表达式


为什么要介绍?

在从Linux内核中删除所有可变长度数组(VLA)的 过程中,宏出现了。

为了促进这一点,需要在内核范围内启用GCC的-Wvla警告 ; 以便编译器标记所有VLA实例。

当启用警告时,结果发现GCC报告了许多arrays为VLA的情况,而这些情况并非如此。 例如在fs / btrfs / tree-checker.c中 :

 #define BTRFS_NAME_LEN 255 #define XATTR_NAME_MAX 255 char namebuf[max(BTRFS_NAME_LEN, XATTR_NAME_MAX)]; 

开发人员可能希望max(BTRFS_NAME_LEN, XATTR_NAME_MAX)已解析为255 ,因此应将其视为标准数组(即非VLA)。 但是,这取决于max(x, y)宏扩展到什么。

关键问题是如果数组的大小不是C标准定义的(整数)常量表达式 ,GCC会生成VLA代码。 例如:

 #define not_really_constexpr ((void)0, 100) int a[not_really_constexpr]; 

根据C90标准, ((void)0, 100) 0,100 ((void)0, 100) 不是 常数表达式 (6.6),因为使用了逗号运算符 (6.6.3)。 在这种情况下,GCC选择发出VLA代码, 即使它知道大小是编译时常量 。 相比之下,Clang没有。

由于内核中的max(x, y)宏不是常量表达式,因此GCC触发了警告并生成了内核开发人员不想要的VLA代码。

因此,一些内核开发人员试图开发max和其他宏的替代版本以避免警告和VLA代码。 一些尝试试图利用GCC的__builtin_constant_p内置 ,但没有一种方法适用于当时内核支持的所有GCC版本( gcc >= 4.4 )。

在某些时候,Martin Uecker 提出了一种特别聪明的方法,它不使用内置函数 (从glibc的tgmath.h中 获取灵感 ):

 #define ICE_P(x) (sizeof(int) == sizeof(*(1 ? ((void*)((x) * 0l)) : (int*)1))) 

虽然该方法使用GCC扩展, 但它仍然很受欢迎 ,并被用作__is_constexpr(x)宏背后的关键思想,它在与其他开发人员进行几次迭代后出现在内核中。 然后使用该宏来实现max宏和其他需要为常量表达式的宏,以避免GCC生成VLA代码。