Skip to content

21. バイナリファイル

この章では、データを最小限のコストでファイルとやり取りする方法を学びます。

21.1 バイナリファイルとは

バイナリファイル は、データをテキストではなく バイナリデータ 形式で保存したものです。例えば 299792458 という int32 型の数値は、"299792458" という 9 文字(9 バイト) ではなく、00010001110111100111100001001010 のビット列で表される 4 バイトのデータとして保存します。

データをファイルに保存する際、テキスト形式で保存すると、テキストから数値、数値からテキストへの変換にコストがかかり、必要なサイズも大きくなります。一方、バイナリデータは変換のコストがかからず、コンパクトな固定サイズのデータ容量しか必要としません。

int32double などのプリミティブ型や、プリミティブ型で構成された trivially copyable なクラス (Point, Vec2, Rect, ColorF) などは、単純にデータをコピーするだけで容易にバイナリデータとして扱えますが、ArrayString など、ポインタで内部データを管理するデータ型をバイナリデータとして適切に扱には少し手間がかかります。

この章の後半て説明する シリアライズ機能 を使うと、ArrayString, その他いくつかの Siv3D の trivially copyable でないクラスを簡単にバイナリデータとして扱えるようになります。独自に定義した型をシリアライズに対応させることもできます。

21.2 バイナリファイルに単純な値を書き込む

ファイルにバイナリデータを書き込むには BinaryWriter の機能を使います。BinaryWriter のコンストラクタ引数に、書き込み先のファイルのパスを渡します。このファイルパスは、実行ファイルがあるフォルダ(開発中は App フォルダ)を基準とする相対パスか、絶対パスを使用します。ファイルが使用中だったり、ファイル名が不正なものだったりしてオープンに失敗したかどうかは if (!writer) で調べられます。

同名のファイルがすでに存在する場合はそれを破棄してからオープンし、存在しない場合は新しい空のファイルを作成してオープンします。

.BinaryWriter::write()trivially copyable な型の値を渡すと、その値のバイナリデータをコピーしてファイルの末尾に追加で書き込みます。

# include <Siv3D.hpp>

// ゲームのスコアを記録する構造体
struct GameScore
{
    int32 a, b, c, d;
};

void Main()
{
    // 書き込み用のバイナリファイルをオープン
    BinaryWriter writer(U"test.bin");

    if (!writer) // もしオープンに失敗したら
    {
        throw Error(U"Failed to open `test.bin`");
    }

    // int32 型の値 (4 バイト) を書き込む
    writer.write(777);

    // double 型の値 (8 バイト) を書き込む
    writer.write(3.1415);

    // Point 型の値 (8 バイト) を書き込む
    writer.write(Point(123, 456));

    // GameScore 型の値 (16 バイト) を書き込む
    const GameScore s = { 10, 20, 30, 40 };
    writer.write(s);

    while (System::Update())
    {

    } 

    // writer のデストラクタで自動的にファイルがクローズされる
}

21.3 バイナリファイルから単純な値を読み込む

バイナリファイルからバイナリデータを読み込むには BinaryReader の機能を使います。BinaryReader のコンストラクタ引数に、読み込みたいテキストファイルのパスを渡します。このファイルパスは、実行ファイルがあるフォルダ(開発中は App フォルダ)を基準とする相対パスか、絶対パスを使用します。リリース用のアプリを作るときには埋め込みリソースパスの使用を推奨します。ファイルが存在しない場合など、オープンに失敗したかどうかは if (!reader) で調べられます。

オープン直後は、読み込み位置はファイルの先頭にセットされています。BinaryReader::read()trivially copyable な型の変数を参照で渡すと、読み込み位置を始点とし、その値のサイズ分のバイナリデータを読み込んでその変数にコピーし、読み込み位置をその分進めて true を返します。読み込み位置がすでにファイルの終端にあって、これ以上読み込めないときには false を返します。

# include <Siv3D.hpp>

// ゲームのスコアを記録する構造体
struct GameScore
{
    int32 a, b, c, d;
};

void Main()
{
    // バイナリファイルをオープン
    BinaryReader reader(U"test.bin");

    if (!reader) // もしオープンに失敗したら
    {
        throw Error(U"Failed to open `test.bin`");
    }

    // int32 型の値 (4 バイト) を読み込む
    int32 n;
    reader.read(n);
    Print << n;

    // double 型の値 (8 バイト) を読み込む
    double d;
    reader.read(d);
    Print << d;

    // Point 型の値 (8 バイト) を読み込む
    Point pos;
    reader.read(pos);
    Print << pos;

    // GameScore 型の値 (16 バイト) を読み込む
    GameScore s;
    reader.read(s);
    Print << U"{}, {}, {}, {}"_fmt(s.a, s.b, s.c, s.d);

    while (System::Update())
    {

    }

    // reader のデストラクタで自動的にファイルがクローズされる
}

