bitsery

ライブラリシリアライゼーションバイナリC++11ヘッダーオンリー高速組み込み

ライブラリ

bitsery

概要

bitseryは、C++11用のヘッダーオンリーバイナリシリアライゼーションライブラリです。特にゲーム開発やリアルタイムデータ配信のネットワーク要件を念頭に設計されており、高速性と最小限のバイナリフットプリントを実現しています。外部依存関係がなく、ビットレベルでの細かい制御が可能で、信頼できないネットワークデータに対しても安全に使用できます。

詳細

bitseryは、cerealの使いやすさ、flatbuffersの前方/後方互換性、そして高速で小さなバイナリフットプリントを併せ持つように設計されています。ライブラリ自体はメモリアロケーションを行わないため、データの読み書きはバッファへのmemcpyと同等の速度で実行されます。

主な特徴:

  • ビットレベル制御: ビットパッキングによる極限までのデータサイズ削減
  • クロスプラットフォーム: コンパイル時にすべての要件を強制、メタデータ不要
  • コード生成不要: IDLやメタデータなしで直接型を使用可能
  • 設定可能なエラーチェック: 信頼できないデータに対する実行時エラーチェック(無効化可能)
  • 柔軟なI/O: ストリーム(ファイル、ネットワーク)やバッファ(vector、C配列)対応
  • 拡張性: 任意の型に対して簡単に拡張可能
  • 構文の選択: 詳細構文(s.value4b())と簡潔構文(s())の両方をサポート
  • 前方/後方互換性: Growable拡張による互換性サポート
  • エンディアン対応: 設定可能なエンディアン変換

パフォーマンス最適化:

  • ValueRange: 値の範囲を指定してビット数を削減(例:0-1000のint16_tを10ビットに)
  • CompactValue: 頻出する小さな値を効率的にエンコード
  • ビットパッキング: bool値やenumを最小限のビット数で保存

メリット・デメリット

メリット

  • 極めて高速: ベンチマークで他のライブラリを大幅に上回る性能
  • 最小バイナリサイズ: ビットレベル最適化により業界最小クラスのデータサイズ
  • メモリ効率: ライブラリ自体のメモリアロケーションなし
  • 安全性重視: 悪意のあるデータに対する保護機能内蔵
  • ヘッダーオンリー: 簡単な統合、追加の依存関係なし
  • 組み込み対応: 最小限のメモリフットプリントで組み込みシステムに最適
  • 柔軟な拡張: カスタム型やシリアライゼーション戦略の実装が容易
  • マクロ不使用: クリーンなコードベース

デメリット

  • バイナリ形式のみ: JSON、XMLなど他の形式はサポートしない
  • 型情報なし: すべての型情報はコードに記述する必要がある
  • 学習曲線: ビットレベル操作など高度な機能の習得に時間が必要
  • C++11必須: 一部の拡張機能はより新しいC++標準が必要
  • 動的サイズ制限: 安全性のため、動的コンテナには最大サイズの指定が必須

参考ページ

書き方の例

基本的なシリアライゼーション

#include <bitsery/bitsery.h>
#include <bitsery/adapter/buffer.h>
#include <bitsery/traits/vector.h>

enum class MyEnum : uint16_t { V1, V2, V3 };

struct MyStruct {
    uint32_t i;
    MyEnum e;
    std::vector<float> fs;
};

template <typename S>
void serialize(S& s, MyStruct& o) {
    s.value4b(o.i);              // 4バイト値
    s.value2b(o.e);              // 2バイト値
    s.container4b(o.fs, 10);     // 最大10要素のfloat配列
}

using Buffer = std::vector<uint8_t>;
using OutputAdapter = bitsery::OutputBufferAdapter<Buffer>;
using InputAdapter = bitsery::InputBufferAdapter<Buffer>;

int main() {
    MyStruct data{8941, MyEnum::V2, {15.0f, -8.5f, 0.045f}};
    MyStruct res{};
    Buffer buffer;
    
    // シリアライゼーション
    auto writtenSize = bitsery::quickSerialization<OutputAdapter>(buffer, data);
    
    // デシリアライゼーション
    auto state = bitsery::quickDeserialization<InputAdapter>({buffer.begin(), writtenSize}, res);
    
    assert(state.first == bitsery::ReaderError::NoError && state.second);
    assert(data.fs == res.fs && data.i == res.i && data.e == res.e);
    
    return 0;
}

詳細なシリアライザーとデシリアライザーの使用

