Linux 内核的__is_constexpr 宏
简介
__is_constexpr(x) 宏可以在 Linux Kernel 的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 <Martin.Uecker@med.uni-goettingen.de>
*/
#define __is_constexpr(x) \
(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
它是在 2018-04-05 的 Linux Kernel v4.17 合并窗口期间引入的,commit 3c8ba0d61d04;尽管围绕它的讨论是在一个月前开始的。
该宏以利用 C 标准的微妙细节而著称:条件运算符确定其返回类型的规则 (6.5.15.6) 和 null 的定义指针常量 (6.3.2.3.3)。
此外,它依赖于sizeof(void) 被允许(与sizeof(int) 不同),即GNU C extension。
它是如何工作的?
宏的主体是:
(sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))
让我们重点关注这部分:
((void *)((long)(x) * 0l))
注意:(long)(x) 转换为 intended 以允许 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 文字是 intended 以避免编译器警告有关创建指向未对齐地址的指针。第一个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 extension、sizeof(void) == 1。因此,如果x 是一个整数常量表达式,那么宏的结果就是1;否则,0。
此外,由于我们只比较两个sizeof 表达式是否相等,结果本身就是另一个整数常量表达式(6.6.3, 6.6.6):
常量表达式不应包含赋值、递增、递减、函数调用或逗号运算符,除非它们包含在未计算的子表达式中。
整型常量表达式应为整型,且操作数应为整型常量、枚举常量、字符常量、sizeof结果为整型常量的表达式,以及作为强制转换的直接操作数的浮动常量。整数常量表达式中的强制转换运算符只能将算术类型转换为整数类型,但作为 sizeof 运算符的操作数的一部分除外。
因此,总而言之,如果参数是整数常量表达式,__is_constexpr(x) 宏将返回值为1 的整数常量表达式。否则,它返回值为0 的整数常量表达式。
为什么要引入它?
宏变成了during the effort,用于从Linux内核中删除所有Variable Length Arrays (VLAs)。
为了方便,最好在内核范围内启用GCC's -Wvla warning;以便编译器标记所有 VLA 实例。
当警告被启用时,事实证明 GCC 报告了许多数组是 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) 不是 常量表达式 (6.6),因为使用了 逗号运算符 ( 6.6.3)。在这种情况下,GCC 选择发布 VLA 代码even when it knows the size is a compile-time constant。相比之下,Clang 不会。
由于内核中的 max(x, y) 宏不是常量表达式,GCC 会触发警告并在内核开发人员不希望的地方生成 VLA 代码。
因此,一些内核开发人员尝试开发 max 和其他宏的替代版本以避免警告和 VLA 代码。一些尝试尝试利用 GCC's __builtin_constant_p builtin,但没有一种方法适用于内核当时支持的所有 GCC 版本 (gcc >= 4.4)。
在某些时候,Martin Uecker proposed 是一种特别聪明的方法,它不使用内置函数(taking inspiration 来自 glibc's tgmath.h):
#define ICE_P(x) (sizeof(int) == sizeof(*(1 ? ((void*)((x) * 0l)) : (int*)1)))
虽然该方法使用 GCC 扩展,it was nevertheless well-received 并被用作 __is_constexpr(x) 宏背后的关键思想,该宏在与其他开发人员进行了几次迭代后出现在内核中。然后使用该宏来实现max 宏和其他需要为常量表达式的宏,以避免 GCC 生成 VLA 代码。