Skip to content
Published at:

通过"字符串"调用同名的函数

在GUI编程里面,在写静态xml布局时;比如Android中,可以通过在xml中指定onClick的回调函数名,即可以实现,在事件触发时调用相应的函数

xml
<Button
    android:id="@+id/login_btn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="onLoginClick"
    android:text="Login" />
java
public class MainActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    public void onLoginClick(View v) {
        Toast.makeText(this, "Clicked on Login Button", Toast.LENGTH_LONG).show();
    }
}

上述大多是在高级语言中,这是Java强大的地方,可以通过反射类全名创建对象,调用函数,做相应的;通过自定义注解去生成代码;Java框架中随处可见这样的代码,写一堆xml,properties配置,自定义注解;最大的好处在于模块之间解耦,只要通过字符串,不需要依赖于真正的定义,就可以创建对象或其它操作

类似的,还有QT里面的Signals & Slots, 后来研究发现其实区别挺大,它并没有把这事件函数配置化(把函数名写到配置中),代码:QObject::connect(&a, &Counter::valueChanged, &b, &Counter::setValue);这是一个信号槽的绑定,在这里其实是已经可以拿到两个函数的地址了,本质上是qmake在编译的时候会去生成新代码

现在就用C来实现一个,不需要依赖于真正的函数定义(不需要#include "header.h"),通过这个字符串,去获取这个函数的地址,调用/使用这个函数

一开始想解决这个问题的时候,在网上找了一圈,在C的语言层面上没有解决方案;其实也正常,如果能通过一个字符串去获取一个函数的地址,这会是一个非常大的安全问题;后来我们公司C工在研究另外一个框架,那个框架在做了一件事,把接口配置化了,也就是接口/函数名写在配置文件里面, 后面让他看下源码怎么实现的,他当时也看的是一脸懵逼,触及了知识盲区,正好我早前看过一本书:程序员的自我修养--链接,装载,库, 然后看过了一些gcc的文档; 然后给他梳理了,就有了这文章,下最后通过gcc的__attribute__((section("section_name"))) 属性来解决,下面讲人话:

技术原理:

gcc在编译代码的时候,会对我们写的代码"分类",会把不同的"元素"放在不同的段,也叫section,比如熟悉的.text/.data/.bss段;

程序可执行文件段的大纲:可以通过objdump -h program_name 或是readelf -S program_name去查看:

shell
$ objdump -h a.out

a.out:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .interp       0000001c  0000000000000318  0000000000000318  00000318  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
    .....
 15 .text         000001f5  0000000000001080  0000000000001080  00001080  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 16 .fini         0000000d  0000000000001278  0000000000001278  00001278  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 17 .rodata       00000017  0000000000002000  0000000000002000  00002000  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
    .....
 24 .data         00000010  0000000000004000  0000000000004000  00003000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 25 handler       00000010  0000000000004010  0000000000004010  00003010  2**4
                  CONTENTS, ALLOC, LOAD, DATA
 26 .bss          00000008  0000000000004020  0000000000004020  00003020  2**0
                  ALLOC
 27 .comment      0000002a  0000000000000000  0000000000000000  00003020  2**0
                  CONTENTS, READONLY

当然也能通过gcc的__attribute__((section("section_name"))) 来自定义段

__attribute__关键字:

gcc文档:https://gcc.gnu.org/onlinedocs/gcc/index.html大纲 6.33 Attribute Syntax文档:https://gcc.gnu.org/onlinedocs/gcc/Attribute-Syntax.html

怎么理解这个关键字:这里可以用下常的面向对象;gcc在编译代码文件的时候,会对代码文件中的"元素"进行处理,这里的"元素"指的是代码中的基本单位,一个函数,一个类型定义,一个全局变量... __attribute__关键字是用来指定这些元素的属性,比如一个元素放在编译文件的什么位置,

__attribute__关键字可以修饰的"元素":

__attribute__关键字语法:

c
__attribute__ ((attribute-list)) // 多个属性用逗号隔开

section属性:

将"元素"放在指定段,指定段名字

c
#include <stdint.h>
#include <stdio.h>

#define SECTION_NAME "handler"

// linker会给这个段添加两个symbol,格式如下:
// start: __start_${section_name}
// stop: __stop_${section_name}
extern int32_t __start_handler[];
extern int32_t __stop_handler[];

// 修饰全局变量
int32_t __attribute__((section(SECTION_NAME))) var1 = 100;
int32_t __attribute__((section(SECTION_NAME))) var2 = 200;

int main() {
    printf("var1 = %d\n", var1);
    printf("var2 = %d\n", var2);

    // 段起始地址
    printf("__start_handler addr = %p\n", __start_handler);
    printf("__stop_handler addr = %p\n", __stop_handler);

    // 段大小
    int section_size = (char*)__stop_handler - (char*)__start_handler;
    printf("setion_size = %d\n", section_size);

    // 通过段去获取数据
    printf("__start_handler = %d\n", *__start_handler);
    printf("__stop_handler = %d\n", *(__start_handler + 1));
}

输出:

bash
$ ./a.out
var1 = 100
var2 = 200
__start_handler addr = 0x5561de356010
__stop_handler addr = 0x5561de356018
setion_size = 8
__start_handler = 100
__stop_handler = 200

程序文件段查看:

bash
$ objdump -h a.out

a.out:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .interp       0000001c  0000000000000318  0000000000000318  00000318  2**0
    .....
 15 .text         000001f5  0000000000001080  0000000000001080  00001080  2**4
 16 .fini         0000000d  0000000000001278  0000000000001278  00001278  2**2
 17 .rodata       00000017  0000000000002000  0000000000002000  00002000  2**2
    .....
 24 .data         00000010  0000000000004000  0000000000004000  00003000  2**3
 25 handler       00000010  0000000000004010  0000000000004010  00003010  2**4
 26 .bss          00000008  0000000000004020  0000000000004020  00003020  2**0
 27 .comment      0000002a  0000000000000000  0000000000000000  00003020  2**0

这样,就可以把程序的数据中的数据放在自定义的段,自己去获取;

实现:

回到最开始的问题,可以把函数的地址放在自定义的section段,自己来这个section取数据就能实现;这样定义函数的地方和调用函数的地方就解耦了,没有依赖关系

示意图:这图是不严谨的,但我想表达这个那个意思

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct handler_info {
    char* name;
    void (*handler)();
};

// 段名
#define SECTION_NAME "handler_section"

// 段起始地址
extern struct handler_info __start_handler_section[];  // 格式: __start_${section_name}
extern struct handler_info __stop_handler_section[];   // 格式:__stop_${section_name}

void handler_func() {
    printf("handler_func \n");
}

// 放在自定义段中的结构体数据
struct handler_info info_func __attribute__((section(SECTION_NAME))) = {
    .name    = "func",
    .handler = handler_func,
};

int main(int argc, char* argv[]) {
    printf("__start_handler_section = %p\n", __start_handler_section);
    printf("__stop_handler_section = %p\n", __stop_handler_section);

    printf("sizeof(struct handler_info) = %ld\n", sizeof(struct handler_info));
    printf("handler_section size = %ld\n", __stop_handler_section - __start_handler_section);

    printf("~~~~~~~\n");

    // 遍历数据
    const char*                name = "func";
    for (struct handler_info* iter = __start_handler_section; iter < __stop_handler_section; iter++) {
        if (strcmp(name, iter->name) == 0) {
            iter->handler();  // call func
        }
    }

    return 0;
}

当然,上面只是个demo,实际生产环境会进行封装:可以把不需要关心的部分抽离,用宏进行替换

c++
#ifndef MACRO_H
#include <limits.h>
#include <stdlib.h>

typedef struct handler_info {
    char* name;
    void (*handler)();
} handler_info_t;

#define handler_(name_)                                                      \
    void handler_##name_();                                                  \
    struct handler_info __attribute__((section("handler"))) info_##name_ = { \
        .name    = #name_,                                                   \
        .handler = handler_##name_,                                          \
    };                                                                       \
    void handler_##name_()

