32

私の問題のコンテキストは、ネットワーク プログラミングにあります。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

実際の例

この状況が他の場所でどのように処理されるかを確認するために、ネットワーク コードの例を探していました。軽量 ipには、いくつかの同様のケースがあります。udp.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 *です。私の理解とこの答えに進むと、これは間違いなく[edit-not] strict-aliasing を破っているため、未定義の動作をしています。

4

2 に答える 2

9

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

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

§6.3.2.3 ポインター

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

そして§3.14

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

§6.5

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

§6.5

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

§J.2 未定義の動作

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

結論

a との間でキャストすることは (明確に定義された) OK ですが、C99void*で type の値を使用することは OK ではありません。したがって、「実際の例」は未定義の動作ではありません。したがって、アラインメント、パディング、およびバイト順が考慮されている限り、明示的なキャスト方法を次の変更で使用できます。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)
        /* ... */
}
于 2013-10-07T23:04:10.747 に答える
5

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

単一のホストでこれを実行し、バイト順が問題にならない場合でも、接続の両端が同じオプションで構築されていることと、構造体が同じ方法でパディングされている、型が同じサイズであるなど。実際のシリアライゼーションの実装を検討するのに少なくとも少し時間をかけることをお勧めします。目の前の大きなアップデート。

于 2013-10-03T17:58:21.913 に答える