硬汉嵌入式论坛

 找回密码
 立即注册
查看: 1815|回复: 11
收起左侧

[编程语言] C语言的一点高级用法

  [复制链接]

27

主题

95

回帖

181

积分

初级会员

积分
181
发表于 2022-11-8 12:35:22 | 显示全部楼层 |阅读模式
在LINUX中内核中有很多C语言的奇技淫巧,比如container_of()这个宏, 在很多地方会用到,比如在定义宏to_i2c_driver, to_spi_driver时都用到这个,有些就直接在函数中调用 container_of宏。

1. 作用
这个宏的作用就是通过一个结构体的某个成员的指针,反推出这个结构体的首地址。
这种操作非常有用,尤其在C语言中写面向对象的程序时,相当模拟了C++多态的效果。

以LINUX内核中I2C子系统为例, i2c驱动结构体如下:
[C] 纯文本查看 复制代码
struct i2c_driver {

	unsigned int class;



	/* Standard driver model interfaces */

	int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);

	void (*remove)(struct i2c_client *client);

	/* New driver model interface to aid the seamless removal of the

	 * current probe()'s, more commonly unused than used second parameter.

	 */

	int (*probe_new)(struct i2c_client *client);


	/* driver model interfaces that don't relate to enumeration  */

	void (*shutdown)(struct i2c_client *client);


	/* Alert callback, for example for the SMBus alert protocol.

	 * The format and meaning of the data value depends on the protocol.

	 * For the SMBus alert protocol, there is a single bit of data passed

	 * as the alert response's low bit ("event flag").

	 * For the SMBus Host Notify protocol, the data corresponds to the

	 * 16-bit payload data reported by the slave device acting as master.

	 */

	void (*alert)(struct i2c_client *client, enum i2c_alert_protocol protocol,

		      unsigned int data);


	/* a ioctl like command that can be used to perform specific functions

	 * with the device.

	 */

	int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);

[color=#ff0000]	struct device_driver driver;[/color]

	const struct i2c_device_id *id_table;


	/* Device detection callback for automatic device creation */

	int (*detect)(struct i2c_client *client, struct i2c_board_info *info);

	const unsigned short *address_list;

	struct list_head clients;

	u32 flags;

};


在struct i2c_driver 中有一个成员struct device_driver driver, 这相当于“is-a”的关系,即 i2c_driver 是一种 device_driver. 这种操作实际就是继承。

紧接着定义了一个i2c专用宏:
[C] 纯文本查看 复制代码
#define to_i2c_driver(d) container_of(d, struct i2c_driver, driver)


在i2c_device_match函数中使用这个宏:

[C] 纯文本查看 复制代码
static int i2c_device_match(struct device *dev, struct device_driver *drv)

{

	struct i2c_client	*client = i2c_verify_client(dev);

	struct i2c_driver	*driver;





	/* Attempt an OF style match */

	if (i2c_of_match_device(drv->of_match_table, client))

		return 1;



	/* Then ACPI style match */

	if (acpi_driver_match_device(dev, drv))

		return 1;

[color=#ff0000]	driver = to_i2c_driver(drv);[/color]


	/* Finally an I2C match */

	if (i2c_match_id(driver->id_table, client))

		return 1;


	return 0;

}


在此函数中会调用driver = to_i2c_driver(drv); 相当于:driver = container_of(drv, struct i2c_driver, driver)

注意在传入函数的参数中有一个参数是struct device_driver 类型的引用( *drv ),通过这个宏就可以通过drv的地址
反推出drv所在结构体struct i2c_driver 对象driver 的首地址,从而可以通过driver去操作其他各个成员。即可达到通过基类指针寻址到派生类地址的效果。


2. 宏实现

来看看内核中是如何定义和实现这个container_of宏的。
[C] 纯文本查看 复制代码
/tools/include/linux/kernel.h



#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)



/**

 * container_of - cast a member of a structure out to the containing structure

 * @ptr:	the pointer to the member.

 * @type:	the type of the container struct this is embedded in.

 * @member:	the name of the member within the struct.

 *

 */

#define container_of(ptr, type, member) ({			\

	const typeof(((type *)0)->member) * __mptr = (ptr);	\

	(type *)((char *)__mptr - offsetof(type, member)); })