#define SECTION_START(name_) __start_##name_[]
#define SECTION_END(name_) __stop_##name_[]

#define SECTION_START_SYMBOL(section_name_, iter_)                \
    ({                                                            \
        extern const typeof(*iter_) SECTION_START(section_name_); \
        __start_##section_name_;                                  \
    })

#define SECTION_STOP_SYMBOL(section_name_, iter_)               \
    ({                                                          \
        extern const typeof(*iter_) SECTION_END(section_name_); \
        __stop_##section_name_;                                 \
    })

#define SECTION_FOREACH(section_name_, iter_)                                                                   \
    for (iter_ = SECTION_START_SYMBOL(section_name_, iter_); iter_ < SECTION_STOP_SYMBOL(section_name_, iter_); \
         (iter_)++)

#endif  // !MACRO_H

main.c

c

#include <stdio.h>
#include <string.h>

#include "macro.h"

// 宏展开
// void handler_func();
// struct handler_info __attribute__((section("handler"))) info_func = {
//     .name    = "func",
//     .handler = handler_func,
// };
// void handler_func()
handler_(func) {
    printf("handler_func \n");
}

int main(int argc, char* argv[]) {
    const char* name = "func";

    const struct handler_info* iter;
    // 宏展开
    // for (iter = SECTION_START_SYMBOL(handler, iter); iter < SECTION_STOP_SYMBOL(handler, iter); (iter)++){ }
    SECTION_FOREACH(handler, iter) {
        if (strcmp(name, iter->name) == 0) {
            iter->handler();  // call func
        }
    }

    return 0;
}

Updated at: