web-dev-qa-db-ja.com

バッファを構造体として解釈するための正しくてポータブルな方法

私の問題の文脈はネットワークプログラミングにあります。 2つのプログラム間でネットワークを介してメッセージを送信したいとします。簡単にするために、メッセージがこのように見え、バイト順序は問題ではないとしましょう。これらのメッセージをC構造体として定義するための、正しく、移植性があり、効率的な方法を見つけたいと思います。私はこれに対する4つのアプローチを知っています:明示的なキャスト、ユニオンを介したキャスト、コピー、およびマーシャリング。

struct message {
    uint16_t logical_id;
    uint16_t command;
};

明示的なキャスト:

void send_message(struct message *msg) {
    uint8_t *bytes = (uint8_t *) msg;
    /* call to write/send/sendto here */
}

void receive_message(uint8_t *bytes, size_t len) {
    assert(len >= sizeof(struct message);
    struct message *msg = (struct message*) bytes;
    /* And now use the message */
    if (msg->command == SELF_DESTRUCT)
        /* ... */
}

私の理解では、send_messageはエイリアス規則に違反していません。これは、バイト/文字ポインタが任意の型をエイリアスする可能性があるためです。ただし、その逆は真ではないため、receive_messageはエイリアシング規則に違反し、未定義の動作をします。

ユニオンを介したキャスト:

union message_u {
    struct message m;
    uint8_t bytes[sizeof(struct message)];
};

void receive_message_union(uint8_t *bytes, size_t len) {
    assert(len >= sizeof(struct message);
    union message_u *msgu = bytes;
    /* And now use the message */
    if (msgu->m.command == SELF_DESTRUCT)
        /* ... */
}

しかし、これは、組合には常にそのメンバーの1人しか含まれていないという考えに違反しているようです。さらに、これは、ソースバッファがワード/ハーフワード境界で整列されていない場合、整列の問題につながる可能性があるようです。

コピー:

void receive_message_copy(uint8_t *bytes, size_t len) {
    assert(len >= sizeof(struct message);
    struct message msg;
    memcpy(&msg, bytes, sizeof msg);
    /* And now use the message */
    if (msg.command == SELF_DESTRUCT)
        /* ... */
}

これにより正しい結果が得られることが保証されているようですが、もちろん、データをコピーする必要がないことを強く望んでいます。

マーシャリング

void send_message(struct message *msg) {
    uint8_t bytes[4];
    bytes[0] = msg.logical_id >> 8;
    bytes[1] = msg.logical_id & 0xff;
    bytes[2] = msg.command >> 8;
    bytes[3] = msg.command & 0xff;
    /* call to write/send/sendto here */
}

void receive_message_marshal(uint8_t *bytes, size_t len) {
    /* No longer relying on the size of the struct being meaningful */
    assert(len >= 4);    
    struct message msg;
    msg.logical_id = (bytes[0] << 8) | bytes[1];    /* Big-endian */
    msg.command = (bytes[2] << 8) | bytes[3];
    /* And now use the message */
    if (msg.command == SELF_DESTRUCT)
        /* ... */
}

まだコピーする必要がありますが、構造体の表現から切り離されています。しかし今、私たちは各メンバーの位置とサイズを明確にする必要があり、エンディアンネスははるかに明白な問題です。

関連情報:

厳密なエイリアシングルールとは何ですか?

標準に違反せずに構造体へのポインタを使用して配列をエイリアシングする

char *は厳密なポインタエイリアスに対していつ安全ですか?

http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html

実例

この状況が他の場所でどのように処理されるかを確認するために、ネットワークコードの例を探していました。 light-weight ip にも同様のケースがいくつかあります。 dp.c ファイルには、次のコードがあります。

/**
 * Process an incoming UDP datagram.
 *
 * Given an incoming UDP datagram (as a chain of pbufs) this function
 * finds a corresponding UDP PCB and hands over the pbuf to the pcbs
 * recv function. If no pcb is found or the datagram is incorrect, the
 * pbuf is freed.
 *
 * @param p pbuf to be demultiplexed to a UDP PCB (p->payload pointing to the UDP header)
 * @param inp network interface on which the datagram was received.
 *
 */
void
udp_input(struct pbuf *p, struct netif *inp)
{
  struct udp_hdr *udphdr;

  /* ... */

  udphdr = (struct udp_hdr *)p->payload;

  /* ... */
}

ここで、struct udp_hdrはudpヘッダーのパック表現であり、p->payloadはタイプvoid *です。私の理解と this の答えを続けると、これは間違いなく[編集-ではない]厳密なエイリアスを破っているため、未定義の動作があります。

25
croyd

これは私が避けようとしていたことだと思いますが、私はついに行って、 C99標準 自分自身を見ました。これが私が見つけたものです(強調が追加されました):
§6.3.2.2無効

1 void式(void型の式)の(存在しない)値は、いかなる方法でも使用してはならず、暗黙的または明示的な変換(voidを除く)をそのような式に適用してはなりません。他のタイプの式がvoid式として評価される場合、その値または指定子は破棄されます。 (void式はその副作用について評価されます。)

§6.3.2.3ポインタ

1voidへのポインタは、不完全またはオブジェクト型へのポインタとの間で変換できます。不完全なタイプまたはオブジェクトタイプへのポインタは、voidへのポインタに変換されて再び戻る可能性があります。結果は元のポインタと同じになります。

そして§3.14

1つのオブジェクト
実行環境のデータストレージの領域。その内容は値を表すことができます

§6.5

オブジェクトの格納値には、次のいずれかのタイプの左辺値式によってのみアクセスできます。
オブジェクトの有効なタイプと互換性のあるタイプ、
—オブジェクトの有効なタイプと互換性のあるタイプの認定バージョン。
—オブジェクトの有効な型に対応する符号付きまたは符号なしの型である型。
—オブジェクトの有効な型の修飾バージョンに対応する符号付きまたは符号なしの型である型。
—前述のタイプの1つを含む集合体または共用体タイプ
メンバー(再帰的に、サブアグリゲートまたは含まれているユニオンのメンバーを含む)、または
—文字タイプ。

§6.5

保存された値にアクセスするためのオブジェクトの有効なタイプは、宣言されたタイプです。
オブジェクト(ある場合)。 文字型ではない型を持つ左辺値を介して、宣言された型を持たないオブジェクトに値が格納されている場合、左辺値の型は、そのアクセスおよびのためのオブジェクトの有効な型になります。保存された値を変更しない後続のアクセス。 memcpyまたはmemmoveを使用して、宣言された型を持たないオブジェクトに値がコピーされる場合、または文字型の配列としてコピーされる場合、そのアクセスおよび値を変更しない後続のアクセスの変更されたオブジェクトの有効な型は値のコピー元のオブジェクトの有効なタイプ(ある場合)。宣言された型を持たないオブジェクトへの他のすべてのアクセスの場合、オブジェクトの有効な型は、単にアクセスに使用される左辺値の型です。

§J.2未定義の振る舞い

— void式の値を使用しようとするか、暗黙的または明示的な変換(voidを除く)がvoid式に適用されます(6.3.2.2)。

結論

void*との間でキャストすることは問題ありませんがC99でタイプvoidの値を使用することは問題ありません。したがって、「実世界の例」は未定義の動作ではありません。したがって、アラインメント、パディング、およびバイトオーダーが処理される限り、明示的なキャスト方法を次の変更で使用できます。

void receive_message(void *bytes, size_t len) {
    assert(len >= sizeof(struct message);
    struct message *msg = (struct message*) bytes;
    /* And now use the message */
    if (msg->command == SELF_DESTRUCT)
        /* ... */
}
7
croyd

唯一の正しい方法は、ご想像のとおり、データをcharバッファーから構造体にコピーすることです。他の選択肢は、厳密なエイリアスルール、または1人の組合員がアクティブなルールに違反しています。

もう一度、単一のホストでこれを実行し、バイト順序が重要ではない場合でも、接続の両端が同じオプションで構築されていることと、構造体が構築されていることを確認する必要があることを思い出してください。は同じ方法でパディングされ、タイプは同じサイズなどです。実際のシリアル化の実装を検討する場合は、少なくとも少し時間をかけて、より幅広い条件をサポートする必要がある場合に、それならあなたの目の前で大きなアップデート。

4
Mark B