21. Binary file¶
この章では、データを最小限のコストでファイルとやり取りする方法を学びます。
21.1 バイナリファイルとは¶
バイナリファイル は、データをテキストではなく バイナリデータ 形式で保存したものです。例えば 299792458
という int32
型の数値は、"299792458"
という 9 文字(9 バイト) ではなく、00010001110111100111100001001010
のビット列で表される 4 バイトのデータとして保存します。
データをファイルに保存する際、テキスト形式で保存すると、テキストから数値、数値からテキストへの変換にコストがかかり、必要なサイズも大きくなります。一方、バイナリデータは変換のコストがかからず、コンパクトな固定サイズのデータ容量しか必要としません。
int32
や double
などのプリミティブ型や、プリミティブ型で構成された trivially copyable
なクラス (Point
, Vec2
, Rect
, ColorF
) などは、単純にデータをコピーするだけで容易にバイナリデータとして扱えますが、Array
や String
など、ポインタで内部データを管理するデータ型をバイナリデータとして適切に扱には少し手間がかかります。
この章の後半て説明する シリアライズ機能 を使うと、Array
や String
, その他いくつかの 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 バイナリファイルに複雑なデータを書き込む¶
String
や Array
のように単純にコピーできない (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 バイナリファイルから複雑なデータを読み込む¶
前項に続いて、String
や Array
のように単純にコピーできない (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()
に、シリアライズに対応したオブジェクトを渡すコードを書くと、そのクラスはシリアライズ可能になり、Serializer
や Deserializer
で読み書きできるようになります。
# 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())
{
}
}