定义扩展为可变数量元素的 C 宏

Definining a C macro that expands to a variable number of elements

我正在编写 USB 报告描述符,它是一个字节序列:一个标记字节(其中较低的位表示后面有多少数据字节)后跟 0、1、2 或 4 个数据字节。例如定义输入的逻辑范围:

uint8_t report_descriptor[] = {
    ...
    0x15, 0x00,                     //   Logical Minimum (0)
    0x26, 0xFF, 0x03,               //   Logical Maximum (1023)
    ...
};

由于 0 适合一个字节,我们使用标记类型 0x15(逻辑最小值,一个数据字节)。但是 1023 需要两个字节,所以标记类型 0x26(两个数据字节的逻辑最大值)。

我曾希望定义一些宏以使其更具可读性(并避免必须注释每一行):

uint8_t report_descriptor[] = {
    ...
    LOGICAL_MINIMUM(0),
    LOGICAL_MAXIMUM(1023),
    ...
};

但是,我遇到了一个障碍:该宏需要根据值扩展到不同数量的元素。我没有看到任何简单的方法来实现这一目标。我试过像 value > 255 ? (value & 0xFF, value >> 8) : value 这样的技巧,但它总是被扩展到一个字节。

我认为规范允许始终使用 4 字节标签,但那样会很浪费,所以我宁愿不这样做。

预处理器可以实现我的目标吗?

有一个肮脏的 hack 可以实现所要求的功能。但作为一个肮脏的 hack,它不太可能提高可读性。但它有效。首先让我们像这样定义一个包含文件 helper.h

#if PARAM > 255
0x26, (PARAM & 0xFF), (PARAM >> 8),
#else
0x15, (PARAM),
#endif

那么我们主要会做:

uint8_t report_descriptor[] = {

        #define PARAM 0
        #include "helper.h"
        #undef PARAM

        #define PARAM 1023
        #include "helper.h"
        #undef PARAM

};

要查看它是否正常工作,这里是测试代码:

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

uint8_t report_descriptor[] = {

        #define PARAM 0
        #include "helper.h"
        #undef PARAM

        #define PARAM 1023
        #include "helper.h"
        #undef PARAM

};


int main(int argc, char** args) {

    int i;
    for (i=0; i < sizeof(report_descriptor); i++ )
        printf("%x\n", report_descriptor[i]);
    return 0;
}

输出为:

15
0
26
ff
3

我认为 C 预处理器的功能不足以以干净的方式执行此操作。如果您愿意求助于 M4 宏处理器,它可以很容易地完成。 M4 应该可以在绝大多数 GNU/Linux 系统上使用,并且便携式实现应该适用于大多数平台。

让我们在单独的文件中定义 M4 宏并将其命名为 macros.m4

define(`EXTRACT_BYTE', `(( >> (8 * )) & 0xFF)')

dnl You probably don't want to define these as M4 macros but as C preprocessor
dnl macros in your header files.

define(`TAG_1_BYTES', `0x15')
define(`TAG_2_BYTES', `0x26')
define(`TAG_3_BYTES', `0x37')
define(`TAG_4_BYTES', `0x48')

define(`EXPAND_1_BYTES', `TAG_1_BYTES, EXTRACT_BYTE(, 0)')
define(`EXPAND_2_BYTES', `TAG_2_BYTES, EXTRACT_BYTE(, 1), EXTRACT_BYTE(, 0)')
define(`EXPAND_3_BYTES', `TAG_3_BYTES, EXTRACT_BYTE(, 2), EXTRACT_BYTE(, 1), EXTRACT_BYTE(, 0)')
define(`EXPAND_4_BYTES', `TAG_4_BYTES, EXTRACT_BYTE(, 3), EXTRACT_BYTE(, 2), EXTRACT_BYTE(, 1), EXTRACT_BYTE(, 0)')

define(`ENCODE',
  `ifelse(eval( < 256), `1', `EXPAND_1_BYTES()',
    `ifelse(eval( < 65536), `1', `EXPAND_2_BYTES()',
      `ifelse(eval( < 16777216), `1', `EXPAND_3_BYTES()',
      `EXPAND_4_BYTES()')')')')

现在,编写 C 文件非常简单。将以下代码放入文件 test.c.m4:

include(`macros.m4')

`static unint8_t report_descriptor[] = {'
    ENCODE(50),
    ENCODE(5000),
    ENCODE(500000),
    ENCODE(50000000),
`};'

在您的 Makefile 中,添加以下规则

test.c: test.c.m4 macros.m4
    ${M4} $< > $@

其中 M4 设置为 M4 处理器(通常 m4)。

如果 M4 在 test.c.m4 上是 运行,它将 – 省略一些多余的白色 space – 产生以下 test.c 文件:

static unint8_t report_descriptor[] = {
  0x15, ((50 >> (8 * 0)) & 0xFF),
  0x26, ((5000 >> (8 * 1)) & 0xFF), ((5000 >> (8 * 0)) & 0xFF),
  0x37, ((500000 >> (8 * 2)) & 0xFF), ((500000 >> (8 * 1)) & 0xFF), ((500000 >> (8 * 0)) & 0xFF),
  0x48, ((50000000 >> (8 * 3)) & 0xFF), ((50000000 >> (8 * 2)) & 0xFF), ((50000000 >> (8 * 1)) & 0xFF), ((50000000 >> (8 * 0)) & 0xFF),
};

您可能会发现将 test.c.m4 文件保持得尽可能小并且 #include 在普通 C 文件中更方便。

如果你不了解M4,你可以很快地学习基础知识。如果已经在使用 GNU Autoconf, you might find it convenient to use their M4sugar M4 宏库而不是我上面使用的普通 M4。