替代灵活数组成员,无需动态内存分配

Alternative to flexible array member, without dynamic memory allocation

我正在使用灵活的数组成员来定义 USB 字符串描述符:

// defined
typedef struct {
    uint8_t  bLength;
    uint8_t  bDescriptorType;
    uint16_t bString[];
} ATTR_PACKED USB_StringDescriptor;

// declared
#define STR_PRODUCT  L"My Cool Product"
const USB_StringDescriptor __flash manufacturer_string = {
    sizeof(STR_PRODUCT)+2,
    USB_DTYPE_String,       // 0x03
    STR_PRODUCT
};

这非常有效,并允许在别处定义实际的字符串常量,以便在项目之间轻松更改。但是由于编译器的限制,我不能再使用灵活的数组成员。我想避免对 char 数组进行硬编码,我基本上只需要将两个字节添加到宽字符数组的前面。是否有任何聪明的方法来声明一个数组或一个预处理器宏来完成这个?

我认为如果不以可移植的方式使用动态内存分配,就无法实现具有灵活大小的结构。

但是,以下两种具有固定大小结构的方法之一也可能适用于您:

  1. 定义结构使得bstring是一个固定大小的数组;如果你的程序中只有一个STR_PRODUCT,你可以使用它的大小。如果您有更多结构实例,请使用您希望允许的最大大小定义数组。
  2. 定义结构bstring为指针;您仍然可以使用指向字符串文字的指针对其进行初始化。请注意,一个对象将不会包含连续内存块中的所有信息。如果你需要包含所有信息的连续记忆,这个方法2就不行了。

请参阅以下演示这两种方法的程序:

#define STR_PRODUCT_1   L"My Cool Product"
#define STR_PRODUCT_2   L"Another Cool Product"
#define MAX_PRODUCT_STR  "012345678901234567890"

// Approach 1:
typedef struct {
    uint8_t  bLength;
    uint8_t  bDescriptorType;
    wchar_t  bString[sizeof(MAX_PRODUCT_STR)];
} USB_StringDescriptor;

const USB_StringDescriptor manufacturer_string = {
    sizeof(STR_PRODUCT_1)+2,
    0x03,
    STR_PRODUCT_1
};

const USB_StringDescriptor manufacturer_string2 = {
    sizeof(STR_PRODUCT_2)+2,
    0x03,
    STR_PRODUCT_2
};

// Approach 2:
typedef struct {
    uint8_t  bLength;
    uint8_t  bDescriptorType;
    wchar_t  *bString;
} USB_StringDescriptor_V2;

const USB_StringDescriptor_V2 manufacturer_string_v2 = {
    sizeof(STR_PRODUCT_1)+2,
    0x03,
    STR_PRODUCT_1
};

const USB_StringDescriptor_V2 manufacturer_string_v2_2 = {
    sizeof(STR_PRODUCT_2)+2,
    0x03,
    STR_PRODUCT_2
};

int main() {
    wprintf(L"1: %ls\n",manufacturer_string.bString);
    wprintf(L"2: %ls\n",manufacturer_string2.bString);

    wprintf(L"1 ptr: %ls\n",manufacturer_string_v2.bString);
    wprintf(L"2 ptr: %ls\n",manufacturer_string_v2_2.bString);
}

在我看来,如果你的编译器不支持这种类型的初始化,最好的方法是使用宏(我不喜欢长多行宏,但如果没有其他方法的话)

#define STR_PRODUCT  L"My Cool Product"

#define USB_desc(name, bL, bD, bS) struct { \
    uint8_t  bLength; \
    uint8_t  bDescriptorType;  \
    wchar_t  bString[]; \
} name = {bL, bD, bS}

USB_desc(manufacturer_string, sizeof(STR_PRODUCT) + 2, 3, STR_PRODUCT);

除了,您可以使用外部程序生成字符串描述符(以及其他类似程序),输出一个C 源文件以供实施包含。只有当你有很多常量需要按摩时,这才有意义。

请注意,您可以将 uint16_t 的数组用于 USB 字符串描述符,而不是结构。 (所有你需要知道的是架构上的字节顺序,所以字节长度字节也是内存中的第一个字节:它是 big-endian 架构的高字节,以及小字节的低字节 -端架构。)

例如,考虑以下 awk 脚本,constants.awk:

