0%

利用__attribute__((section))实现C语言反射

必要说明:目前C语言本身并不支持反射,本文仅是讨论利用编译器特性实现部分类似反射机制的思路和方法。

引子

反射,简单理解就是通过字符串获取对应的类型或者函数,比如下面这段代码,可以根据用户输入的参数直接调用对应的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct cmd {
void (*fn) (char*, char*);
const char* name;
};

#define reflect(x) __attribute__((section("cmds")))\
struct cmd __##x = {x,#x};

void add(char* a, char* b)
{
printf("%d\n", atoi(a) + atoi(b));
}
reflect(add);

void del(char* a, char* b)
{
printf("%d\n", atoi(a) - atoi(b));
}
reflect(del);

int main(int argc, char* argv[])
{
extern struct cmd __start_cmds;
extern struct cmd __stop_cmds;

for (struct cmd* c = &__start_cmds; c < &__stop_cmds; c++) {
if (strcmp(c->name, argv[1]) == 0) {
c->fn(argv[2], argv[3]);
}
}

return 0;
}

编译执行结果如下:

1
2
3
4
5
gcc main.c
./a.out add 1 1
2
./a.out del 1 1
0

这里所谓的“反射”无外乎就是通过查表的方式去比对用户输入的第一个参数(方法名),字符串匹配后直接调用相应的函数即可。但有趣的地方在于我们并没有刻意去维护这张“表”,仅仅是在函数后紧跟着一个宏——reflect(x),一切就自动完成了…

gcc中的__attribute__((section))

以add反射为例,我们把reflect(add)展开可以得到:

1
__attribute__((section("cmds"))) struct cmd __add = {add,"add"}

后半段很容易理解,就是定义了一个__add结构体变量,并把add函数指针及其名称传递进去,那前半段这个__attribute__((section("cmds")))有意味着什么呢?

主角登场:__attribute__((section()))

这是gcc的特色之一(貌似armcc也支持),它可以通知编译器将某个变量放在指定的段中。有关__attribute__的知识建议参考gcc官方文档,我仅将其中有关section的章节简单翻译一下:

section (“section-name”)
通常,编译器将它生成的对象放在像databss等段空间中。但有时你也会需要一些额外的段,或者将特定的变量放在特定的段中,比如特定硬件的映射。section属性可以指定某个变量(或函数)放在特定的段。

这种解释还是云里雾里,毕竟能用到__attribute__就说明已经贴近编译器底层了,很多内容都需要一定编译原理的基础。鄙人才疏学浅,为避免误导,建议移步此文:《Gcc 编译的背后》详细了解。

总之,__attribute__((section(name)))就是告诉编译器将指定的变量或者函数放在我们指定的段名当中,如此一来,程序中的其它代码可以即可通过对应名称的段地址去访问了,比如下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 分别定义a、b变量,并将其放在mysec段中
int __attribute__((section("mysec"))) a = 123;
int __attribute__((section("mysec"))) b = 456;

// 在其它代码中可以这么调用
extern int __start_mysec; // 编译器会自动定义
extern int __stop_mysec; // 编译器会自动定义

void other_fn()
{
int* mysec = &__start_mysec;
int my_a = mysec[0];
int my_b = mysec[1];
printf("a=%d b=%d\n", my_a, my_b); // a=123 b=456
}

本来还想在写一点东西来提升文章逼格,比如elf格式介绍啊、__attribute__在Linux内核中的应用啊。。。算了吧,自己也是半瓶醋,就此打住!

结尾

鄙人以为,真正的反射机制,应该是让程序像会照镜子一样具备“自我分析”的能力。

在java、.net、javascript、python等语言都具备反射机制,且功能远不止本文那样单一,可人家处境和咱c/c++不一样啊,人家背后要么有虚拟机,要么有解释器,总之很多事情可以放在runtime处理,反观c/c++连个内存释放都要亲力亲为…哎~

说到c++,貌似在c++20中的有了新关键字——reflexpr,不过目前仅在实验库中,最晚在c++26中加入标准。不过就像前面说的,毕竟没有虚拟机,即便提供了反射我估摸着也是在编译期实现,能获得的特性应该不会太多。但,有总比没有好吧。

小小鼓励,大大心意!