2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

swiftでprotoのCustomOptionsを拾う

Last updated at Posted at 2019-12-13

本記事はSwiftでCustomOptions(以下、option)を解釈できるprotocを書けるようになろうという趣旨です

背景

実装

optionを付ける

Proto3 Language Guide(和訳) #カスタムオプション

Language Guide #customoptions

descriptorを継承してフィールドやメッセージに情報を付与するもので、proto表現外の情報を定義することができます

optionは FileOptions, MessageOptions, FieldOptions, EnumOptions, EnumValueOptions, ServiceOptions, MethodOptions の7種類ありますが、今回はFieldOptionsで説明します

なお公開する際には下のタグナンバー 50002https://github.com/protocolbuffers/protobuf/blob/master/docs/options.md にプルリクをして他protocと衝突しないようにする必要があるみたいです

myoptions.proto
syntax = "proto3";

import "google/protobuf/descriptor.proto";

extend google.protobuf.FieldOptions {
  MyField my_field_option = 50002;
}
message MyField {
  float foo = 1;
}
api.proto
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の実装がすこし癖があり時間がかかりましたが、久しぶりにバイナリに触れていい刺激になりました

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?