定义扩展为可变数量元素的 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。
我正在编写 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。