1
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

swiftでprotoのCustomOptionsを拾う

本記事は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の実装がすこし癖があり時間がかかりましたが、久しぶりにバイナリに触れていい刺激になりました

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
1
Help us understand the problem. What are the problem?