RT-Thread 中也有类似的操作:

[C] 纯文本查看 复制代码
/**

 * rt_container_of - return the start address of struct type, while ptr is the

 * member of struct type.

 */

#define rt_container_of(ptr, type, member) \

    ((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member)))


从注释上可以清楚的看出这个宏的作用就是:当ptr是结构体的一个成员时,可以反推出结构体的首地址。


先写个小程序做个实验,验证一下(&((type *)0)->member)到底是什么结果:
[C] 纯文本查看 复制代码
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)



typedef struct {



    char a;

    char b;

    char c;

    char d;

}test_t;



void main(void)

{

    printf("offsetof: %d\n",offsetof(test_t, a));

    printf("offsetof: %d\n",offsetof(test_t, b));

    printf("offsetof: %d\n",offsetof(test_t, c));

    printf("offsetof: %d\n",offsetof(test_t, d));

}


运行:
1.png


当把结构体成员类型换一下,如下:
typedef struct {

    int a;
    int b;
    int c;
    int d;
}test_t;

运行:

2.png

现在我们可以知道(&((type *)0)->member) 这种操作,就是返回member在type这种类型结构体中的相对地址偏移,注意是相对偏移而不是成员在内存中的地址。

理解了以上内容后再看offsetof(),它的作用是获取结构体中某个成员相对于该结构体首元素地址的偏移量。

[C] 纯文本查看 复制代码
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)


test_t 对象内存布局:

3.png


从图中可以看出,只要获得了某个成员的地址,再减去这个成员在结构体中的偏移就到到顶了,即反推出了结构体的地址。

先看RTT内核中的宏定义,比较明了一些。
[C] 纯文本查看 复制代码
#define rt_container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member)))


(char *)(ptr)强转换以将移动步进控制为字节,ptr地址减掉这个偏移,就回到结构体首地址了,真TM神奇!

3.原理

在linux内核中的宏定义是这样的:
[C] 纯文本查看 复制代码
#define container_of(ptr, type, member) ({			\
	const typeof(((type *)0)->member) * __mptr = (ptr);	\
	(type *)((char *)__mptr - offsetof(type, member)); })


这里面还出现了一个typeof关键字.

那么,typeof(((type *)0)->member), (&((type *)0)->member) 到底是什么样的一种操作了?


原来,ANSI C标准允许值为0的常量被强制转换成任何一种类型的指针,并且转换的结果是个NULL,因此((type *)0)的结果就是一个类型为type *的NULL指针.

如果利用这个NULL指针来访问type的成员当然是非法的,但typeof( ((type *)0)->member )是想取该成员的类型,所以编译器不会生成访问type成员的代码,
类似的代码&( ((type *)0)->member )在最前面有个取地址符&,它的意图是想取member的相对于type类型首地址的偏移,所以编译器同样会优化为直接取地址偏移。


typeof()关键字

这个关键字有点像C++11中的decltype关键字,用来获取对象的类型

typeof()关键字常见用法一共有以下几种。

1.不用知道函数返回什么类型,可以使用typeof()定义一个用于接收该函数返回值的变量。
[C] 纯文本查看 复制代码
int func()
{
	return 0;
}
typeof(func()) r1;//定义一个变量r1,用于接收函数func()返回的值,由于该函数返回的类型是:int,所以变量r1也是该类型。注意,函数不会执行。
r1 = func();



2. 在宏定义中动态获取相关结构体成员的类型

如下代码,定义一个和变量x相同类型的临时变量_max1,定义一个和变量y相同类型的临时变量_max2,再判断两者类型是否一致,不一致给出一个警告,最后比较两者。

