c语言 - 内核的"container_of" - 有什么方法可以使其符合 ISO 标准吗?



在查看Linux内核的双链接循环列表实现时,我发现了以下宏:

#define container_of(ptr, type, member) ({           
const typeof( ((type *)0)->member ) *__mptr = (ptr); 
(type *)( (char *)__mptr - offsetof(type,member) );})

它的工作方式是,它返回指向只给定其成员之一地址的结构的指针:

struct blabla
{
int value;
struct list_head *list;
}

这样,只要只给列表一个指针,就可以得到指向blabla的指针(并得到"value")。对于我的问题,我该如何使其尽可能便携(最好符合C89/C99?)。由于使用了typeof(),这只是gcc。

这就是我目前所掌握的:

#define container_of(ptr, type, member) (                  
(type *) (char *)(ptr)-offsetof(type,member)
)

这个片段是否符合ISO标准(因此应该能够在任何符合标准的编译器上编译)?

正如Ouah所评论的,({ ... })语句表达式是GNU扩展;你将无法使用它。您的核心表达式接近所需内容,但没有足够的括号:

#define container_of(ptr, type, member) 
((type *) ((char *)(ptr) - offsetof(type, member)))

在我看来,这是清白的。它只分布在SO的两条线上。

宏是按照对ptr执行类型检查的方式编写的。如果编译器与gcc不兼容,可以使用复合文字而不是语句表达式,并返回到简单的指针检查,而不是使用__typeof__

#ifdef __GNUC__
#define member_type(type, member) __typeof__ (((type *)0)->member)
#else
#define member_type(type, member) const void
#endif
#define container_of(ptr, type, member) ((type *)( 
(char *)(member_type(type, member) *){ ptr } - offsetof(type, member)))

ISO C90兼容版本,带类型检查。(但是,注意:ptr的两个评估!)

#define container_of(ptr, type, member) 
((type *) ((char *) (ptr) - offsetof(type, member) + 
(&((type *) 0)->member == (ptr)) * 0))
struct container {
int dummy;
int memb;
};

#include <stddef.h>
#include <stdio.h>
int main()
{
struct container c;
int *p = &c.memb;
double *q = (double *) p;
struct container *pc = container_of(p, struct container, memb);
struct container *qc = container_of(q, struct container, memb);
return 0;
}

测试:

$ gcc -Wall containerof.c
containerof.c: In function ‘main’:
containerof.c:20:26: warning: comparison of distinct pointer types lacks a cast
containerof.c:20:21: warning: unused variable ‘qc’
containerof.c:19:21: warning: unused variable ‘pc’

我们得到的distinct pointer types警告是26,但不是25。这是我们对指针被滥用的诊断。

我首先尝试将类型检查放在逗号运算符的左侧,gcc抱怨说这没有效果,这很麻烦。但是,通过将其作为操作数,我们可以确保它得到使用。

&((type *) 0)->member技巧在ISO C中没有得到很好的定义,但它被广泛用于定义offsetof。如果编译器对offsetof使用这种空指针技巧,那么它几乎肯定会在自己的宏中表现出来。

是的,您可以制作"container_ of";宏严格符合ISO C。要做到这一点,你需要两件事:

  1. 去掉GNU扩展;

  2. 找到检查类型兼容性的方法。

基本上,类型检查不是运行时操作,而是编译时操作。而且我看不出有什么原因,为什么原创的"container_ of";实现创建新变量只是为了分配它并执行类型检查。这可以在不在某些表达式中创建新变量的情况下完成,这些表达式仅在编译时计算(并检查类型)。幸运的是,我们在C中没有太多的选择,唯一的选择是使用"sizeof(表达式)";以检查类型。参见示例:

#define container_of(ptr, type, member) 
( (void)sizeof(0 ? (ptr) : &((type *)0)->member), 
(type *)((char*)(ptr) - offsetof(type, member)) )

在第一行中,检查类型的兼容性(对于三元运算符,编译器必须确保类型可以转换为通用类型,或者两种类型都兼容)。第二行与原来的";container_ of";宏。

你可以在GodBolt上玩测试程序(https://godbolt.org/z/MncvzWfYn)并确保这种符合ISO的变体即使在微软的Visual Studio编译器中也能工作。

PS:过了一段时间,我发现以下变体可以更好:

#define CONTAINER_OF(ptr, type, member) 
( (void)sizeof(0 ? (ptr) : &((type*)0)->member), 
(typeof(_Generic((typeof(ptr))0, const typeof(*(typeof(ptr))0)*: (const type*)0, default: (type*)0))) 
((uintptr_t)(const void*)(ptr) - offsetof(type, member)) )

不同之处在于,它保留了ptr中的const限定符,并将其分配给结果,例如:

  • 如果ptr参数是const struct *指针,则无论type是否为常量,结果都将具有const type *类型
  • 如果ptr参数是非常量指针(struct*),则结果将具有类型type*,根据type参数的类型,该类型可以是常量或非常量

因此,当指向某个结构的常量指针通过container_of宏转换为非常量指针时,保留const限定符可以降低出错的可能性。

不幸的是,对于C标准的早期版本,此版本需要C23或非标准typeof()运算符。

与从Linux内核实现相反,ISO兼容container_of宏的另一个原因是后者使用";语句表达式";GCC扩展,在参数ptr是临时变量的情况下工作不好。当container_of宏应用于函数调用的结果(这里假设container_of(func().x, struct y, m)func()返回一个结构,其中x是一个结构数组)或应用于复合语句(container_of((&(struct S){...}), struct B, m))时,可能会发生后者。在这两种情况下,对从linux借用的container_of宏的调用都会导致一个悬空指针发生这种情况是因为作为ptr参数传递的临时对象将在第一个分号之后(在container_of宏的Linux实现的第一行)被销毁,并且因为由复合语句表达式创建的变量将在最近的块的末尾被销毁,该块是";语句表达式";它本身符合ISO的container_of宏的实现没有这样的问题。

相关内容

最新更新