#!/usr/bin/awk -f
BEGIN {
    # 0 = little endian, least significant byte first
    # 1 = big endian, most significant byte first
    BYTEORDER = 1

    RS = "[\t\v\f ]*(\r\n|\n\r|\r|\n)[\t\v\f ]*"
    FS = "[\t\v\f ]+"

    split("", codepoint)
    for (i = 1; i < 128; i++)
        codepoint[sprintf("%c", i)] = i
    # Note: Could add unicode code points U+00A0 to U+FFFF
    #       to codepoint[] array.

    printf "#ifndef   CONSTANTS_H\n"
    printf "#define   CONSTANTS_H\n"
    printf "\n"
    printf "/* Do not edit this file; edit constants.in instead.\n"
    printf "   This file is automatically generated by constants.awk.\n"
    printf "*/\n\n"
}

 == "usb_string_descriptor" && NF >= 3 {
    name = 
    value = [=10=]
    sub(/^[^"]*"/, "", value)   # Remove everything before first "
    sub(/"[^"]*$/, "", value)   # Remove everything after last "
    valuelen = length(value)
    type = 3

    printf "#define  %s_size  %d\n", name, 2*valuelen + 2
    printf "#define  %s_type  %d\n", name, type
    printf "#define  %s_len   %d\n", name, valuelen
    printf "static const uint16_t %s[%d] = {\n", name, valuelen + 1
    printf "    /* \"%s\" */\n", value
    if (BYTEORDER == 1)
        printf "    0x%02x%02x, /* length = %d bytes, type = 0x%02x */", 2*valuelen + 2, type, 2*valuelen + 2, type
    else
        printf "    0x%02x%02x, /* length = %d bytes, type = 0x%02x */", type, 2*valuelen + 2, 2*valuelen + 2, type
    for (i = 1; i <= valuelen; i++) {
        if ((i % 8) == 1)
            printf "\n    "
        printf "0x%04x, ", codepoint[substr(value, i, 1)]
    }
    printf "\n};\n\n"
}

END {
    printf "#endif /* CONSTANTS_H */\n"
}

假设您有一个文件,比如 constants.in,描述了上面的 scriptlet 应该定义的一些常量:

usb_string_descriptor manufacturer_string_1 "My Cool Product"
usb_string_descriptor manufacturer_string_2 "My Other Cool Product"

拥有您的构建机器 运行 例如awk -f constants.awk constants.in > constants.h。如果您使用 Makefile,则

AWK := awk

constants.h: constants.in
    $(AWK) -f constants.h constants.in > constants.h

应该可以解决问题。 (它甚至会导致重新生成 constant.h 头文件,如果您编辑 constants.in

在实际的C源文件实现中,你只需#include "constants.h".

以上会输出

#ifndef   CONSTANTS_H
#define   CONSTANTS_H

/* Do not edit this file; edit constants.in instead.
   This file is automatically generated by constants.awk.
*/

#define  manufacturer_string_1_size  32
#define  manufacturer_string_1_type  3
#define  manufacturer_string_1_len   15
static const uint16_t manufacturer_string_1[16] = {
    /* "My Cool Product" */
    0x2003, /* length = 32 bytes, type = 0x03 */
    0x004d, 0x0079, 0x0020, 0x0043, 0x006f, 0x006f, 0x006c, 0x0020, 
    0x0050, 0x0072, 0x006f, 0x0064, 0x0075, 0x0063, 0x0074, 
};

#define  manufacturer_string_2_size  44
#define  manufacturer_string_2_type  3
#define  manufacturer_string_2_len   21
static const uint16_t manufacturer_string_2[22] = {
    /* "My Other Cool Product" */
    0x2c03, /* length = 44 bytes, type = 0x03 */
    0x004d, 0x0079, 0x0020, 0x004f, 0x0074, 0x0068, 0x0065, 0x0072, 
    0x0020, 0x0043, 0x006f, 0x006f, 0x006c, 0x0020, 0x0050, 0x0072, 
    0x006f, 0x0064, 0x0075, 0x0063, 0x0074, 
};

#endif /* CONSTANTS_H */

(注意:我记得 USB 字符串描述符不需要终止 NUL 字符 (0)。我可能记错了。)

我使用了 awk,因为它以各种形式适用于所有平台——不仅是 GNU awk,还有 nawk、mawk 和 many more。您需要使用一种脚本语言来生成易于维护的脚本;上述版本旨在扩展到 USB 字符串描述符以外的其他类型。

我怀疑 Python 脚本从长远来看可能更容易维护,因为它比 awk(或 Perl,另一种用于这种预处理的典型脚本语言)更受欢迎。此外,Python 具有您可以使用的 ord() 内置函数,并允许您在 USB 字符串描述符中使用非 ASCII Unicode 字符。