読み込み位置の移動

BinaryReader::setPos() で、読み込み位置を指定した場所に移動できます。 BinaryReader::skip() で、指定したサイズ分読み飛ばすことができます。

# include <Siv3D.hpp>

// ゲームのスコアを記録する構造体
struct GameScore
{
    int32 a, b, c, d;
};

void Main()
{
    // バイナリファイルをオープン
    BinaryReader reader(U"test.bin");

    if (!reader) // もしオープンに失敗したら
    {
        throw Error(U"Failed to open `test.bin`");
    }

    // 先頭から 4 バイト進んだ位置に移動
    reader.setPos(4);

    // double 型の値 (8 バイト) を読み込む
    double d;
    reader.read(d);
    Print << d;

    // 8 バイト分読み飛ばす
    reader.skip(8);

    // GameScore 型の値 (16 バイト) を読み込む
    GameScore s;
    reader.read(s);
    Print << U"{}, {}, {}, {}"_fmt(s.a, s.b, s.c, s.d);

    while (System::Update())
    {

    }

    // reader のデストラクタで自動的にファイルがクローズされる
}

21.4 ファイルをオープン・クローズするタイミングを制御する

BinaryReader のコンストラクタでファイルをオープンせずに、BinaryReader::open() でファイルをオープンすることもできます。オープンに成功した場合は true, 失敗した場合は false を返します。

また通常は BinaryReader のデストラクタが実行されるタイミングでファイルがクローズされますが、例えば内容を読み込んだあとにファイルを削除したり、別の内容を書き込みたい場合には、ファイルがオープンされたままだと操作ができないため、すぐにクローズしたいというケースがあるでしょう。そうしたときには BinaryReader::close() でファイルを明示的にクローズします。

# include <Siv3D.hpp>

void Main()
{
    BinaryReader reader;

    if (!reader.open(U"test.bin")) // もしオープンに失敗したら
    {
        throw Error(U"Failed to open `test.bin`");
    }

    // int32 型の値 (4 バイト) を読み込む
    int32 n;
    reader.read(n);
    Print << n;

    // ファイルを明示的にクローズ
    reader.close();

    while (System::Update())
    {

    }
}

21.5 バイナリファイルに複雑なデータを書き込む

StringArray のように単純にコピーできない (trivially copyable でない) データをバイナリファイルで扱うのは少し手間がかかります。後述するシリアライズ機能を使えば簡単になりますが、まずはシリアライズ機能を使わないサンプルを紹介します。

# include <Siv3D.hpp>

void Main()
{
    // 書き込み用のバイナリファイルをオープン
    BinaryWriter writer(U"test-no-serialize.bin");

    if (!writer) // もしオープンに失敗したら
    {
        throw Error(U"Failed to open `test-no-serialize.bin`");
    }

    // 書き込みたいテキスト
    const String text = U"Hello, Siv3D";

    // 書き込みたいテキストの長さ
    const uint64 length = text.length();

    // テキストの長さを書き込む
    writer.write(length);

    // テキストデータを書き込む
    writer.write(text.data(), sizeof(char32) * length);

    while (System::Update())
    {

    }

    // writer のデストラクタで自動的にファイルがクローズされる
}

21.6 バイナリファイルから複雑なデータを読み込む

前項に続いて、StringArray のように単純にコピーできない (trivially copyable でない) データを、シリアライズ機能を使わずにバイナリファイルで扱う方法です。

# include <Siv3D.hpp>

void Main()
{
    // バイナリファイルをオープン
    BinaryReader reader(U"test-no-serialize.bin");

    if (!reader) // もしオープンに失敗したら
    {
        throw Error(U"Failed to open `test-no-serialize.bin`");
    }

    // 書き込みたいテキストの長さ
    uint64 length = 0;

    // 書き込みたいテキスト
    String text;

    // テキストの長さを読み込む
    reader.read(length);

    if (0 < length)
    {
        // テキストデータの読み込みのためにリサイズ
        text.resize(length);

        // テキストのサイズ分だけデータを読み込む
        reader.read(text.data(), sizeof(char32) * length);
    }

    Print << length;

    Print << text;

    while (System::Update())
    {

    }

    // reader のデストラクタで自動的にファイルがクローズされる
}

21.7 バイナリファイルに複雑なデータを書き込む(シリアライズ機能)

