JP / EN

広告

C/C++構造体をバイナリデータとしてPythonに渡す

タグ:C++ Python

C/C++で作った構造体やクラスのデータをPythonで読んで解析したいことがツール類などを作っていると間々ある。 Pythonは柔軟な動的型付け言語であるので、何バイトのどんなデータ型を扱っているは隠蔽されていることが多く Cのようにバイト単位でかっちりと制御可能なデータを受け取ろうとすると?となってしまうかもしれない。

このようなときのためにPythonの標準ライブラリとしてCの構造体を扱うstructパッケージが用意されている。 C構造体を例に説明するが、C++のクラスもC++内で呼びだし可能なメソッドが付いているとことと 多少のアクセス制御が付いていることを除けばCの構造体とさして変わらず、特にバイト列として吐き出してしまった後は ほぼ同一の扱いが可能である。

まず読み込みテスト用データをC++で構造体をバイナリファイルにダンプすることで作成する。
(余談 構造体しか使わないのでCのコードとしてコンパイルできるかと思ったが、構造体を初期化子リストに よって初期化することはC++でしか許されなかった。そのためC++のコードということにする)
      #include 

      struct Test
      {
        char c;
        int i;
        float f;
        double d;
        char str[16];
      };
      
      int main()
      {
        Test t = {1, 2, 3, 4, "abcd"};  // 適当なデータを入れる
        FILE *file = fopen("test.dat", "wb");
        fwrite(&t, sizeof(t), 1, file);
        fclose(file);
        return 0; 
      }
  
    $ g++ main.cpp  # コンパイル
    $ ./a.out  # 実行
  
これによって様々な数値データや文字列を含む構造体がtest.datに吐き出された。

次はPython上でこのデータを読み込む。もっとも簡便には structパッケージのstruct.unpack_from を使えばよい。これはstruct.unpack_from(format, buffer, offset=0) として呼びだすと、formatに指定された構造に合わせてバイト列bufferを解釈してタプルとして返してくれる。 offsetはオプショナルで、指定しておけばバイト列の先頭offsetバイト分を読み飛ばす。

関数unpack_fromを使う上で胆になるのはformatの指定方法だ。まず先頭1文字目は バイト列全体に関する設定・バイトオーダ、サイズ、アラインメントをしているために使われ、@, =, <, >, ! のどれかとなる。この設定はややこしいが、まずローカルマシンで生成されたデータだけを考えるなら@にしておけばよい。

各設定の詳細:ここでバイトオーダは複数バイトのデータの並び順に関する設定で、リトルエンディアンかビッグエンディアンである。 最近は大体のマシンでリトルエンディアンが使われ、ネットワーク関係でビッグエンディアンが使われると思えばよい。 これを"ネイティブ"にしておけば勝手に実行環境のエンディアンにそろえてくれる。
サイズは各データ型がどんなサイズを持つかの設定である。Cではintなどの型がどんな大きさを実際に持つかは処理系依存であるため これで設定できるようにしてあるのだ。この設定が"ネイティブ"ならこれも実行環境に合わせて決まり、"スタンダード"なら世の中でよくある数字に決め打ちされる。
アラインメントはC/C++の機能「データ構造アライメント」を考慮するかどうかである。これが適用されていると、 構造体内部のCPUにとって中途半端な長さのデータが扱いやすいようにパディングされている。gcc/g++ではデフォルトではonである。
もしどれでもない文字が1文字目に来ていた場合は@が設定される。

これらの各項目が
記号 バイトオーダ サイズ アラインメント
@ ネイティブ ネイティブ ネイティブ
= ネイティブ スタンダード なし
< リトルエンディアン スタンダード なし
> ビッグエンディアン スタンダード なし
! ネットワーク スタンダード なし

のように設定される。なおどれか一つを「ネイティブ」(実行環境に合わせる)に設定して他をそうしないのは意味がないと思われるため、 全てネイティブにするか、すべてネイティブ以外のどれかに手動設定するかのどちらかしか選択肢にない。 また@以外のときはアラインメントの対応はサポートされていない。どうしてもしたいときは後述の書式指定で パディングを使って手動でアラインメントを取る必要がある。

1文字目の後は"書式指定文字"を好きな数並べる。これはどんなデータ型がどんな順番で入っているかを指示するもので、 Cの構造体の定義を見ながら写せばよい。よく使うものだけメモしておくと
文字 型名 標準サイズ
c char 1
B unsigned char 1
i int 4
I unsigned int 4
f float 4
d double 8
x パディング 1

等がある。 全ての書式指定文字のリストは 公式ドキュメントを参照されたい。
また最後のxがアラインメント用のパディング、つまり使用されない余白用の1バイトである。 これをうまいこと入れるとアラインメントを手動で取れる(どう入れるかはややこしいので本稿では割愛(笑))。

ここまでを踏まえてやっと上のサンプルの構造体を読み込むPythonのコードが書ける:
    import struct

    fin = open("test.dat", "rb").read()
    data = struct.unpack_from("@clfdcccc", fin)
    print(data)
    # (b'\x01', 2, 3.0, 4.0, b'a', b'b', b'c', b'd')  とプリントされればOK
  
のようになる。これにより構造体の各メンバがタプルにまとめられて返されるので、あとはPython側で好きなように処理できる。


このエントリーをはてなブックマークに追加

https://wonderhorn.net/