【问题标题】:Linux Kernel's __is_constexpr MacroLinux 内核的 __is_constexpr 宏
【发布时间】:2018-03-25 21:40:04
【问题描述】:

Linux 内核的__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 <Martin.Uecker@med.uni-goettingen.de>
 */
#define __is_constexpr(x) \
        (sizeof(int) == sizeof(*(8 ? ((void *)((long)(x) * 0l)) : (int *)8)))

如需讨论解决同一问题的不同方法,请参阅:Detecting Integer Constant Expressions in Macros

【问题讨论】:

  • 我不认为sizeof(void)是标准不允许的常识......相关段落是§6.5.3.4 ¶ 1void是一个不完整的类型)和§6.2.5 ¶ 19(没有sizeof 的类型不完整,这应该很明显)。
  • 您没有明确指出“解释用于测试整数常量表达式的宏”是your own recent question。这是任何实际用途,还是学术口香糖?很好的回答你自己的问题。
  • @WeatherVane:谢谢!几天前在 LKML 中讨论了这个宏,作为解决内核中 VLA 问题的可能解决方案,因此很明显它有实际用途——特别是如果它最终出现在内核树中。
  • 我同意 Linus 的意见。而且由于这个宏也是特定于编译器的,为什么不使用一个清晰​​的内置函数呢?
  • 不知道怎么有人觉得这个问题不清楚

标签: c linux-kernel macros language-lawyer


【解决方案1】:

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 extensionsizeof(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 &gt;= 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 代码。

【讨论】:

  • 在这里引用标准并没有真正的帮助,因为行为包含实现定义的和可能(我没有心情验证)未定义的行为。这只是糟糕的编码风格。
  • @Olaf:我不确定我是否遵循您的逻辑。实现定义的、未指定的和未定义的行为是标准本身定义和使用的概念——因此引用标准实际上是了解某事物是实现定义的、未定义的还是其他事物的正确方法。在任何情况下,sizeof(void) 都不是有效的 C 程序,因为它违反了约束(6.2.5.19、6.5.3.4.1)。至于编码风格,它不可能是“坏的”,因为在 C89 中没有任何众所周知的“好的”可移植方式(如果有的话!)。
  • “没有任何众所周知的“好的”便携方式”——这正是我的观点。如果你坚持使用 IDB/UB(后者只是致力于特定的实现):K.I.S.S.
  • @Olaf:如果没有其他方法,就无法“保持简单”。无论如何,这个问题是关于解释上面的宏作为对链接问题的补充——所以请参考那个问题来讨论或提出更好的方法来实现这一点。
猜你喜欢
  • 2017-03-02
  • 2021-12-30
  • 2013-03-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-02-11
  • 2011-12-04
相关资源
最近更新 更多