看一段内核中的代码 :
[C] 纯文本查看 复制代码
#ifndef max
#define max(x, y) ({				\
	typeof(x) _max1 = (x);			\
	typeof(y) _max2 = (y);			\
	(void) (&_max1 == &_max2);		\
	_max1 > _max2 ? _max1 : _max2; })
#endif

#ifndef min
#define min(x, y) ({				\
	typeof(x) _min1 = (x);			\
	typeof(y) _min2 = (y);			\
	(void) (&_min1 == &_min2);		\
	_min1 < _min2 ? _min1 : _min2; })
#endif


再回头看看在linux内核中的container_of宏定义:
[C] 纯文本查看 复制代码
#define container_of(ptr, type, member) ({			\
	const typeof(((type *)0)->member) * __mptr = (ptr);	\
	(type *)((char *)__mptr - offsetof(type, member)); })


const typeof(((type *)0)->member) * __mptr  这句是不是也是定义一个临时变量 __mptr?

3. 也可直接取得已知类型

如下代码,定义了一个int类型的指针p,不过这纯属脱库子放P,像这种用法没什么太大的意义了。
[C] 纯文本查看 复制代码
    int a = 2;
    typeof (int *) p;
    p = &a;
    printf("%d\n", *p);



注意:
typeof构造中的类型名不能包含存储类说明符,如extern或static。不过允许包含类型限定符,如const或volatile。
    例如,下列代码是无效的,因为它在typeof构造中声明了extern:
        typeof(extern int) a;


强制转换

再回头想想,有没有另外一种办法了可以通过结构体成员的地址反推结构体地址的呢? 当然是有的: 把基类对象放在结构体的第一个位置即可。

以struct i2c_drive为例,如下:

[C] 纯文本查看 复制代码
struct i2c_driver {
	struct device_driver driver;

	unsigned int class;

	....

	u32 flags;
};


在函数中调用时,只要强制转换一下就行:
[C] 纯文本查看 复制代码
static int i2c_device_match(struct device *dev, struct device_driver *drv)
{
	struct i2c_client	*client = i2c_verify_client(dev);
	struct i2c_driver	*driver;


	/* Attempt an OF style match */
	if (i2c_of_match_device(drv->of_match_table, client))
		return 1;

	/* Then ACPI style match */
	if (acpi_driver_match_device(dev, drv))
		return 1;

	//driver = to_i2c_driver(drv);	
	driver = (struct i2c_driver*)drv; 

	/* Finally an I2C match */
	if (i2c_match_id(driver->id_table, client))
		return 1;

	return 0;
}


将 driver = to_i2c_driver(drv); 改为: driver = (struct i2c_driver*)drv; 即可。

因为在一个系统上指针大小都一样的,对指针的引用本质是对指针所指向的地址的有效作用域的引用,通过强制类型转换可以改变这种作用域的范围,
struct device_driver 对象处在struct i2c_driver结构体的第一个成员地址,因此struct device_driver 对象的地址即是其所在结构体的地址。
通过将struct device_driver 类型的指针强制转换为struct i2c_driver类型,也就重新定义了该指针的作用范围,即该指针的作用域从struct device_driver
扩大到了struct i2c_driver。这种方式更加简单,但也因此丧失了一定的灵活性。


//=============================END====================================================================





评分

参与人数 2金币 +102 收起 理由
fswyt + 2
eric2013 + 100 很给力!

查看全部评分

回复

使用道具 举报

1万

主题

6万

回帖

10万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
106913
QQ
发表于 2022-11-8 14:45:34 | 显示全部楼层
非常感谢楼主分享,核心思想就是这句:&((type *)0)->member 获取参数成员的在结构体里面的相对地址,这操作确实高
回复

使用道具 举报

1万

主题

6万

回帖

10万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
106913
QQ
发表于 2022-11-8 15:08:24 | 显示全部楼层
Zephyr也是,我贴下,它的前面做了个很好的举例
https://github.com/zephyrproject ... e/zephyr/sys/util.h

