本記事はSwiftでCustomOptions(以下、option)を解釈できるprotocを書けるようになろうという趣旨です
背景
-
Golang ProtoBuf Validator Compiler - https://github.com/mwitkow/go-proto-validators
- これのSwift版がほしい。でもGoは書けない...だったらSwiftで書けばいいだろ!
- proto=>swiftの swift-protobuf はSwiftで書かれているが swift-protobufではoptionは必要ないからキャプチャ/公開しない とのこと
- とはいえ最後のIssueのとおりUnknownFieldsにちゃんと入っていました。ツンデレか
実装
optionを付ける
Proto3 Language Guide(和訳) #カスタムオプション
descriptorを継承してフィールドやメッセージに情報を付与するもので、proto表現外の情報を定義することができます
optionは FileOptions, MessageOptions, FieldOptions, EnumOptions, EnumValueOptions, ServiceOptions, MethodOptions
の7種類ありますが、今回はFieldOptionsで説明します
なお公開する際には下のタグナンバー 50002
を https://github.com/protocolbuffers/protobuf/blob/master/docs/options.md にプルリクをして他protocと衝突しないようにする必要があるみたいです
syntax = "proto3";
import "google/protobuf/descriptor.proto";
extend google.protobuf.FieldOptions {
MyField my_field_option = 50002;
}
message MyField {
float foo = 1;
}
syntax = "proto3";
import "myoptions.proto";
message APIMessage {
int32 foo = 1 [(my_field_option) = {foo:4.5}];
string bar = 2;
}
option定義をSwiftに変換
git clone git@github.com:apple/swift-protobuf.git
cd swift-protobuf
swift build
vi myoptions.proto
protoc --plugin=protoc-gen-swift=.build/debug/protoc-gen-swift --swift_out=./ -I./Protos -I. ./myoptions.proto
# generate myoptions.pb.swift
ジェネレートしたpb.swiftをProjectに含める
今回は本丸に含めます
cp myoptions.pb.swift Sources/protoc-gen-swift/
[補足]ProtocolBuffersのバイナリ
参考:
165行で実装するProtocol Buffersデコーダ(ミニマム版)
Protocol Buffers のエンコーディング仕様の解説
公式エンコーディング解説
簡単にまとめると
- ProtocolBuffersのtypeは次のとおり
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 |
3 | Start group | groups (deprecated) |
4 | End group | groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
- 各タイプの構成は次のとおり。 TagとPayloadLengthの導出にもdecodeVarintが必要 になります。
Type | タグナンバー*8+タイプ値 | PayloadLength | Payload |
---|---|---|---|
0 | Varint<<3|0 | - | Varint |
1 | Varint<<3|1 | - | 8byte |
2 | Varint<<3|2 | Varint | PayloadLength分の何か |
5 | Varint<<3|5 | - | 4byte |
UnknownFields切り分け+デシリアライズ
UnknownFieldsには他optionもシリアライズされて入っているので、公式ソースコード を参考に対象タグナンバーのPayloadを切り出す必要があります。(タグナンバーはmyoptions.protoで定義した 50002
)
optionのデシリアライズは対象Messageのinitに渡すだけです
diff --git a/Sources/protoc-gen-swift/FieldGenerator.swift b/Sources/protoc-gen-swift/FieldGenerator.swift
index 8b2177c2..15ba3505 100644
--- a/Sources/protoc-gen-swift/FieldGenerator.swift
+++ b/Sources/protoc-gen-swift/FieldGenerator.swift
@@ -88,9 +88,98 @@ class FieldGeneratorBase {
}
}
+ var myoptions: MyField?
+
init(descriptor: FieldDescriptor) {
precondition(!descriptor.isExtension)
number = Int(descriptor.number)
fieldDescriptor = descriptor
+ // UnknownFields
+ myoptions = MyField.init(unknownFieldData: fieldDescriptor.options.unknownFields.data)
+ // 確認用
+Stderr.print("\(myoptions?.foo ?? 0.0)")
+ }
+}
+
+extension MyField {
+ init?(unknownFieldData: Data) {
+ if unknownFieldData.count == 0 {
+ return nil
+ }
+ // TODO: change tagNumber
+ let extensionsTagNumber = 50002
+ guard let data = getFieldData(data: unknownFieldData, tagNumber: extensionsTagNumber) else {
+ return nil
+ }
+ // デシリアライズ
+ guard let v = try? MyField(serializedData: data) else {
+ return nil
+ }
+ self = v
+ }
+}
+
+// https://developers.google.com/protocol-buffers/docs/encoding
+func getFieldData(data: Data, tagNumber: Int) -> Data? {
+ var index = 0
+ // タグナンバー*8+タイプ値 の形にしておく。my_field_optionはType2なので0x02
+ let targetKey = tagNumber << 3 | 0x02
+ while index < data.count {
+ let wireFormat = data[index] & 0x07
+ switch wireFormat {
+ case 0x02: // Length-delimited string, bytes, embedded messages, packed repeated fields
+ guard let key = decodeVarint(data: data, index: &index) else {
+ return nil
+ }
+ guard let length = decodeVarint(data: data, index: &index) else {
+ return nil
+ }
+ if key == targetKey {
+ // Target Option
+ return data.subdata(in: index..<index+Int(length))
+ }
+ index += Int(length)
+ break
+ case 0x00: // Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
+ // key
+ guard let _ = decodeVarint(data: data, index: &index) else {
+ return nil
+ }
+ // value
+ guard let _ = decodeVarint(data: data, index: &index) else {
+ return nil
+ }
+ break
+ case 0x01: // 64-bit fixed64, sfixed64, double
+ // key
+ guard let _ = decodeVarint(data: data, index: &index) else {
+ return nil
+ }
+ index += 8
+ break
+ case 0x05: // 32-bit fixed32, sfixed32, float
+ // key
+ guard let _ = decodeVarint(data: data, index: &index) else {
+ return nil
+ }
+ index += 4
+ break
+ default:
+ return nil
+ }
+ }
+ return nil
+}
+func decodeVarint(data: Data, index i: inout Int) -> UInt64? {
+ var value = UInt64(0)
+ var shift = UInt64(0)
+
+ while i < data.count {
+ let c = data[i]
+ i += 1
+ value |= UInt64(c & 0x7f) << shift
+ shift += 7
+ let msb = (c & 0x80) >> 7
+ if msb == 0 {
+ return value
+ }
}
+ return nil
}
swift build
protoc --plugin=protoc-gen-swift=.build/debug/protoc-gen-swift --swift_out=./ -I./Protos -I. ./api.proto
# protoc-gen-swift: 4.5 // int32 foo = 1 のoption
# protoc-gen-swift: 0.0 // string bar = 2 のoption は無いので0.0
所感
protoはとてもシンプルにインターフェイスを書き表せるツールで、今後protocの需要も多少出てくるかなということで勉強がてらやってみました
swift-protobufの実装がすこし癖があり時間がかかりましたが、久しぶりにバイナリに触れていい刺激になりました