2008-07-12
近況
新刊が多く慌しい. 谷川史子の "草の上星の下", 岩本ナオの "町でうわさの天狗の子", あとは Google の "Protocol Buffers". 谷川史子の洗練を綴るには余白が狭過ぎる. かわりに Protocol Buffers の話をすこし.
Protocol Buffers
Protocol Buffers (以下 protobuf) は Google 製のオブジェクトシリアライザ. 名前からは RPC を連想しそうだけれど, RPC そのものではない. もっともオブジェクトを直列化して送受信するのが RPC だから, あとは送受信だけあればいい. 実装は含まれないものの, protobuf にも RPC を前提としたインターフェイスがいくつか含まれている. ...といった細かい話は ドキュメント や インタビュー を見ればわかる. 今日はコードを見てみることに.
なお, 例の Style Guide から 想像できるとおり, protobuf は特段モダンな C++ ではない. boost が好みの先鋭的 C++ 愛好家には物足りないだろう. 主題もオブジェクトのシリアライザという再発明の親玉なので, 食傷気味の人は多いとおもう. (ほんとに...) とはいえ少しは面白いところもあったので紹介してみたい.
概観
まず行数から.
omo@contentiss:~/src/protobuf-2.0.0beta$ lines.rb -d src/ 6006 /google/protobuf/compiler/cpp 923 /google/protobuf/compiler/python 3690 /google/protobuf/compiler/java 16312 /google/protobuf/compiler 6534 /google/protobuf/io 496 /google/protobuf/testing 3940 /google/protobuf/stubs 55766 /google/protobuf 55766 /google 1814 /gtest/internal 10125 /gtest 65891
6.5 万行くらいある. これは C++ の部分だけで, python や java のランタイムは含まない.
compiler 以下は .proto ファイルのコンパイラ. stubs 以下は Google 社内のツリーから抜粋したと思しき細かなユーティリティ. io 以下は stream の実装. testing は単体テストのヘルパ. 残りがランタイムになる. テストやら何やらを除いたランタイム本体は, およそ 2 万行くらいの感触.
ランタイムのコードの多くは C++ のオブジェクトに対するリフレクションの実装に割かれている. protobuf の生成するオブジェクトは, このランタイムによって reflective に操作できるようになる.
みどころ
オブジェクトのシリアライザや RPC には, 実装を特徴づける設計上の意思決定がある. コード本体を眺める前にいくつか列挙しておく. こういうのを気にしながら読んでます, という参考まで.
直列化の自己記述度: 直列化の書式がどれだけ自己記述的か. バイト列を見ただけで何のデータかわかるデータは自己記述的だ. HTML や XML は自己記述的なデータの代表例. 構造体を memcpy() しだけのデータは, ふつう中身をみてもさっぱりわからない. 自己記述的でない. 各種画像フォーマットはその間くらい. フォーマットを知らなければさっぱり読めないけれど, フォーマットがわかれば画像のサイズやピクセル幅, そのほか各種メタデータをとりだすことができる. Java オブジェクトの直列化書式もそれなりに自己記述的で, たとえばクラス名やバージョン, プロパティのデータ型などがひとそろい入っている. 自己記述度が高いほど空間効率や速度は下がる.
バージョニングへの対処: 古いデータを新しいコードで読みこめるか. 逆に新しいデータを古いコードで読めるか. 素朴な memcpy() データはまったくバージョニングされていない. 定義が変わると使えなくなる. そもそもバージョン違いを検出できるかも怪しい. HTML はかなり堅牢. 昔のブラウザで最新のページを見ても, いちおう文章くらいは拾える. 上位互換はほぼ完璧. Java のオブジェクトもそこそこ頑張っており, プロパティが追加されたくらいならスルーしてくれる.
オブジェクトグラフの自由度: 直列化するオブジェクトが他のオブジェクトを参照しているときどうするか. 特に, 参照の循環や合流がおきたらどうなるか. 合流がコピーになり, 循環を許さない実装がある一方で, そうした複雑なオブジェクトグラフを保存できる実装もある. Java は後者 (だったはず...)
オブジェクトモデルの自己記述度: C++ 固有の話題として, 独自実装したリフレクションの表現能力やオーバーヘッドが気になる. フィールドを取得するにはどうするのか. メソッドは呼べるのか. アクセス制限, 継承関係は再現されるのか. リフレクションを実現するために個々のオブジェクトが消費するオーバーヘッドはどれだけか. シリアライザを作るだけならリフレクションいらなくね? など. C++ 以外の言語 (protobuf なら Java や Python) はふつう言語レベルから reflective なので, こういう悩みは少ない. 言語機能を使えば済む.
ランタイムとコード生成の役割分担: 各種高級言語のシリアライザがを見ればわかるとおり, オブジェクトが reflective ならシリアライザにコード生成は必要ない. けれど性能上の理由で自動生成を使うことはある. こうしたトレードオフの線引きが個性になる
Message: オブジェクトモデル
protobuf のコンパイラ protoc で生成されるクラスは, すべて Message インターフェイス(状態をもたない抽象クラス)を実装している.
// message.h class LIBPROTOBUF_EXPORT Message { public: .... virtual Message* New() const = 0; ... virtual void MergeFrom(const Message& from); ... virtual bool MergePartialFromCodedStream(io::CodedInputStream* input); ... virtual bool SerializeWithCachedSizes(io::CodedOutputStream* output) const; virtual int GetCachedSize() const = 0; ... virtual const Descriptor* GetDescriptor() const = 0; ... virtual const Reflection* GetReflection() const = 0; ... };
まず多態を実現する最低限の仕組みとして, コンストラクタに相当する New() と Clone() の亜種 MergeFrom() がある. ファクトリでなくオブジェクト自身に New() があるたりは設計に prototype 指向のノリが混じっている.
リフレクションは GetReflection() や GetDescriptor() でとりだしたオブジェクトを介して行う. Descriptor オブジェクトが Java の Class オブジェクトに相当し, メタクラスを記述する. Reflection オブジェクトは Message オブジェクトごとにインスタンス化され, リフレクションで使うオブジェクト単位の状態を保持する. ちょっとややこしい.
肝心の直列化は SerializeWithCachedSizes() が, 復元は MergePartialFromCodedStream() が受け持つ. この二つが純粋仮想関数でないのは面白い. (低級言語だと典型的なシリアライザはこの部分のコードを生成するよね.) 実装ではリフレクションを使い直列化を行う.
// message.cc bool Message::MergePartialFromCodedStream(io::CodedInputStream* input) { return WireFormat::ParseAndMergePartial( // WireFormat はいわゆるヘルパクラス GetDescriptor(), input, GetReflection()); } ... bool Message::SerializeWithCachedSizes(io::CodedOutputStream* output) const { return WireFormat::SerializeWithCachedSizes( GetDescriptor(), GetReflection(), GetCachedSize(), output); }
Message には様々なユーティリティメソッドが定義されており, ファイルや配列への直列化を行うこともできる. こうしたユーティリティはどれも 最終的に上の二つの仮想関数を呼びだす.
// Message class ... // Serialize the message and store it in the given byte array. All required // fields must be set. bool SerializeToArray(void* data, int size) const; // Serialize the message and write it to the given file descriptor. All // required fields must be set. bool SerializeToFileDescriptor(int file_descriptor) const; ...
Reflection: リフレクションのインターフェイス
Message オブジェクトに対する reflective な操作は, Reflection インターフェイスに分離されている.
class LIBPROTOBUF_EXPORT Message::Reflection { public: ... virtual const UnknownFieldSet& GetUnknownFields() const = 0; ... virtual bool HasField(const FieldDescriptor* field) const = 0; ... virtual void ListFields(vector<const FieldDescriptor*>* output) const = 0; ... virtual int32 GetInt32 (const FieldDescriptor* field) const = 0; ... virtual string GetString(const FieldDescriptor* field) const = 0; ... virtual const Message& GetMessage(const FieldDescriptor* field) const = 0; ... virtual void SetInt32 (const FieldDescriptor* field, int32 value) = 0; ... virtual int32 GetRepeatedInt32 (const FieldDescriptor* field, int index) const = 0; ... virtual void SetRepeatedInt32 (const FieldDescriptor* field, int index, int32 value) = 0; ... virtual void AddInt32 (const FieldDescriptor* field, int32 value) = 0; ... private: GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(Reflection); };
FieldDescriptor が Java でいう Field クラス. SetXxx() や GetXxx() で該当フィールドの値を操作する.
repepated field:
SetRepeatedInt32() などの repeated メソッドは, 要するに配列. "配列" というオブジェクトを用意するかわりに, "repepated field" という概念を持ち込んでいる. ださい気はするけど効率はいいかも. ちなみに連想配列は用意されていない.
optional field:
protobuf には optional フィールドという概念があり, 特定のフィールドを空にしておくことができる. 空のフィールドは直列化されない. この仕組みと MergeFrom() を使ったオブジェクトの上書きによって, プログラマの利便性を損ねずに空間効率の高い直列化を実現できる. たとえば差分の保存や送信がやりやすい. HasField() は, その(オプション)フィールドがセットされているかを確認する.
unknown field:
protobuf には "unknown field" という概念があり, バージョニングや互換性の扱いを支援している. 直列化されたデータを読み込む際に定義にないフィールドがみつかると, Message は "unknown field" としてそれを保存しておく. 再び直列化するときは, unknown field も書き出される. そのため, 古いバージョンのプログラムが新しいデータを読み書きしても内容が損われることはない.
GeneratedMessageReflection : リフレクションの実装
Reflection インターフェイスを実装しているのは GeneratedMessageReflection クラス.
class LIBPROTOBUF_EXPORT GeneratedMessageReflection : public Message::Reflection { ... // メソッド定義がずらずら const Descriptor* descriptor_; void* base_; const void* default_base_; const int* offsets_; ... uint32* has_bits_; ExtensionSet* extensions_; ... UnknownFieldSet unknown_fields_; .... };
void* を見て嫌な予感がしている人は正しい. たとえば base_ は対応する Message インスタンスの this を指している. offsets_ は this の中にある各フィールドへのオフセット配列.
GetInt32() などの reflective な操作は, 巡り巡って GeneratedMessageReflection::GetField() メソッドを呼ぶ. (const なしの MutableField() メソッドもある. 実装は同じ.)
// These simple template accessors obtain pointers (or references) to // the given field. template <typename Type> inline const Type& GeneratedMessageReflection::GetRaw( const FieldDescriptor* field) const { const void* ptr = reinterpret_cast<const uint8*>(base_) + offsets_[field->index()]; return *reinterpret_cast<const Type*>(ptr); };
だいたい想像どおりだったと思う... まあ言語の上でバイト列に構造を与えようとすれば, こうなるのは仕方ない. MySQL のレコードなんかも大枠では似たようなかんじだった, はず.
offset_ 配列の値は生成されたコードが静的に計算し, 外側から GeneratedMessageReflection に与えられる. あと unknown_fields_ は Message (のサブクラス) じゃなくてこっちに持たせておくんだね.
Descriptor: メタクラス
既に述べたように, Descriptor は Java の Class クラスに相当する. 同様のクラスとして FileDescriptor, EnumDescriptor, FieldDescriptor, EnumDescriptor などが 用意されている. それぞれが .proto ファイルにあらわれる construct に対応している. これらのクラスはコンパイラでも使われる. だからメタクラスと見てもいいし, .proto ファイルの抽象構文木だと思ってもいい. データ中心の立場からスキーマとみなすこともできる.
各 descriptor クラスには, 対応する protobuf メッセージが用意されている. (descriptor.proto 参照) たとえば Descriptor には DescriptorProto メッセージクラスがある.
// descriptor.proto ... // Describes a message type. message DescriptorProto { optional string name = 1; repeated FieldDescriptorProto field = 2; repeated FieldDescriptorProto extension = 6; ... } ...
これらのメッセージを使って各 descriptor を永続化することができる.
// descriptor.h class LIBPROTOBUF_EXPORT Descriptor { ... void CopyTo(DescriptorProto* proto) const; };
メッセージからの復元は DescriptorBuilder が行う.
// descriptor.cc class DescriptorBuilder { ... // FileDescriptor は java の package に相当. 自身の内包する各 descriptor を持つ. const FileDescriptor* BuildFile(const FileDescriptorProto& proto); ... };
Message のメタクラス自身が Message になれる再帰性がちょっと面白い. オブジェクトモデルの醍醐味. 面白いだけでなく使い道もある. 後でみるように, protobuf の直列化書式は効率重視で注釈が乏しい. しかし直列化したメッセージに直列化した Descriptor を添付すれば, アプリケーションレベルで自己記述的なデータを作ることができる.
DescriptorPool:
descriptor は DescriptorPool に登録する...というよりは, DescriptorPool が Descriptor の ファクトリになっている. Java の類比でいくと ClassLoader みたいなもの. 特殊なインスタンスとして, protoc で生成したクラスを登録する generated_pool() というシングルトンがあるようす.
class LIBPROTOBUF_EXPORT DescriptorPool { ... static const DescriptorPool* generated_pool(); ... const FileDescriptor* FindFileByName(const string& name) const; ... const FileDescriptor* BuildFile(const FileDescriptorProto& proto); ... scoped_ptr<Tables> tables_; ... };
生成されたコード
ランタイムをざっと眺めたのはいいけれど, 生成されたコードはランタイムをどう使うのだろう. examples に入っている addressbook.proto をコンパイルし, 生成された addressbook.pb.h と addresssbook.pb.cc を見てみる.
コンパイルするのはこんなの:
// addressbook.proto message Person { required string name = 1; required int32 id = 2; // Unique ID number for this person. optional string email = 3; ... } ... message AddressBook { repeated Person person = 1; }
クラスの概観を眺め, descriptor の構築方法と直列化ルーチンの実装を覗けば 生成されたコードがどんなものかわかるだろう.
というわけで, まず概観を求めヘッダファイルから:
// addressbook.pb.h class Person : public ::google::protobuf::Message { // Message の subclass ... inline static const Person& default_instance() { // プロトタイプの根っこ. return default_instance_; } // メソッドの宣言がずらずらと... // フィールドのアクセサ // required string name = 1; inline bool has_name() const; inline void clear_name(); inline const ::std::string& name() const; inline void set_name(const ::std::string& value); inline void set_name(const char* value); inline ::std::string* mutable_name(); .... private: ::google::protobuf::internal::GeneratedMessageReflection _reflection_; mutable int _cached_size_; // シリアライズ時のサイズ. // フィールドの実体. ::std::string* name_; static const ::std::string _default_name_; ::google::protobuf::int32 id_; ::std::string* email_; static const ::std::string _default_email_; ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber > phone_; ... static const Person default_instance_; static const int _offsets_[4]; // reflection で使うフィールドへのオフセット. ::google::protobuf::uint32 _has_bits_[(4 + 31) / 32]; // optional を実現するフラグ .... }
Message インターフェイスを実装するメソッドの他, フィールドへのアクセサやいくつかの static method が生成されている. フィールド本体以外にも, has_bits_ など Message インターフェイスの規定するオブジェクトモデルを 実現するための値がいくつかある.
Descriptor のインスタンス化:
次に descriptor の構築方法を調べよう. とりあえず GetDescriptor() から追いかけていく.
// addressbook.pb.cc const ::google::protobuf::Descriptor* Person_PhoneNumber::GetDescriptor() const { return descriptor(); }
はいはい...
.... const ::google::protobuf::Descriptor* Person_PhoneNumber::descriptor() { if (Person_PhoneNumber_descriptor_ == NULL) proto_BuildDescriptors_addressbook_2eproto(); return Person_PhoneNumber_descriptor_; }
はいはい...
... void proto_BuildDescriptors_addressbook_2eproto() { ... const ::google::protobuf::FileDescriptor* file = pool->InternalBuildGeneratedFile( "\n\021addressbook.proto\022\010tutorial\"\332\001\n\006Person" "\022\014\n\004name\030\001 \002(\t\022\n\n\002id\030\002 \002(\005\022\r\n\005email\030\003 \001(" "\t\022+\n\005phone\030\004 \003(\0132\034.tutorial.Person.Phone" "Number\032M\n\013PhoneNumber\022\016\n\006number\030\001 \002(\t\022.\n" "\004type\030\002 \001(\0162\032.tutorial.Person.PhoneType:" "\004HOME\"+\n\tPhoneType\022\n\n\006MOBILE\020\000\022\010\n\004HOME\020\001" "\022\010\n\004WORK\020\002\"/\n\013AddressBook\022 \n\006person\030\001 \003(" "\0132\020.tutorial.PersonB)\n\024com.example.tutor" "ialB\021AddressBookProtos", 342); Person_descriptor_ = file->message_type(0); Person_PhoneNumber_descriptor_ = Person_descriptor_->nested_type(0); ::google::protobuf::MessageFactory::InternalRegisterGeneratedMessage( Person_PhoneNumber_descriptor_, &Person_PhoneNumber::default_instance()); .... }
はて. これは何をしているのだろう.
// descriptor.cc const FileDescriptor* DescriptorPool::InternalBuildGeneratedFile( const void* data, int size) { ... FileDescriptorProto proto; GOOGLE_CHECK(proto.ParseFromArray(data, size)); const FileDescriptor* result = BuildFile(proto); GOOGLE_CHECK(result != NULL); return result; }
なるほど. コンパイラは直列化した FileDescriptorProto をコードに埋め込んでおき, 実行時にはそれを解釈して FileDescriptor を作っている. FileDescriptor を構築する "コード" 自体は出力しないんだね. ちょっと面白い. コンパイラも内部モデルに descriptor を使っているから, 簡単に descriptor の直列化バイト列を生成できる. このへんはうまい設計だと思う.
ところでシングルトンの初期化がスレッドセーフじゃないのが気になった人もいるとおもう.
// addressbook.pb.cc // Force BuildDescriptors() to be called at static initialization time. struct StaticDescriptorInitializer_addressbook_2eproto { StaticDescriptorInitializer_addressbook_2eproto() { proto_BuildDescriptors_addressbook_2eproto(); } } static_descriptor_initializer_addressbook_2eproto_;
グローバル変数のコンストラクタを使ったイディオムを使って初期化時に登録を済ませている. 各種組込み機器への移植を考えていた人は残念でした...
オフセットの計算:
よりみちで, Reflection が参照していたオフセットの計算方法も覗いておく.
// addressbook.pb.cc const int Person::_offsets_[4] = { GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Person, name_), GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Person, id_), GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Person, email_), GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(Person, phone_), };
// generated_messagere_flection.h #define GOOGLE_PROTOBUF_GENERATED_MESSAGE_FIELD_OFFSET(TYPE, FIELD) \ (reinterpret_cast<const char*>( \ &reinterpret_cast<const TYPE*>(16)->FIELD) - \ reinterpret_cast<const char*>(16))
コメントによれば, 0 を使うと gcc が NULL だと怒るから 16 らしい. あとは align してないとダメだとか.
直列化:
いよいよ直列化のコードをみよう...と思ったけれど直列化より復元の方が面白そうなので, そっちを覗くことにした. (直列化はだいたいこの逆になる.)
bool Person_PhoneNumber::MergePartialFromCodedStream( ::google::protobuf::io::CodedInputStream* input) { #define DO_(EXPRESSION) if (!(EXPRESSION)) return false ::google::protobuf::uint32 tag; while ((tag = input->ReadTag()) != 0) { switch (::google::protobuf::internal::WireFormat::GetTagFieldNumber(tag)) { // required string number = 1; case 1: { if (::google::protobuf::internal::WireFormat::GetTagWireType(tag) != ::google::protobuf::internal::WireFormat::WIRETYPE_LENGTH_DELIMITED) { goto handle_uninterpreted; } DO_(::google::protobuf::internal::WireFormat::ReadString(input, mutable_number())); if (input->ExpectTag(16)) goto parse_type; break; } // optional .tutorial.Person.PhoneType type = 2 [default = HOME]; case 2: { if (::google::protobuf::internal::WireFormat::GetTagWireType(tag) != ::google::protobuf::internal::WireFormat::WIRETYPE_VARINT) { goto handle_uninterpreted; } parse_type: int value; DO_(::google::protobuf::internal::WireFormat::ReadEnum(input, &value)); if (::tutorial::Person_PhoneType_IsValid(value)) { set_type(static_cast< ::tutorial::Person_PhoneType >(value)); } else { mutable_unknown_fields()->AddField(2)->add_varint(value); } if (input->ExpectAtEnd()) return true; break; } ...
基本的にはフィールド ID で switch し, 各 case の中ではフィールドの型に対応した WireFormat::ReadXxx() を呼びだしている. エラーの処理は unknown field に戻ったり false を返したり色々.
未知の ID をもつフィールドは WireFormat::SkipField() で unknown field によけておく.
default: { handle_uninterpreted: if (::google::protobuf::internal::WireFormat::GetTagWireType(tag) == ::google::protobuf::internal::WireFormat::WIRETYPE_END_GROUP) { return true; } DO_(::google::protobuf::internal::WireFormat::SkipField( input, tag, mutable_unknown_fields())); break; } } } return true; #undef DO_ }
未知のフィールドは一見すると型がわからない. どうやって値を解釈するのだろう. SkipField() は...
// wire_format.cc bool WireFormat::SkipField(io::CodedInputStream* input, uint32 tag, UnknownFieldSet* unknown_fields) { UnknownField* field = (unknown_fields == NULL) ? NULL : unknown_fields->AddField(GetTagFieldNumber(tag)); switch (GetTagWireType(tag)) { ... case WIRETYPE_FIXED64: { uint64 value; if (!input->ReadLittleEndian64(&value)) return false; if (field != NULL) field->add_fixed64(value); return true; } ... } ... }
んー...
// wire_format.h inline WireFormat::WireType WireFormat::GetTagWireType(uint32 tag) { return static_cast<WireType>(tag & kTagTypeMask); }
なるほど. tag の値に型が埋めこんであるんだね. よくみると GetTagFieldNumber() も似た処理をしていた.
// wire_format.h inline int WireFormat::GetTagFieldNumber(uint32 tag) { return static_cast<int>(tag >> kTagTypeBits); }
tag の32bit 整数にはフィールドの ID と型 ID の両方が入っていたわけか.
さて, デフォルト設定のままだと上のような直列化/復元のコードは生成されない. 先に登場した, リフレクションを使う実装の Message::MergePartialFromCodedStream() などが使われる.
専用の復元コードを出力するには, .proto に以下の行を追加する.
option optimize_for = SPEED;
生成コードとランタイムのトレードオフはユーザが指定するというわけ.
CodedOutputStream, CodedInputStream : 直列化プリミティブの実装
生成されたコードの中では WireFormat::ReadXxx() が呼ばれていた. この呼び出しは CodedInputStream に移譲される.
inline bool WireFormat::ReadInt32(io::CodedInputStream* input, int32* value) { uint32 temp; if (!input->ReadVarint32(&temp)) return false; *value = static_cast<int32>(temp); return true; }
CodedInputStream はバイト列のストリームである ZeroCopyInputStream から構造化データを読み出すアダプタ. Java でいうと InputStream に対する Reader みたいな関係にある. 地味なクラスだけれど, 読み込み処理ループの中心で呼ばれるだけあってちまちまとした高速化がしてある.
代表例として ReadVarint32() を見てみよう. (varint は utf8 のような可変長の整数符号化方式で, 実際の値が小さいほど必要なバイト数が小さくなる. 亜種はよく使われている. たとえば swf とか abc でも使っていた気がする.)
さて, 色々な場合わけを追いかけていこう.
// coded_stream.h inline bool CodedInputStream::ReadVarint32(uint32* value) { if (buffer_size_ != 0 && *buffer_ < 0x80) { *value = *buffer_; Advance(1); return true; } else { return ReadVarint32Fallback(value); } }
まず符号化結果が 1 バイトに収まるケースを特別扱いし, インライン関数に置いている.
ここから先も芸が細かい.
bool CodedInputStream::ReadVarint32Fallback(uint32* value) { if (buffer_size_ >= kMaxVarintBytes || ... (buffer_size_ != 0 && !(buffer_[buffer_size_-1] & 0x80))) { // (訳) // 高速なパス: バッファに十分なバイト数があり, 終端をまたないことが保証されているなら // そのチェックを省略する const uint8* ptr = buffer_; uint32 b; uint32 result; // 可変長ってかんじだ... b = *(ptr++); result = (b & 0x7F) ; if (!(b & 0x80)) goto done; b = *(ptr++); result |= (b & 0x7F) << 7; if (!(b & 0x80)) goto done; b = *(ptr++); result |= (b & 0x7F) << 14; if (!(b & 0x80)) goto done; b = *(ptr++); result |= (b & 0x7F) << 21; if (!(b & 0x80)) goto done; b = *(ptr++); result |= b << 28; if (!(b & 0x80)) goto done; // (訳) // もし入力が 32 ビット以上なら, 上位ビットはすべて読み出して捨てる for (int i = 0; i < kMaxVarintBytes - kMaxVarint32Bytes; i++) { b = *(ptr++); if (!(b & 0x80)) goto done; } // (訳) // サイズが最大値(10 バイト)を越えている. データが壊れているとみなす. return false; done: Advance(ptr - buffer_); *value = result; return true; } else { ... while (buffer_size_ == 0) { // (訳) Refresh() を呼ばないとバイト数の限界に届いてしまう場合を検出する if (// (訳) 届いているなら, buffer_size_after_limit_ は非ゼロのはず buffer_size_after_limit_ > 0 && // (訳) 届いてしまう限界が total_bytes_limit_ でないことを確認する. // その場合も Refresh() を読んでエラーを出力する必要はある. total_bytes_read_ - buffer_size_after_limit_ < total_bytes_limit_) { // バイト数の限界に届いた legitimate_message_end_ = true; return false; } // Call refresh. if (!Refresh()) { // ... 細かなエラー状況の確認色々 return false; } } // Slow path: Just do a 64-bit read. uint64 result; if (!ReadVarint64(&result)) return false; *value = (uint32)result; return true; } }
ポイントは, バッファの残量によって処理をわけているところ. ストリームのバッファをコピーしないポリシー(zero copy)でコードが書かれているため, 値がバッファの終端をまたく場合に備えて面倒な処理が必要になる. そこで終端をまたがないケースを特別扱いして高速化している. 大半はこっちに入りそうだよね.
CodedInputStream や CodedOutputStream には こんなかんじの細々したコードが入っている. でも長いのでつづきは割愛.
DynamicMessage
ここまでのコードで, 自分が生成した Message クラスを読み書きする仕組みはおおよそわかったと思う.
さて, 素朴なシリアライザでは, 直列化したオブジェクトは元のクラスの定義がないと復元できない. protobuf にはクラス定義のないデータを操作するために DynamicMessage というオブジェクトが用意されている. DynamicMessage を使うと, C++ としての定義を持たない Message も descriptor さえあれば復元することができる. 復元した Message はリフレクションごしに操作する. この仕組みを使うことで, たとえばデータのビューアのような汎用データ操作プログラムや, 異なるバージョン間のフォーマットコンバータのようなクラスベースだとやりにくいコードを書くことができる.
DynamicMessage クラスは dynamic_message.cc で定義され, クライアントには公開されていない. クライアントは DynamicMessageFactory を通じてこのインスタンスを取り出す.
// dynamic_message.h class LIBPROTOBUF_EXPORT DynamicMessageFactory : public MessageFactory { public: ... const Message* GetPrototype(const Descriptor* type); ... }
リフレクションに必要な Descriptor は DescriptorProto から構築できたのを思いだそう.
さて, 肝心の DynamicMessage はどんなクラスなのだろうか.
// dynamic_message.cc class DynamicMessage : public Message { public: ... // 一連のメソッド定義... private: ... const Descriptor* descriptor_; const DescriptorPool* descriptor_pool_; DynamicMessageFactory* factory_; scoped_ptr<ExtensionSet> extensions_; GeneratedMessageReflection reflection_; uint8* base_; const uint8* prototype_base_; const int* offsets_; int size_; mutable int cached_byte_size_; };
reflection には通常のメッセージ同様 GeneratedMessageReflection を使いそうだ. それはさておき, base_, prototype_base_, size_ あたりがあやしい. コンストラクタに進もう.
DynamicMessage::DynamicMessage(const Descriptor* descriptor, uint8* base, const uint8* prototype_base, int size, const int offsets[], const DescriptorPool* pool, DynamicMessageFactory* factory) : descriptor_(descriptor), descriptor_pool_((pool == NULL) ? descriptor->file()->pool() : pool), factory_(factory), extensions_(descriptor->extension_range_count() > 0 ? new ExtensionSet(descriptor, descriptor_pool_, factory_) : NULL), reflection_(descriptor, base, prototype_base, offsets, // has_bits reinterpret_cast<uint32*>(base + size) - DivideRoundingUp(descriptor->field_count(), bitsizeof(uint32)), extensions_.get()), base_(base), prototype_base_(prototype_base), offsets_(offsets), size_(size), cached_byte_size_(0) { // (訳) 各フィールドのコンストラクタを手動で呼び出し, あるならデフォルト値をセットする. // placement new について聞いたことがないなら, 今すぐググるとよい. // 一貫性のため, コンストラクタを持たないプリミティブ型についても placement new を呼ぶ. // (理論上は, 型なしのメモリを型ありのメモリにする時はいつも placement new を使うべきだ. // しかし実際はコンストラクタの無い型についてそう厳密である必要はない.) for (int i = 0; i < descriptor->field_count(); i++) { const FieldDescriptor* field = descriptor->field(i); void* field_ptr = base + offsets[i]; switch (field->cpp_type()) { #define HANDLE_TYPE(CPPTYPE, TYPE) \ case FieldDescriptor::CPPTYPE_##CPPTYPE: \ if (!field->is_repeated()) { \ new(field_ptr) TYPE(field->default_value_##TYPE()); \ } else { \ new(field_ptr) RepeatedField<TYPE>(); \ } \ break; HANDLE_TYPE(INT32 , int32 ); HANDLE_TYPE(INT64 , int64 ); HANDLE_TYPE(UINT32, uint32); HANDLE_TYPE(UINT64, uint64); HANDLE_TYPE(DOUBLE, double); HANDLE_TYPE(FLOAT , float ); HANDLE_TYPE(BOOL , bool ); #undef HANDLE_TYPE case FieldDescriptor::CPPTYPE_ENUM: .... case FieldDescriptor::CPPTYPE_STRING: .... case FieldDescriptor::CPPTYPE_MESSAGE: { // (訳) オブジェクトがプロトタイプなら, CPPTYPE_MESSAGE フィールドは初期化を // あとまわしにしなければならない. CrossLinkPrototypes() を使う. だからここでは初期化しない. if (!is_prototype()) { if (!field->is_repeated()) { new(field_ptr) Message*(NULL); } else { ... } } break; } } } }
コメントが丁寧なので特に申しあげることはございません...
base_ など, いくつかのフィールドがコンストラクタの外側から与えられているのは気になる. 呼び出し元を見てみよう.
Message* DynamicMessage::New() const { uint8* new_base = reinterpret_cast<uint8*>(operator new(size_)); memset(new_base, 0, size_); return new DynamicMessage(GetDescriptor(), new_base, prototype_base_, size_, offsets_, descriptor_pool_, factory_); }
base_ は動的に確保し, 他は自分自身からコピーしている. 最初のインスタンス(プロトタイプ)を作るところを探そう.
const Message* DynamicMessageFactory::GetPrototype(const Descriptor* type) { const Message** target = &prototypes_->map_[type]; if (*target != NULL) { // Already exists. return *target; } // (訳) // GeneratedMessageReflection のコンストラクタに渡す構造をすべて作る必要がある. // - メッセージのフィールドを含むメモリブロック // - そのメモリブロック内のフィールドのオフセットをあらわす整数値の配列 // - フィールドがセットされているかどうかをあらわしたビットを保持する大きなビットフィールド // // Compute size and offsets. int* offsets = new int[type->field_count()]; // (訳) // メッセージのフィールドをサイズの降順にソートする. // その順序ならフィールドをタイトに詰め込めるし, 全てのフィールドが正しく整列(align)される. // フィールドは 2 の階乗か, システムのワードサイズの倍数をもつからだ. scoped_array<const FieldDescriptor*> ordered_fields( new const FieldDescriptor*[type->field_count()]); for (int i = 0; i < type->field_count(); i++) { ordered_fields[i] = type->field(i); } stable_sort(&ordered_fields[0], &ordered_fields[type->field_count()], DescendingFieldSizeOrder()); // Decide all field offsets by packing in order. int current_offset = 0; for (int i = 0; i < type->field_count(); i++) { offsets[ordered_fields[i]->index()] = current_offset; current_offset += FieldSpaceUsed(ordered_fields[i]); } // (訳) 全てのフィールドと has_bits ぶんのメモリを確保する. has_bits は終端にくっつける. int size = current_offset + DivideRoundingUp(type->field_count(), bitsizeof(uint32)) * sizeof(uint32); // (訳) // サイズを 64 ビット境界まで繰り上げる. こうすることで, どんな賢いアロケータも // アラインメントについて気にしなくてよくなる. // これによって has_bits も整列する. has_bits は常に構造体の末尾にくっつけるため. size = DivideRoundingUp(size, sizeof(uint64)) * sizeof(uint64); uint8* base = reinterpret_cast<uint8*>(operator new(size)); memset(base, 0, size); // Construct message. DynamicMessage* result = new DynamicMessage(type, base, base, size, offsets, pool_, this); *target = result; result->CrossLinkPrototypes(this); return result; }
コメントが丁寧なので特に申しあげることはございません... CrossLinkPrototypes() は色々やってるけど割愛.
DynamicMessage はコンパイラばりに自らのフィールドを初期化し, reflective な操作を実現している. これに関数ポインタテーブルを追加すれば, ほとんど言語処理系のオブジェクトモデルと変わらないような気がする. (GC はないけど...)
コンパイラ
これでランタイムは一通り眺めた. 残るはコンパイラ. 疲れてきたのでささっと済ませます.
コンパイラの作りはおおよそ次のようになっている: フロントエンド(?)が .proto ファイルを読み込み, descriptor オブジェクトを構築する. descriptor はバックエンド(?)のコード生成器に渡される. コード生成器は指定された descriptor を解釈する Message 実装のソースコードを生成する. コード生成器は C++, Java, Python と言語毎に用意されている.
中間モデルに descriptor を使い回している作りのため, コンパイラ自身の仕事ほとんど文字列を読んだり書いたりするだけ. あまり面白くない.
ただ .proto から Descriptor を作る部分はツールとして使えると便利そう. 実際, 該当機能は Importer クラスとして公開されている.
//importer.h class LIBPROTOBUF_EXPORT Importer { public: Importer(SourceTree* source_tree, MultiFileErrorCollector* error_collector); ... const FileDescriptor* Import(const string& filename); ... };
Importer は内部で Parser を利用する. Parser の実装は手書きの再帰下降方式. 面倒なので割愛.
みどころ再訪と個人的な印象
そんなわけでコードを一通り眺めた. 以上をふまえて冒頭に書いた見所を再確認してみる.
直列化の自己記述度: データは基本的にタグと値の列でしかなく, 自己記述度は低い. (そのぶん空間効率は良い.) タグには型の ID が入っているので, オブジェクトは復元できないまでも, 不明な要素を読み飛ばすことはできる. またメタデータである descriptor も直列化できるため, アプリケーションレベルで自己記述度の高いデータを設計するのは難しくない.
バージョニングへの対処: バージョン情報そのものはない. 古いプログラムが新しいデータを復元するときは, 新しい追加フィールドを "unknown field" として保持する. unknown field は直列化時に書き戻されるため, データが失われることはない. 古いデータを新しいプログラムが読むときは, 新しい追加フィールドが必須のものでない限りエラーにならない. フィールドは空のままになる.
データ内のフィールドは ID だけで識別されているため, .proto のフィールド宣言で ID を変更すると互換性は失われる. ID が同じで型を変更したフィールドは "unknown field" として扱われる.
クラスのないオブジェクトを扱う DynamicMessage の仕組みがあり, 非互換な変更の行われたデータを手動で変換することができる. やりたくないけど. またメッセージの定義を変更せず付加データを与える extension という仕組みもある. (読んでない.)
オブジェクトグラフの自由度: 複雑なオブジェクトグラフは想定されていない. 合流や循環は保存されない. (データを再帰的に読みつつインスタンス化するだけ.)
オブジェクトモデルの自己記述度: 自己記述度は高い. .proto の記述内容は descriptor オブジェクトを通して実行時にも参照できる. reflective な操作もできる. DynamicMessage や Importer など, 実行時に型を構築する仕組みがある. ただ, .proto 自体はそれほど表現能力が高くない. IDL のような継承やらメソッドやらはない.
ランタイムとコード生成の役割分担: 直列化のコードは, リフレクションとランタイムを使ったコンパクトな実装が標準で用意されている. オプションで専用の直列化コードを生成することもできる. 生成されたリフレクション情報 (descriptor) は永続化されたバイナリ形式でソースコードに埋め込まれ, 実行時に復元される.
...
こうしてみると, C++ で reflective なオブジェクトモデルを作るという宿命的負け戦を適当にしのぎつつ, そこそこ高速で堅牢なデータ定義を実現しているのがわかる. 内製ツールという利点もあるのだろう. 売り物にしようとしたらもっと色々な機能や高級なオブジェクトモデルが求められ, 複雑さを割けるのは難しくなるだろうから.
統一された寿命管理の仕組みがない点は少し気になった. オブジェクトは静的に確保するものもあるし, 動的にヒープへ確保するものもある. 動的なオブジェクトには, 呼び出し側が delete しなければいけないものがある一方, prototype や descriptor など delete してはいけないオブジェクトもある. おそらく色々イディオムがあるのだろう. 真似をするコードがある中の人はいいけれど, 外野がスクラッチで protobuf を使おうとしたら, それなりに苦労するかもしれない. 特に非同期や RPC では寿命管理が面倒だからね...
おまけ
//common.h ... namespace google { namespace protobuf { using namespace std; // Don't do this at home, kids. ...