Bitcoin Core Serialize.h
クラスのデータをシリアライズするための仕組み。シリアライズとはディスクに保管するためやネットワークを経由して外部に送信したりするための行われる変換処理のこと。Bitcoin においてもブロックやチェーンのデータを始め、様々なデータがシリアライズされる。
シリアライズ対象のクラスへの記述
このシリアライズの仕組みを使うためにはいくつかのルールに従ってシリアライズ対象のクラスに記述を追加する必要があるみたい。一つが ADD_SERIALIZE_METHODS マクロを使ってシリアライズのためのインターフェースをクラスに追加することで、もう1つは SerializationOp 関数を対象クラスの中に定義し、シリアライズのルールを決める。
code:cpp
ADD_SERIALIZE_METHODS;
シリアライズのためのメソッドを追加するマクロ。実装は以下のようになっている。見てわかるように SerializationOp を呼び出すためのラッパー関数を定義している。
code:cpp
/**
* Implement three methods for serializable objects. These are actually wrappers over
* "SerializationOp" template, which implements the body of each class' serialization
* code. Adding "ADD_SERIALIZE_METHODS" in the body of the class causes these wrappers to be
* added as members.
*/
template<typename Stream> \
void Serialize(Stream& s) const { \
NCONST_PTR(this)->SerializationOp(s, CSerActionSerialize()); \
} \
template<typename Stream> \
void Unserialize(Stream& s) { \
SerializationOp(s, CSerActionUnserialize()); \
}
SerializationOp の実装例を見てみる。以下は block.h にあるブロックヘッダの例。READWRITE というマクロにヘッダの各フィールドを渡している。動作を把握するためにはREADWRITEを確認する必要がある。
code:cpp
template <typename Stream, typename Operation>
inline void SerializationOp(Stream& s, Operation ser_action) {
READWRITE(this->nVersion);
READWRITE(hashPrevBlock);
READWRITE(hashMerkleRoot);
READWRITE(nTime);
READWRITE(nBits);
READWRITE(nNonce);
}
マクロの定義を見てみると SerReadWriteMany 関数を呼び出しています。(関数を呼ぶのではなくマクロを経由しているのはどういう理由なんでしょ?インターフェースを簡素化するため?)
また、READWRITEAS という似たマクロも定義されていますが、これの使用法はまだよくわかっていません。
code:cpp
#define READWRITE(...) (::SerReadWriteMany(s, ser_action, __VA_ARGS__)) #define READWRITEAS(type, obj) (::SerReadWriteMany(s, ser_action, ReadWriteAsHelper<type>(obj))) SerReadWriteMany はシリアライズ用とデシリアライズ用でオーバーロード定義されており、ここで処理が分岐しているようです。
また最後の引数は可変引数になっており、READWRITE(nTime, nBits, nNonce) のように複数の引数を渡せるようになっているようです。このような使い方をしている箇所は実際には見当たりませんが。
以降はシリアライズに絞って見ていきます。
code:cpp
template<typename Stream, typename... Args>
inline void SerReadWriteMany(Stream& s, CSerActionSerialize ser_action, const Args&... args)
{
::SerializeMany(s, args...);
}
template<typename Stream, typename... Args>
inline void SerReadWriteMany(Stream& s, CSerActionUnserialize ser_action, Args&&... args)
{
::UnserializeMany(s, args...);
}
SeralizeMany を見ていきます。オーバーロードしているのは再帰的にすべての引数に対して Serialize を呼ぶためですね。つまり、READWRITE(...) マクロに渡された引数にたいして、 Serialize() 関数を読んでいるだけです。
code:cpp
template<typename Stream>
void SerializeMany(Stream& s)
{
}
template<typename Stream, typename Arg, typename... Args>
void SerializeMany(Stream& s, const Arg& arg, const Args&... args)
{
::Serialize(s, arg);
::SerializeMany(s, args...);
}
そして、その Serialize() 関数がどのように定義されているかというと、こんなかんじです。第2引数に実際のシリアライズ対象のフィールドが渡ってくるわけですが、その型ごとにオーバーロードしています。言い換えると、用意されているインターフェースはこれだけなので、自分で定義したデータをシリアライズするためには、ここで定義されている型まで分解してあげる必要があるということかと思います。
下に挙げたコードでは int8_t のようなプリミティブなデータ型が並んでいますが、string や vector といったハイレベルなデータ型やコンテナにおいても Serialize が定義されています。
code:cpp
template<typename Stream> inline void Serialize(Stream& s, char a ) { ser_writedata8(s, a); } // TODO Get rid of bare char
template<typename Stream> inline void Serialize(Stream& s, int8_t a ) { ser_writedata8(s, a); }
template<typename Stream> inline void Serialize(Stream& s, uint8_t a ) { ser_writedata8(s, a); }
template<typename Stream> inline void Serialize(Stream& s, int16_t a ) { ser_writedata16(s, a); }
template<typename Stream> inline void Serialize(Stream& s, uint16_t a) { ser_writedata16(s, a); }
template<typename Stream> inline void Serialize(Stream& s, int32_t a ) { ser_writedata32(s, a); }
template<typename Stream> inline void Serialize(Stream& s, uint32_t a) { ser_writedata32(s, a); }
template<typename Stream> inline void Serialize(Stream& s, int64_t a ) { ser_writedata64(s, a); }
template<typename Stream> inline void Serialize(Stream& s, uint64_t a) { ser_writedata64(s, a); }
template<typename Stream> inline void Serialize(Stream& s, float a ) { ser_writedata32(s, ser_float_to_uint32(a)); }
template<typename Stream> inline void Serialize(Stream& s, double a ) { ser_writedata64(s, ser_double_to_uint64(a)); }
template<typename Stream, int N> inline void Serialize(Stream& s, const char (&a)N) { s.write(a, N); } template<typename Stream, int N> inline void Serialize(Stream& s, const unsigned char (&a)N) { s.write(CharCast(a), N); } template<typename Stream> inline void Serialize(Stream& s, const Span<const unsigned char>& span) { s.write(CharCast(span.data()), span.size()); }
template<typename Stream> inline void Serialize(Stream& s, const Span<unsigned char>& span) { s.write(CharCast(span.data()), span.size()); }
// つづく...
ハイレベルな方のSerializeの例として vector のケースを見てみます。
下にコードを貼りました。3つめの関数がファサードとして機能しており、上2つが実装の実態であるようです。1つめの関数はvector が vector<unsigned char> のときに呼ばれます。
余談ですが、vector<unsigned char> は Bitcoin Core において可変長のバイナリデータを扱うために多用される型で、よくvchという名前の変数として宣言されます。
2つめの関数は、それ以外の型の場合で、vectorの各アイテムに対して、再び Serialize を呼ぶ様になっています。
ここを読む限り、vectorをシリアライズする場合には、内部のアイテムの型は最終的には unsigned char になる必要がありそうです。
code:cpp
/**
* vector
*/
template<typename Stream, typename T, typename A>
void Serialize_impl(Stream& os, const std::vector<T, A>& v, const unsigned char&)
{
WriteCompactSize(os, v.size());
if (!v.empty())
os.write((char*)v.data(), v.size() * sizeof(T));
}
template<typename Stream, typename T, typename A, typename V>
void Serialize_impl(Stream& os, const std::vector<T, A>& v, const V&)
{
WriteCompactSize(os, v.size());
for (typename std::vector<T, A>::const_iterator vi = v.begin(); vi != v.end(); ++vi)
::Serialize(os, (*vi));
}
template<typename Stream, typename T, typename A>
inline void Serialize(Stream& os, const std::vector<T, A>& v)
{
Serialize_impl(os, v, T());
}
READWRITEAS() を使うケース
script.h の CScript で使っている。
VARINT() があるケース
code:cpp
READWRITE(VARINT(nBlocks));
のように VARINT() マクロを挟んでいるケースが有る。このケースがどういう場合のケースなのか確認したい。名前から推測するに可変長の数値を扱いたいということのように見えるが、ここでの nBlocks は unsigned int なので4バイトの固定長であるはず。VARINT は不要に見える。
内部で CSizeComputer というクラスを使っているが、このクラスのコメントを見る限り、シリアライズしようとしている整数値が型に対して極めて小さいときにデータサイズにオーバーヘッドが生まれるため、これを詰める処理をしているということのように見える。
Stream について
Serialize に関係する関数をみると Stream というテンプレートの型名が多く使われていることに気がつくと思います。これは stream.h で定義されている各種ストリームクラスを使うことを意図されているようです。各ストリームクラスは void write(const char* pch, size_t nSize) 及び void read(char* pch, size_t nSize)というインターフェースを共通して持っています(ダックタイピング)。
以下はシンプルな Stream である CVectorWriter を使って Serialize された16進数データを取り出すサンプル。
header は CBlockHeader のインスタンス。
code:cpp
std::vector<unsigned char> vch;
CVectorWriter stream(SER_NETWORK, INIT_PROTO_VERSION, vch, 0);
header.Serialize(stream);
std::cout << "CBlockHeader serialize: " << HexStr(vch.begin(), vch.end()) << std::endl;