#include <bitsery/bitsery.h>
#include <bitsery/adapter/stream.h>
#include <bitsery/traits/string.h>
#include <fstream>

struct GameData {
    std::string player_name;
    uint32_t score;
    uint16_t level;
    bool is_online;
    
    template<typename S>
    void serialize(S& s) {
        s.text1b(player_name, 50);  // 最大50文字
        s.value4b(score);
        s.value2b(level);
        s.value1b(is_online);
    }
};

int main() {
    GameData save_data{"プレイヤー1", 150000, 42, true};
    
    // ファイルへの保存
    {
        std::ofstream file("savegame.dat", std::ios::binary);
        bitsery::Serializer<bitsery::OutputStreamAdapter> ser{file};
        ser.object(save_data);
        
        // アダプタから実際に書き込まれたサイズを取得
        auto writtenSize = ser.adapter().writtenBytesCount();
        std::cout << "Written " << writtenSize << " bytes" << std::endl;
    }
    
    // ファイルからの読み込み
    GameData loaded_data{};
    {
        std::ifstream file("savegame.dat", std::ios::binary);
        
        // ファイルサイズを取得
        file.seekg(0, std::ios::end);
        size_t fileSize = file.tellg();
        file.seekg(0, std::ios::beg);
        
        bitsery::Deserializer<bitsery::InputStreamAdapter> des{file};
        des.object(loaded_data);
        
        auto state = std::make_pair(des.adapter().error(), 
                                   des.adapter().isCompletedSuccessfully());
        
        if (state.first == bitsery::ReaderError::NoError && state.second) {
            std::cout << "Successfully loaded: " << loaded_data.player_name << std::endl;
        }
    }
    
    return 0;
}

ビットパッキングと最適化

#include <bitsery/bitsery.h>
#include <bitsery/adapter/buffer.h>
#include <bitsery/ext/value_range.h>
#include <bitsery/ext/compact_value.h>

struct OptimizedData {
    uint16_t health;      // 0-100の範囲
    uint8_t ammo;        // 0-50の範囲
    bool flags[8];       // 8つのブールフラグ
    int32_t position_x;  // -1000から1000の範囲
    int32_t position_y;  // -1000から1000の範囲
};

template <typename S>
void serialize(S& s, OptimizedData& o) {
    // ValueRange拡張を使用して値の範囲を指定
    s.ext(o.health, bitsery::ext::ValueRange<uint16_t>{0, 100});  // 7ビットで保存
    s.ext(o.ammo, bitsery::ext::ValueRange<uint8_t>{0, 50});      // 6ビットで保存
    
    // ビットパッキングを使用して8つのboolを1バイトに
    s.enableBitPacking([&o](typename S::BPEnabledType& sbp) {
        for (int i = 0; i < 8; ++i) {
            sbp.boolValue(o.flags[i]);
        }
    });
    
    // CompactValue拡張で頻出する小さな値を効率的に保存
    s.ext(o.position_x, bitsery::ext::CompactValue{});
    s.ext(o.position_y, bitsery::ext::CompactValue{});
}

int main() {
    OptimizedData data{75, 30, {true, false, true, true, false, false, true, false}, 
                      -150, 200};
    
    std::vector<uint8_t> buffer;
    auto writtenSize = bitsery::quickSerialization(buffer, data);
    
    std::cout << "Optimized size: " << writtenSize << " bytes" << std::endl;
    // 通常なら16バイト以上必要なデータが大幅に圧縮される
    
    return 0;
}

ポリモーフィズムとポインタ

#include <bitsery/bitsery.h>
#include <bitsery/adapter/buffer.h>
#include <bitsery/ext/inheritance.h>
#include <bitsery/ext/std_smart_ptr.h>
#include <memory>

struct Base {
    virtual ~Base() = default;
    int base_value = 10;
    
    template<typename S>
    void serialize(S& s) {
        s.value4b(base_value);
    }
};

struct Derived1 : Base {
    float derived1_value = 3.14f;
    
    template<typename S>
    void serialize(S& s) {
        s.ext(*this, bitsery::ext::BaseClass<Base>{});
        s.value4b(derived1_value);
    }
};

struct Derived2 : Base {
    std::string derived2_value = "test";
    
    template<typename S>
    void serialize(S& s) {
        s.ext(*this, bitsery::ext::BaseClass<Base>{});
        s.text1b(derived2_value, 20);
    }
};

// ポリモーフィック型の登録
namespace bitsery {
namespace ext {
    template<typename TBase>
    struct PolymorphicBaseClass;
    
