ノータブルコード12 - データ構造を分かりやすくドキュメントする技 - 2020-09-25 - ククログ

ククログ

株式会社クリアコード > ククログ > ノータブルコード12 - データ構造を分かりやすくドキュメントする技

ノータブルコード12 - データ構造を分かりやすくドキュメントする技

第12回目のノータブルコードでは、データ構造を分かりやすくするドキュメントのテクニックを紹介します。

C言語とシリアライズ処理

C言語でよくあるパターンとして、データをバイト列にシリアライズするという作業があります。多くの場合、この作業はデータフォーマットの仕様と密接に絡み合っています。フォーマット仕様書にある「最初の1バイトは8ビット識別用の固定値。次はフォーマットを表す3バイトの符号で埋める。その次が...」という指示に基づいて配列を埋めていくのが典型です。

シリアライズの例

この処理は、もちろん正確に実装する必要があるのですが、一方でヒューマンエラーの入り込みやすい部分でもあります。例えば、二つの隣り合うフィールドを入れ違いで埋めてしまった、データが実は1バイトずれていたというのは、実にありがちなミスです。最初は慎重に実装しても、フォーマットの拡張に対応するために、何度か手を加えるうちにいつのまにか壊れていた、というケースもままあります。できれば、こういったミスは未然に防げるようにしたいものです。

データ構造を明示するテクニック

私の好きなRFCに、GZIPのRFCがあります。その説明から少し引用すると、例えば以下のようなものです。

2.3. Member format

      Each member has the following structure:

         +---+---+---+---+---+---+---+---+---+---+
         |ID1|ID2|CM |FLG|     MTIME     |XFL|OS | (more-->)
         +---+---+---+---+---+---+---+---+---+---+
      ...

      2.3.1. Member header and trailer

         ID1 (IDentification 1)
         ID2 (IDentification 2)
            These have the fixed values ID1 = 31 (0x1f, \037), ID2 = 139
            (0x8b, \213), to identify the file as being in gzip format.

         CM (Compression Method)
            This identifies the compression method used in the file.  CM
            = 0-7 are reserved.  CM = 8 denotes the "deflate"
            compression method, which is the one customarily used by
            gzip and which is documented elsewhere.

私は、データ構造をドキュメントする方法としては、このような図1による視覚的な説明が最も優れていると思います。形式言語や自然言語の文章による説明よりもずっと分かりやすいと感じます。この主たる理由は、おそらくプログラミングで言うところのデータフォーマットが、私たちの住む物理的な世界とは根本的に異質な概念だからだと思います。

実際、このRFCの作者であるL. ピーター・ドイチュは、プログラミングの本質的な難しさについて「(現実世界では)見回したところでアドレスやポインタみたいなものを目にすることはない」2と説明しています。メモリアドレスやバイト列のような概念は、私たちの住む日常的な世界には存在しないので、これを視覚的なイメージで置き換えることが有効なのです。

実際のコード例

このような理由で、私はバイナリのデータフォーマットを扱う時は図でドキュメントするようにしています。例えば、Fluent BitのGELFフォーマットを扱うプラグインについては、次のようにドキュメントを付しました。

/*
 * A GELF header is 12 bytes in size. It has the following
 * structure:
 *
 * +---+---+---+---+---+---+---+---+---+---+---+---+
 * | MAGIC |           MESSAGE ID          |SEQ|NUM|
 * +---+---+---+---+---+---+---+---+---+---+---+---+
 *
 * NUM is the total number of packets to send. SEQ is the
 * unique sequence number for each packet (zero-indexed).
 */
#define GELF_MAGIC "\x1e\x0f"
#define GELF_HEADER_SIZE 12

static void init_chunk_header(uint8_t *buf, uint8_t count)
{
    uint64_t msgid = message_id();

    memcpy(buf, GELF_MAGIC, 2);
    memcpy(buf + 2, &msgid, 8);
    buf[10] = 0;
    buf[11] = count;
}

コミットメッセージにも書いていますが、実は、この改修前のバージョンのフォーマット実装には、いくつかの不具合がありました(コードレビューをすり抜けて、2年以上バグが残っていたという部分にこの種の実装の難しさを感じます)。このような誰にでも起こることが分かっているミスは、様々な工夫をこらしてぜひとも防ぎたいものです。

これと似たテクニックとして、Brubeckのコメントの付け方の記事があります。興味のある方はこちらも参照ください。

  1. この図の詳しい読み方はRFCの「2.1. Overall Convention」に説明があります。この記事の範囲では、一マスが一バイトを表していると理解すればOKです。

  2. ピーター・サイベル著、青木靖訳「Coders at work - プログラミングの技をめぐる探求」(2011年、オーム社) p415