protocol buffers のバイナリを眺めてみる
- protocol buffers を利用することで色々いい感じになります
- 複雑なことの必要がなく、簡単に利用することができます
- ですが、高速なシリアライゼーションの恩恵を受ける上で、protobuf のバイナリを1度眺めておくことで、効率的なモデルの設計ができるようになるかもしれません
- こちらの資料を参考にしています
https://developers.google.com/protocol-buffers/docs/encoding - swift-protobuf を使っています
- 検証に利用したコードはこちら
protocol buffers の使い方
- proto ファイルでスキーマを定義します
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
- protoc を使って、swift のコードを作成します
protoc --swift_out=generated protos/*.proto
- swift のコードは指定した構造の struct がされます
struct SearchRequest {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
var query: String = String()
var pageNumber: Int32 = 0
var resultPerPage: Int32 = 0
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
- この構造体を利用して、protobuf のバイナリを取得できます
var request = SearchRequest()
request.pageNumber = 1
let data = try request.serializedData()
protobuf のバイナリを眺めてみる
- まずは最も簡単なスキーマとそれに対応するバイナリを見てみます
- id が 1 の int64 型の
number
のプロパティを持つ Number1Entity です
message Number1Entity {
int64 number = 1;
}
- protoc を使って swift の構造体を作成し、
number
に10
を設定してバイナリを生成します
var entity = Number1Entity()
entity.number = 10
let data: Data = try entity.serializedData()
- バイナリは
080a
となりました - 2進数表記だと、
00001000 00001010
です - これをビッドごとの意味で色付けをしました

- それぞれのパラメータについて見ていきます
Type
- 1バイト目の下位3ビットには、Type が格納されています
- 上の画像のオレンジ部分です
- Type は、0, 1, 2, 5 の4種類の可能性があります
Type | Meaning | Used For |
---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimited | string, bytes, embedded messages, packed repeated fields |
5 | 32-bit | fixed32, sfixed32, float |
- Type によってこの後の処理が変わります
-
080a
の例では、Type は 0 なので、Varint
です - 確かにスキーマで int64 と定義しています
ID
- 1バイト目の2〜5ビットまでの4ビットに id が入っています
- 上の画像の緑部分です
- 緑の部分は
0001
なので、id は 1 となります - 確かにスキーマで、1と定義しています
Verint
- 2バイト目には、データの本体が入っています
- type が Verint なので、最上位ビットは further (後述)のフラグなので、下位7ビットにデータが入っています
- 上の画像では紫の部分です
-
0001010
= 10 なので代入した通りです
Verintの可変長数値表現
- 先ほどまで見ていた構造体では、id が4ビット、データ本体が7ビットで表現されています
- 当然15より大きいidを使いたいですし、127より大きいデータを使いたいです
- そこで、protobufでは最上位ビットを使って、可変長数値表現を実現しています
- 今度は、number に127より大きい数字である99999を代入してバイナリを生成してみます
var entity = Number1Entity()
entity.number = 99999
let data: Data = try entity.serializedData()
- バイナリは、
089f8d06
となりました - これを色分けしました

- 1バイト目は変化がないですが、2バイト目以降のデータが変わっています
- 2バイト目の最上位ビットを見ると1になっており、次のバイトまでデータが繋がっていることがわかります
- 3バイト目の最上位ビットも立っているので、次のバイトまで繋がっています
- 4バイト目の最上位ビットは立っていないので、このバイトで終わりであることが分かります
- 2~4バイトのそれぞれ下位7ビットを、リトルエンディアンなので繋げます
- 結果は、
000011000011010011111
= 99999 で代入した値です - このことから、varintを利用する場合、7bitごとの数値が効率が良いことが分かります
idの可変長数値表現
- 次に、id が99999の場合も見てみます
message Number99999Entity {
int64 number99999 = 99999;
}
- こちらのスキーマを使った構造体の
number99999
に 10を代入してバイナリを生成します
var entity = Number99999Entity()
entity.number99999 = 10
let data: Data = try entity.serializedData()
- 結果は
f8e9300a
でした - 色分けしました

- id も可変長数値表現が使えます
- これも緑の部分をリトルエンディアンで並び替えると、
000011000011010011111
= 99999 となるので、スキーマで設定した通りになっていることが分かります - このことから、id は 4, 11, 18...ビットが効率良いことが分かります
64-bit, 32-bit
- 次に、固定64bit, 32bitを見てみます
- double, float, fixed64 などの型が含まれます
message Fixed64Entity {
fixed64 number = 1;
}
- fixed64 型の number プロパティに10を代入してバイナリを取得します
var entity = Fixed64Entity()
entity.number = 10
let data: Data = try entity.serializedData()
- 結果は、
0x090a00000000000000
でした - 色分けしました

- 可変長数値表現ではなく、固定長で8バイトのデータになります
- このことから、値がとても大きい場合や、負の数の頻度が高い場合は、固定長の方が有利であることが分かります
Length-delimited (文字列)
- Length-delimited では文字列などを格納できます
- まずは文字列を見てみます
message Text2Entity {
string text = 2;
}
- 文字列に "abc" を代入してバイナリを生成します
var entity = Text2Entity()
entity.text = "abc"
let data: Data = try entity.serializedData()
- 結果は
1203616263
でした(16進数です) - 色分けしました

- 今回の type は、2 なのでLength-delimited です(オレンジ部分)
- id も 2 であることが分かります(緑部分)
- Length-delimited の場合は、データ部分の最初に可変長数値表現でサイズ情報が入っています
- 今回は、
0000011
なので3バイトです(青部分) - 後ろの3バイトが文字列データです(紫部分)
-
0x61
= 'a' なので、設定した文字列になっていることが分かります
2つのプロパティ
- これまではプロパティが1つの場合を見てきました
- 今回は、数値と文字列の2のプロパティを持つ message のバイナリを見ていきます
message TwoPropertyEntity {
int64 number = 1;
string text = 2;
}
- number = 10, text = "abc" でバイナリを生成します
var entity = TwoPropertyEntity()
entity.number = 10
entity.text = "abc"
let data: Data = try entity.serializedData()
- 結果は、
080a1203616263
でした -
080a
と1203616263
に分割でき、これまで見た値と同値になっています -
080a
は id が 1 の値が 10 のバイナリです -
1203616263
は id が 2 の値が "abc" のバイナリです - 色付けしました

- protobuf はバイナリをくっ付けることで、2つのメッセージをマージできるようです
Oneof
- protobuf には oneof という機能があります
- oneof の中の1つのプロパティが設定できます
message OneOfEntity {
int64 number = 1;
oneof one {
string ofText = 2;
int64 ofNumber = 3;
}
}
- oneof のプロパティに ofNumber を設定してバイナリを生成しました
var entity = OneOfEntity()
entity.number = 10
entity.one = .ofNumber(44)
let data: Data = try entity.serializedData()
- 結果は、
080a182c
でした - これを色付けしました

- oneof は定義したうちの1つが設定されるだけで特にネストなどされず、通常のプロパティと同じ感じです
配列
- int64 の配列を見てみます
message RepeatedNumberEntity {
repeated int64 numbers = 1;
}
- 配列には、10と130を設定します
var entity = RepeatedNumberEntity()
entity.numbers = [10, 130]
let data: Data = try entity.serializedData()
- 結果は、
0a030a8201
でした - 色付けしました

- repeated なので、type は Length-delimited です
- Length-delimited の body 部分(紫色部分)は varint になっています
- 1バイト目は、
1010
= 10 - 2〜3バイト目は、
10000010
= 130で指定した値になっていることが分かります
辞書式
- 辞書式を見ていきます
message MapEntity {
map<string, string> dictionary = 1;
}
- バイナリを生成します
var entity = MapEntity()
entity.dictionary = [
"a": "A",
"b": "BB"
]
let data: Data = try entity.serializedData()
- 結果は、
0a060a01611201410a070a016212024242
でした - 色付けします
- id が1の Length-delimited が2つ入っています
-
0a0161120141
の部分に着目します

- id が1に "a"、id が2に"A" が入っていることが分かります
- もう1つ目のbody部分にもキーが id 1 に、値が id 2 に入っています
- それぞれのキーと値のペアが同じ構造体が同じ id で入っていると辞書式となるようです
message のプロパティ
- 1番最初に見た、Number1Entity をプロパティに持つ message です
message ParentEntity {
Number1Entity child = 1;
}
- バイナリを生成します
var child = Number1Entity()
child.number = 10
var entity = ParentEntity()
entity.child = child
let data: Data = try! entity.serializedData()
- 結果は
0a02080a
でした - 色付けしました

- id が1で、typeがLength-delimitedです
- body(紫色)部分は、080aです
- これは、1番最初に見た、idが1の値がvarintの10のバイナリと同じです
- つまり、messageのプロパティのbodyにはそのmessageのバイナリが入っているようです
まとめ
- さまざまな protobuf のバイナリを生成し、眺めて見ました
- バイナリ自体はかなりシンプルな構造でした
- 実際に、値を変えて動かすと、バイナリとその構造が動いていくのが見えるので、とても楽しかったです