    template <>
    struct PolymorphicBaseClass<Base> {
        template<typename S>
        void serialize(S& s, Base& obj) {
            s.ext(obj, bitsery::ext::PolymorphicType{
                std::make_tuple(bitsery::ext::PolymorphicSubclass<Derived1>{},
                               bitsery::ext::PolymorphicSubclass<Derived2>{})
            });
        }
    };
}
}

int main() {
    std::unique_ptr<Base> ptr1 = std::make_unique<Derived1>();
    std::unique_ptr<Base> ptr2 = std::make_unique<Derived2>();
    
    std::vector<uint8_t> buffer;
    
    // シリアライゼーション
    auto writtenSize = bitsery::quickSerialization(buffer, ptr1, ptr2);
    
    // デシリアライゼーション
    std::unique_ptr<Base> loaded_ptr1;
    std::unique_ptr<Base> loaded_ptr2;
    
    auto state = bitsery::quickDeserialization({buffer.begin(), writtenSize}, 
                                              loaded_ptr1, loaded_ptr2);
    
    if (state.first == bitsery::ReaderError::NoError) {
        std::cout << "Successfully deserialized polymorphic objects" << std::endl;
    }
    
    return 0;
}

前方/後方互換性

#include <bitsery/bitsery.h>
#include <bitsery/adapter/buffer.h>
#include <bitsery/ext/growable.h>

// バージョン1のデータ構造
struct ConfigV1 {
    std::string app_name;
    uint32_t version;
    
    template<typename S>
    void serialize(S& s) {
        s.text1b(app_name, 50);
        s.value4b(version);
    }
};

// バージョン2のデータ構造(新しいフィールドを追加)
struct ConfigV2 {
    std::string app_name;
    uint32_t version;
    std::string new_field = "default";  // 新しいフィールド
    
    template<typename S>
    void serialize(S& s) {
        // Growable拡張を使用して互換性を保つ
        s.ext(*this, bitsery::ext::Growable{}, [](S& s, ConfigV2& o) {
            // 必須フィールド(すべてのバージョンで必要)
            s.text1b(o.app_name, 50);
            s.value4b(o.version);
        }, [](S& s, ConfigV2& o) {
            // オプションフィールド(新しいバージョンで追加)
            s.text1b(o.new_field, 100);
        });
    }
};

int main() {
    // 古いバージョンのデータを新しいバージョンで読む
    ConfigV1 old_config{"MyApp", 1};
    std::vector<uint8_t> buffer;
    
    // V1形式で保存
    auto writtenSize = bitsery::quickSerialization(buffer, old_config);
    
    // V2形式で読み込み(互換性あり)
    ConfigV2 new_config;
    auto state = bitsery::quickDeserialization({buffer.begin(), writtenSize}, new_config);
    
    if (state.first == bitsery::ReaderError::NoError) {
        std::cout << "Successfully loaded old version data" << std::endl;
        std::cout << "New field has default value: " << new_config.new_field << std::endl;
    }
    
    return 0;
}

カスタム型の拡張

#include <bitsery/bitsery.h>
#include <bitsery/adapter/buffer.h>
#include <chrono>

// std::chrono::time_pointのカスタムシリアライゼーション
namespace bitsery {
namespace ext {
    template<typename Clock, typename Duration>
    class TimePointExt {
    public:
        template<typename Ser, typename T>
        void serialize(Ser& s, T& tp) {
            auto duration = tp.time_since_epoch();
            auto count = duration.count();
            s.value8b(count);
            if (s.adapter().isReading()) {
                tp = T(Duration(count));
            }
        }
    };
}
}

struct Event {
    std::string name;
    std::chrono::system_clock::time_point timestamp;
    
    template<typename S>
    void serialize(S& s) {
        s.text1b(name, 100);
        s.ext(timestamp, bitsery::ext::TimePointExt<
              std::chrono::system_clock, 
              std::chrono::system_clock::duration>{});
    }
};

int main() {
    Event event{"重要なイベント", std::chrono::system_clock::now()};
    
    std::vector<uint8_t> buffer;
    auto writtenSize = bitsery::quickSerialization(buffer, event);
    
    Event loaded_event;
    auto state = bitsery::quickDeserialization({buffer.begin(), writtenSize}, loaded_event);
    
    if (state.first == bitsery::ReaderError::NoError) {
        std::cout << "Event: " << loaded_event.name << std::endl;
    }
    
    return 0;
}