シリアライズ機能を使うと、シリアライズに対応したデータ型 (trivially copyable でない型も含む) を、少ない記述でバイナリファイルで扱えます。ファイルにシリアライズ機能を使ってバイナリデータを書き込むには Serializer<BinaryWriter> の機能を使います。Serializer<BinaryWriter> のコンストラクタ引数に、書き込み先のファイルのパスを渡します。実行ファイルがあるフォルダ(開発中は App フォルダ)を基準とする相対パスか、絶対パスを使用します。ファイルが使用中だったり、ファイル名が不正なものだったりしてオープンに失敗したかどうかは if (!writer.getWriter()) で調べられます。 Serializer<BinaryWriter>::operator() で値を渡すと、そのデータをシリアライズしてファイルの末尾に追加で書き込みます。

# include <Siv3D.hpp>

void Main()
{
    // 書き込み用のバイナリファイルをオープン
    Serializer<BinaryWriter> writer(U"test-serialize.bin");

    if (!writer.getWriter()) // もしオープンに失敗したら
    {
        throw Error(U"Failed to open `test-serialize.bin`");
    }

    // 書き込みたいテキスト
    const String text = U"Hello, Siv3D";

    // バイナリファイルにシリアライズ対応型のデータを書き込む
    writer(text);

    while (System::Update())
    {

    }

    // writer のデストラクタで自動的にファイルがクローズされる
}

21.8 バイナリファイルから複雑なデータを読み込む(シリアライズ機能)

シリアライズ機能を使って書き込んだデータを読み込むには Deserializer<BinaryReader> の機能を使います。Deserializer<BinaryReader> のコンストラクタ引数に、読み込みたいテキストファイルのパスを渡します。このファイルパスは、実行ファイルがあるフォルダ(開発中は App フォルダ)を基準とする相対パスか、絶対パスを使用します。リリース用のアプリを作るときには埋め込みリソースパスの使用を推奨します。ファイルが存在しない場合など、オープンに失敗したかどうかは if (!reader.getReader()) で調べられます。 Deserializer<BinaryReader>::operator() で値を渡すと、そのデータをシリアライズしてファイルの末尾に追加で書き込みます。

# include <Siv3D.hpp>

void Main()
{
    // バイナリファイルをオープン
    Deserializer<BinaryReader> reader(U"test-serialize.bin");

    if (!reader.getReader()) // もしオープンに失敗したら
    {
        throw Error(U"Failed to open `test-serialize.bin`");
    }

    // 読み込み先のテキスト
    String text;

    // バイナリファイルからシリアライズ対応型のデータを読み込む
    // (自動でリサイズが行われる)
    reader(text);

    Print << text.length();

    Print << text;

    while (System::Update())
    {

    }

    // reader のデストラクタで自動的にファイルがクローズされる
}

21.9 ユーザ定義型をシリアライズに対応させる

ユーザが定義したクラスをシリアライズに対応させるには、template <class Archive> void SIV3D_SERIALIZE(Archive& archive) という public メンバ関数をクラスに実装します。archive() に、シリアライズに対応したオブジェクトを渡すコードを書くと、そのクラスはシリアライズ可能になり、SerializerDeserializer で読み書きできるようになります。

# include <Siv3D.hpp>

// ユーザデータとゲームのスコアを記録する構造体
struct GameScore
{
    String name;

    int32 id;

    int32 score;

    // シリアライズに対応させるためのメンバ関数を定義する
    template <class Archive>
    void SIV3D_SERIALIZE(Archive& archive)
    {
        archive(name, id, score);
    }
};

void Main()
{   
    {
        // 記録したいデータ
        const Array<GameScore> scores =
        {
            GameScore{ U"Player1", 111, 1000 },
            GameScore{ U"Player2", 222, 2000 },
            GameScore{ U"Player3", 333, 3000 },
        };

        // バイナリファイルをオープン
        Serializer<BinaryWriter> writer(U"score.bin");

        if (!writer.getWriter()) // もしオープンに失敗したら
        {
            throw Error(U"Failed to open `score.bin`");
        }

        // シリアライズに対応したデータを記録
        writer(scores);

        // writer のデストラクタで自動的にファイルがクローズされる
    }

    // 読み込み先のデータ
    Array<GameScore> scores;
    {
        // バイナリファイルをオープン
        Deserializer<BinaryReader> reader(U"score.bin");

        if (!reader.getReader()) // もしオープンに失敗したら
        {
            throw Error(U"Failed to open `score.bin`");
        }

        // バイナリファイルからシリアライズ対応型のデータを読み込む
        // (自動でリサイズが行われる)
        reader(scores);

        // reader のデストラクタで自動的にファイルがクローズされる
    }

    // 読み込んだスコアを確認
    for (const auto& score : scores)
    {
        Print << U"{}(id: {}): {}"_fmt(score.name, score.id, score.score);
    }

    while (System::Update())
    {

    }   
}