如何在C中实现动态调度表

Dav*_*bin 6 c dispatch c-preprocessor

首先,我了解如何使用函数指针和字符串或其他查找来实现调度表,这不是挑战.

我正在寻找的是在编译时动态地向该表添加条目的一些方法.

我希望的代码结构类型是这样的:

Strategy.h - 包含调度程序和调度表定义的函数定义Strategy.c - 包含调度程序的代码

MyFirstStrategy.c - 包括Strategy.h并提供策略MyOtherStrategy.c的一个实现 - 包括Strategy.h并提供策略的第二个实现

我们的想法是,将函数指针和策略名称插入到调度表中的代码不应该存在于Strategy.c中,而应该存在于各个策略实现文件中,并且查找表应该以某种方式在编译时动态构造.

对于固定大小的调度表,这可以如下管理,但我想要一个动态大小的表,我不希望Strategy.c实现必须包含实现的所有头文件,我想发送要在编译时构造的表,而不是运行时.

固定大小示例

Strategy.h

typedef void strategy_fn_t(int);
typedef struct {
    char           *strategyName;
    strategy_fn_t  *implementation;
} dispatchTableEntry_t;
Run Code Online (Sandbox Code Playgroud)

MyFirstStrategy.h

#include "Strategy.h"

void firstStrategy( int param );
Run Code Online (Sandbox Code Playgroud)

MyOtherStrategy.h

#include "Strategy.h"

void otherStrategy( int param );
Run Code Online (Sandbox Code Playgroud)

Strategy.c

#include "Strategy.h"
#include "MyFirstStrategy.h"
#include "MyOtherStrategy.h"

dispatchTableEntry_t dispatchTable[] = {
    { "First Strategy", firstStrategy },
    { "Other Strategy", otherStrategy }
};
int numStrategies = sizeof( dispatchTable ) / sizeof(dispatchTable[0] );
Run Code Online (Sandbox Code Playgroud)

我真正想要的是一些预处理器魔法,我可以将其插入到策略实现文件中以自动处理这个问题,例如

MyFirstStrategy.c

#include "Strategy.h"

void firstStrategy( int param );

ADD_TO_DISPATCH_TABLE( "First Strategy", firstStrategy );
Run Code Online (Sandbox Code Playgroud)

有什么想法吗 ?

Art*_*Art 5

在具有 gnu 链接器和编译器或兼容的系统上,可以将某些对象放在不同的部分中。然后链接器将为该部分的开始和结束生成符号。使用它,您可以将所有结构放入不同对象的该部分中,链接器将在链接时合并这些部分,并且您可以将它们作为数组访问。如果您在共享库中执行此操作,那么这需要更多的摆弄,并且绝对不能在 ELF Linux/*BSD 之外移植。

我在 MacOS 和 a.out BSD 上做过类似的事情(尽管这个例子不起作用),但我丢失了该代码。以下是如何在 Linux 上执行此操作的示例:

$ cat link_set.c
#include <stdio.h>

struct dispatch_table {
    const char *name;
    void (*fun)(int);
};

#define ADD_TO_DISPATCH_TABLE(name, fun) \
    static const struct dispatch_table entry_##fun \
        __attribute__((__section__("link_set_dispatch_table"))) = \
        { name, fun }

int
main(int argc, char **argv)
{
    extern struct dispatch_table __start_link_set_dispatch_table;
    extern struct dispatch_table __stop_link_set_dispatch_table;
    struct dispatch_table *dt;

    for (dt = &__start_link_set_dispatch_table; dt != &__stop_link_set_dispatch_table; dt++) {
        printf("name: %s\n", dt->name);
        (*dt->fun)(0);
    }
    return 0;
}

void
fun1(int x)
{
    printf("fun1 called\n");
}
ADD_TO_DISPATCH_TABLE("fun 1", fun1);

void
fun2(int x)
{
    printf("fun2 called\n");
}
ADD_TO_DISPATCH_TABLE("fun 2", fun2);
$ cc -o link_set link_set.c
$ ./link_set
name: fun 1
fun1 called
name: fun 2
fun2 called
$
Run Code Online (Sandbox Code Playgroud)

解释一下宏的作用。它创建一个 structdispatch_table,其名称我们希望是唯一的,因为您可能希望在一个对象中多次使用它(如果多次使用同一个函数,请找出其他方式来命名该结构)并使用gnu 扩展名来指定对象应该结束于哪个部分。如果我们将对象放入“some_section_name”中,则链接器将自动添加符号 __start_some_section_name 和 __end_some_section_name。然后我们可以在这些符号之间行走并获取我们放入该部分的所有结构。

更新了 MacOS 的工作示例:

#include <stdio.h>
#include <mach-o/ldsyms.h>
#include <mach-o/getsect.h>
#include <mach-o/loader.h>

struct dispatch_table {
        const char *name;
        void (*fun)(int);
};

#define ADD_TO_DISPATCH_TABLE(name, fun) \
    static const struct dispatch_table entry_##fun \
    __attribute__((__section__("__DATA,set_dt"))) = \
    { name, fun }

int
main(int argc, char **argv)
{
        struct dispatch_table *start;
        unsigned long sz;
        intptr_t s;
        int i;

        s = (intptr_t)getsectdata("__DATA", "set_dt", &sz);
        if (s == 0)
                return 1;
        s += _dyld_get_image_vmaddr_slide(0);
        start = (struct dispatch_table *)s;
        sz /= sizeof(*start);

        for (i = 0; i < sz; i++) {
                struct dispatch_table *dt = &start[i];
                printf("name: %s\n", dt->name);
                (*dt->fun)(0);
        }
        return 0;
}

void
fun1(int x)
{
        printf("fun1 called\n");
}
ADD_TO_DISPATCH_TABLE("fun 1", fun1);

void
fun2(int x)
{
        printf("fun2 called\n");
}
ADD_TO_DISPATCH_TABLE("fun 2", fun2);
Run Code Online (Sandbox Code Playgroud)

此处该节必须称为“set_dt”,因为在此可执行格式中节名称的长度有限。

当然,如果你已经需要这个,你肯定明白这一切都是非常危险的,不可移植的,不能保证永远工作(我三年前的代码不能在当前版本的 macOS 上工作) ,没有类型或其他安全性,如果其他东西决定将东西放入同名的部分中,那么东西最终会变成非常漂亮的烟花。但这是一个非常巧妙的技巧。我在两个大型项目中使用了这种方法,它确实节省了大量工作,中央调度表不必在共享存储库中进行编辑,这曾经给每个人带来冲突。