/**
* @brief Get a pointer to a structure containing the element
*
* Example:
*
*        struct foo {
*                int bar;
*        };
*
*        struct foo my_foo;
*        int *ptr = &my_foo.bar;
*
*        struct foo *container = CONTAINER_OF(ptr, struct foo, bar);
*
* Above, @p container points at @p my_foo.
*
* @param ptr pointer to a structure element
* @param type name of the type that @p ptr is an element of
* @param field the name of the field within the struct @p ptr points to
* @return a pointer to the structure that contains @p ptr
*/
#define CONTAINER_OF(ptr, type, field) \
        ((type *)(((char *)(ptr)) - offsetof(type, field)))

回复

使用道具 举报

76

主题

685

回帖

913

积分

金牌会员

积分
913
发表于 2022-11-8 15:40:44 | 显示全部楼层
不通用吧这个宏,栈的生长方式不一样算出来结果应该截然不同吧
回复

使用道具 举报

210

主题

1044

回帖

1684

积分

至尊会员

More we do, more we can do.

积分
1684
发表于 2022-11-8 16:47:54 | 显示全部楼层
我常用的有:
ARRAY_SIZE()
BUILD_BUG_ON()
offsetof()
回复

使用道具 举报

9

主题

103

回帖

130

积分

初级会员

积分
130
发表于 2022-11-8 18:04:19 | 显示全部楼层
emwin 发表于 2022-11-8 16:47
我常用的有:
ARRAY_SIZE()
BUILD_BUG_ON()

BUILD_BUG_ON()有什么作用?
回复

使用道具 举报

6

主题

76

回帖

94

积分

初级会员

积分
94
发表于 2022-11-8 19:12:50 | 显示全部楼层
还是RT-Thread 的方式来得直观些。linux中临时变量 __mptr好像没啥用,除非有啥特殊情况。
回复

使用道具 举报

3

主题

75

回帖

84

积分

初级会员

积分
84
发表于 2022-11-8 20:29:08 | 显示全部楼层
想起 kfifo 也是奇淫巧计
回复

使用道具 举报

18

主题

321

回帖

375

积分

高级会员

积分
375
发表于 2022-11-9 09:53:14 | 显示全部楼层
buxinshan 发表于 2022-11-8 19:12
还是RT-Thread 的方式来得直观些。linux中临时变量 __mptr好像没啥用,除非有啥特殊情况。

我觉得 linux 的这个设计更加优秀。

更加准确的说法是,这个是 linux 的新版,RTT 那个其实就是 linux 的老版本,写 linux 内核的人这样改,肯定是有原因的,你可以想一想
回复

使用道具 举报

2

主题

37

回帖

48

积分

新手上路

积分
48
发表于 2022-11-9 10:30:15 | 显示全部楼层
公司项目有这个来算出结构体相对首地址的偏移量, 比较奇怪的是 他不用取地址就可以,本小白在做小测试在gcc编译的时候没有取地址是报错的。
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)  无取地址,有强转  
回复

使用道具 举报

14

主题

109

回帖

151

积分

初级会员

积分
151
发表于 2022-11-12 14:13:38 | 显示全部楼层
不错的用法,有空细细研究学习下
回复

使用道具 举报

18

主题

321

回帖

375

积分

高级会员

积分
375
发表于 2023-6-29 11:52:27 | 显示全部楼层
王海靖 发表于 2022-11-9 09:53
我觉得 linux 的这个设计更加优秀。

更加准确的说法是,这个是 linux 的新版,RTT 那个其实就是 linux ...

因为这是个宏,传递进来的参数,如果结构体类型和成员不匹配,就是说成员,不是结构体里面的,万一手写写错了一个字符,在使用 linux 的方式这里会抛出一个警告。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|小黑屋|Archiver|手机版|硬汉嵌入式论坛

GMT+8, 2024-5-10 03:44 , Processed in 0.294208 second(s), 29 queries .

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2023, Tencent Cloud.

快速回复 返回顶部 返回列表