構造体のポータブルなバイナリ読み書き

MPLの力技で構造体のバイナリ読み書きをやってみました。
<hamigaki/binary_io.hpp>
<hamigaki/struct_traits.hpp>
使い方はこんな感じです。

#include <hamigaki/binary_io.hpp>
#include <boost/mpl/list.hpp>
#include <boost/cstdint.hpp>
#include <iostream>
#include <fstream>
#include <cassert>
#include <cstring>

// LZH Level2ヘッダ
// 構造体にパディングが入っても、バイナリとメンバの順が違ってもよい
// ただし、個々のメンバのサイズは一致しないと駄目
struct level2_header
{
    boost::uint16_t header_size;
    char signature[5];
    boost::uint32_t compressed_size;
    boost::uint32_t file_size;
    boost::int32_t update_time;
    boost::uint8_t reserved;
    boost::uint8_t level;
    boost::uint16_t crc;
    char os_type;
};

namespace hamigaki {

// 構造体ごとに行う特殊化
template<>
struct struct_traits<level2_header>
{
private:
    typedef level2_header self;

    static const hamigaki::endianness endian = hamigaki::little;

public:
    // メンバの型、メンバポインタ、エンディアンのリスト
    typedef boost::mpl::list<
        hamigaki::member<self, boost::uint16_t, &self::header_size, endian>,
        hamigaki::member<self, char[5], &self::signature>,
        hamigaki::member<self, boost::uint32_t, &self::compressed_size, endian>,
        hamigaki::member<self, boost::uint32_t, &self::file_size, endian>,
        hamigaki::member<self, boost::int32_t, &self::update_time, endian>,
        hamigaki::member<self, boost::uint8_t, &self::reserved>,
        hamigaki::member<self, boost::uint8_t, &self::level>,
        hamigaki::member<self, boost::uint16_t, &self::crc, endian>,
        hamigaki::member<self, char, &self::os_type>
    > members;
};

} // namespace hamigaki

int main(int argc, char* argv[])
{
    if (argc != 2)
        return 1;

    // ファイルからchar配列に読み込み
    // struct_size<T>はTの外部バイナリサイズを返すメタ関数
    char data[hamigaki::struct_size<level2_header>::type::value];
    std::ifstream is(argv[1], std::ios_base::binary);
    is.read(data, sizeof(data));

    // char配列から構造体をバイナリ読み込み
    level2_header header;
    hamigaki::binary_read(data, header);

    // 読み出したデータを表示
    std::cout << "header_size = " << header.header_size << std::endl;
    (std::cout << "signature = ").write(header.signature, 5) << std::endl;

    // char配列へ構造体をバイナリ書き出し
    char data2[hamigaki::struct_size<level2_header>::type::value];
    hamigaki::binary_write(data2, header);

    // 結果比較
    assert(std::memcmp(data, data2, sizeof(data)) == 0);
}

必要なことはstruct_traitsの特殊化だけです。
メンバ変数の情報をmpl::listで持っていて、これを頼りにメンバ変数を反復処理します。
このリストは反復に必要なので手書きしないと駄目です。


MPLシーケンス中の各型に対して実行時の処理を行いたい場合はmpl::for_eachが便利です。
mpl::for_eachで扱えるよう、

template<class Struct>
class read_member
{
public:
    read_member(const char* data, Struct& x) : data_(data), ptr_(&x)
    {
    }

    template<class Type, Type Struct::* PtrToMember, endianness E>
    void operator()(const member<Struct, Type, PtrToMember, E>&)
    {
        typedef member<Struct, Type, PtrToMember, E> member_type;

        binary_io_traits<E,Type>::read(
            data_ + member_offset<member_type>::type::value,
            ptr_->*PtrToMember
        );
    }

private:
    const char* data_;
    Struct* ptr_;
};

みたいなファンクタを用意して、

template<endianness E, class T>
struct binary_io_traits
{
    static void read(const char* s, T& x)
    {
        typedef typename struct_traits<T>::members members;

        boost::mpl::for_each<members>(detail::read_member<T>(s, x));
    }
};

で各メンバを読み出しています。(writeも同じ)
あとは基本型や配列用にbinary_io_traitsを用意するだけです。


#構造体を入れ子にした場合にバグありです。
入れ子構造体のサイズにsizeofを使うとパディングまで数えてしまうので。