c - X-Macros 的实际使用

标签 c macros c-preprocessor x-macros

我刚刚了解到X-Macros 。您见过 X 宏在现实世界中的哪些用途?它们什么时候是适合这项工作的工具?

最佳答案

几年前,当我开始在代码中使用函数指针时,我发现了 X 宏。我是一名嵌入式程序员,经常使用状态机。我经常会编写这样的代码:

/* declare an enumeration of state codes */
enum{ STATE0, STATE1, STATE2, ... , STATEX, NUM_STATES};

/* declare a table of function pointers */
p_func_t jumptable[NUM_STATES] = {func0, func1, func2, ... , funcX};

问题是我认为必须维护函数指针表的顺序以使其与状态枚举的顺序相匹配非常容易出错。

我的一位 friend 向我介绍了 X 宏,这就像我脑子里的一个灯泡突然亮了。说真的,我这辈子x宏你都去哪儿了!

所以现在我定义下表:

#define STATE_TABLE \
        ENTRY(STATE0, func0) \
        ENTRY(STATE1, func1) \
        ENTRY(STATE2, func2) \
        ...
        ENTRY(STATEX, funcX) \

我可以按如下方式使用它:

enum
{
#define ENTRY(a,b) a,
    STATE_TABLE
#undef ENTRY
    NUM_STATES
};

p_func_t jumptable[NUM_STATES] =
{
#define ENTRY(a,b) b,
    STATE_TABLE
#undef ENTRY
};

作为奖励,我还可以让预处理器构建我的函数原型(prototype),如下所示:

#define ENTRY(a,b) static void b(void);
    STATE_TABLE
#undef ENTRY

另一种用法是声明和初始化寄存器

#define IO_ADDRESS_OFFSET (0x8000)
#define REGISTER_TABLE\
    ENTRY(reg0, IO_ADDRESS_OFFSET + 0, 0x11)\
    ENTRY(reg1, IO_ADDRESS_OFFSET + 1, 0x55)\
    ENTRY(reg2, IO_ADDRESS_OFFSET + 2, 0x1b)\
    ...
    ENTRY(regX, IO_ADDRESS_OFFSET + X, 0x33)\

/* declare the registers (where _at_ is a compiler specific directive) */
#define ENTRY(a, b, c) volatile uint8_t a _at_ b:
    REGISTER_TABLE
#undef ENTRY

/* initialize registers */
#define ENTRY(a, b, c) a = c;
    REGISTER_TABLE
#undef ENTRY

然而,我最喜欢的用法是通信处理程序

首先,我创建一个通信表,其中包含每个命令名称和代码:

#define COMMAND_TABLE \
    ENTRY(RESERVED,    reserved,    0x00) \
    ENTRY(COMMAND1,    command1,    0x01) \
    ENTRY(COMMAND2,    command2,    0x02) \
    ...
    ENTRY(COMMANDX,    commandX,    0x0X) \

表中既有大写字母又有小写字母的名称,因为大写字母将用于枚举,小写字母将用于函数名称。

然后我还为每个命令定义结构来定义每个命令的外观:

typedef struct {...}command1_cmd_t;
typedef struct {...}command2_cmd_t;

etc.

同样,我为每个命令响应定义结构:

typedef struct {...}command1_resp_t;
typedef struct {...}command2_resp_t;

etc.

然后我可以定义我的命令代码枚举:

enum
{
#define ENTRY(a,b,c) a##_CMD = c,
    COMMAND_TABLE
#undef ENTRY
};

我可以定义我的命令长度枚举:

enum
{
#define ENTRY(a,b,c) a##_CMD_LENGTH = sizeof(b##_cmd_t);
    COMMAND_TABLE
#undef ENTRY
};

我可以定义我的响应长度枚举:

enum
{
#define ENTRY(a,b,c) a##_RESP_LENGTH = sizeof(b##_resp_t);
    COMMAND_TABLE
#undef ENTRY
};

我可以确定有多少个命令,如下所示:

typedef struct
{
#define ENTRY(a,b,c) uint8_t b;
    COMMAND_TABLE
#undef ENTRY
} offset_struct_t;

#define NUMBER_OF_COMMANDS sizeof(offset_struct_t)

注意:我从未真正实例化 offset_struct_t,我只是将它用作编译器为我生成命令定义数量的一种方式。

请注意,我可以生成函数指针表,如下所示:

p_func_t jump_table[NUMBER_OF_COMMANDS] = 
{
#define ENTRY(a,b,c) process_##b,
    COMMAND_TABLE
#undef ENTRY
}

还有我的函数原型(prototype):

#define ENTRY(a,b,c) void process_##b(void);
    COMMAND_TABLE
#undef ENTRY

现在,最后一个最酷的用途是,我可以让编译器计算我的传输缓冲区应该有多大。

/* reminder the sizeof a union is the size of its largest member */
typedef union
{
#define ENTRY(a,b,c) uint8_t b##_buf[sizeof(b##_cmd_t)];
    COMMAND_TABLE
#undef ENTRY
}tx_buf_t

这个 union 就像我的偏移结构,它没有实例化,相反我可以使用 sizeof 运算符来声明我的传输缓冲区大小。

uint8_t tx_buf[sizeof(tx_buf_t)];

现在我的传输缓冲区 tx_buf 是最佳大小,当我向此通信处理程序添加命令时,我的缓冲区将始终是最佳大小。酷!

另一个用途是创建偏移表: 由于内存通常是嵌入式系统的限制,因此当它是稀疏数组时,我不想为我的跳转表使用 512 字节(每个指针 2 字节 X 256 个可能的命令)。相反,我将为每个可能的命令提供一个 8 位偏移量表。然后,该偏移量用于索引到我的实际跳转表,该表现在只需要是 NUM_COMMANDS * sizeof(pointer)。在我的例子中定义了 10 个命令。我的跳转表长 20 字节,偏移表长 256 字节,总共 276 字节,而不是 512 字节。然后我像这样调用我的函数:

jump_table[offset_table[command]]();

而不是

jump_table[command]();

我可以像这样创建一个偏移表:

/* initialize every offset to 0 */
static uint8_t offset_table[256] = {0};

/* for each valid command, initialize the corresponding offset */
#define ENTRY(a,b,c) offset_table[c] = offsetof(offset_struct_t, b);
    COMMAND_TABLE
#undef ENTRY

其中offsetof是“stddef.h”中定义的标准库宏

另一个好处是,有一种非常简单的方法可以确定是否支持命令代码:

bool command_is_valid(uint8_t command)
{
    /* return false if not valid, or true (non 0) if valid */
    return offset_table[command];
}

这也是为什么在我的 COMMAND_TABLE 中我保留了命令字节 0。我可以创建一个名为“process_reserved()”的函数,如果使用任何无效命令字节索引我的偏移量表,就会调用该函数。

关于c - X-Macros 的实际使用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55120453/

相关文章:

C switch case default 总是执行

c++ - 如何在编译时检查类型

sql - 如何配置 CMake 目标或命令来预处理 C 文件?

c - 使用 c 中的 #define 宏预处理器查找输入是否为字母数字

c++ - 来自 C++ 服务的 CreateProcessAsUser 创建进程但没有控制台

c - C语言中如何处理同名函数

c - 如何安全地将 void* 转换为 C 中的 int?

c - “ISO C99 要求可变参数宏中的 "..."至少有一个参数”专门使用 c11 和 -Wno-variadic-macros 时

c - 了解 K&R 的 putc 宏 : K&R Chapter 8 (The Unix System Interface) Exercise 2

c - 如何在 IronPython 中访问宏