1. ホーム
  2. c

[解決済み] X-Macrosの実戦的な活用法

2023-05-27 10:50:51

質問

今知ったのですが X-Macros . X-Macrosの実際の使用例を教えてください。どのような場合にX-Macrosが最適なのでしょうか?

どのように解決するのですか?

私が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};

問題は、関数ポインタテーブルの順序を、状態の列挙の順序と一致するように維持しなければならないことが、非常にエラーになりやすいと考えたことです。

私の友人がX-macrosを紹介してくれ、それは私の頭の中で電球が切れたようなものでした。真面目な話、私の人生において x-macros はどこにいたのでしょうか!

それで今、私は次のような表を定義しています。

#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
};

のように、プリプロセッサに関数のプロトタイプを作らせることもできます。

#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

しかし、私のお気に入りの使い方は、通信ハンドラに関して言えば

まず、各コマンド名とコードを含むcommsテーブルを作成します。

#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
}

そして私の関数プロトタイプ。

#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

このユニオンはオフセット構造体のようなもので、インスタンス化されず、sizeof演算子を使って送信バッファのサイズを宣言します。

uint8_t tx_buf[sizeof(tx_buf_t)];

これで送信バッファ tx_buf は最適なサイズになり、この通信ハンドラにコマンドを追加すると、バッファは常に最適なサイズになります。かっこいいですね。

もうひとつの用途は、オフセットテーブルを作成することです。 組み込みシステムではメモリに制約があることが多いので、疎な配列の場合、ジャンプ テーブルに 512 バイト (ポインターごとに 2 バイト X 256 コマンド) を使用したくありません。 その代わりに、可能なコマンドごとに8ビットオフセットのテーブルを用意することにします。 このオフセットは、実際のジャンプテーブルのインデックスとして使用され、NUM_COMMANDS * sizeof(pointer)である必要があります。 私の場合、10個のコマンドを定義しています。 ジャンプテーブルの長さは20バイト、オフセットテーブルの長さは256バイトで、合計で512バイトではなく276バイトになります。 そして、このように関数を呼び出します。

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()" という関数を一つ作っておけば、無効なコマンドバイトがオフセットテーブルのインデックスに使われたときに呼び出されます。