LoginSignup
4
2

More than 3 years have passed since last update.

protobufの任意のMessageをdeserializeする

Last updated at Posted at 2020-11-29

はじめに

protobufの任意のMessageをdeserializeする方法を調べたので、備忘メモを残しておきます。

誤りやより良い方法などありましたら、ぜひご指摘ください。

以下の環境で確認しています。

$ lsb_release -d
Description:    Ubuntu 18.04.5 LTS
$ uname -r
5.5.8
$ uname -m
x86_64
$ protoc --version
libprotoc 3.14.0

protobufの任意のMessageをdeserializeする

protobufでdeserializeするためには、
「ユーザ定義Message型」のインスタンスのParseFromXXX()メソッドを呼び出す必要があります。

イベント発生時にdeserializeする場合などは
この「ユーザ定義Message型」を動的に決定する必要があります。

C++では型情報を取り回す方法としてtype_info型が用意されていますが、
残念ながらtype_info型からインスタンスを生成することはできないので今回は使えません。

というわけで、protobufで動的に「ユーザ定義Message型」を決定し、deserializeするためには
下記3つのアプローチが考えられます。

  • 方式0. 実インスタンス方式

    • Parseする可能性のあるMessageの実インスタンスを持っておき、必要なタイミングでParseFromXXX()を呼び出す
  • 方式1. デフォルトインスタンス方式

    • Parseする可能性のあるMessageのデフォルトインスタンスへの参照を持っておき、 必要なタイミングでインスタンスを生成してからParseFromXXX()を呼び出す
  • 方式2. Descriptor方式

    • Parseする可能性のあるMessageの型情報(Descriptor)へのポインタを持っておき、 必要なタイミングでインスタンスを生成してからParseFromXXX()を呼び出す

方式0はメモリ効率性からあまり現実的ではなさそうなのと、
仮に採用できたとして実装もそのままなので今回は省略します。

先に結論を言えば、選べるなら方式1の方が簡単ですし、効率が良い分望ましいです。
すでにDescriptorで管理していて変更が難しい場合には、方式2を選択することになると思います。

方式1. デフォルトインスタンス方式

デフォルトインスタンス方式の場合の処理は下記のようになります。

  1. immutableなデフォルトインスタンスから、mutableなMessageインスタンスを生成する
  2. 生成したMessageインスタンスのParseFromXXXを呼び出し、deserializeする
  3. 「ユーザ定義Message型」にdown castして参照する

実装は下記のようになります。

Message::Newで取得したインスタンスの解放責任は呼び出し元にあるので、ここではunique_ptrで受けています。

static std::unique_ptr<Message> LoadMessageBasedOnMessage(
    const Message& message) {
  // 1. Message::Newで、mutableなMessageを生成する
  //    得たMessageは呼び出し元が解放しなければならないので、unique_ptrで受ける
  auto new_message = std::unique_ptr<Message>(message.New());
  if (new_message == nullptr) {
    return nullptr;
  }

  // 2. Message::ParseFromXXXで、Messageをdeserialize
  new_message->ParseFromString(serialized_message);

  return new_message;
}

呼び出し元は以下のようなイメージです。

protobufでは、C++のRTTIが使えないときでもdynamic castできるように
DynamicCastToGeneratedというAPIが用意されています。

    std::unique_ptr<Message> message =
        LoadMessageBasedOnMessage(SampleMessage::default_instance());

    // 3. ユーザ定義Message型にdown cast
    SampleMessage* sample_message =
        DynamicCastToGenerated<SampleMessage>(message.get());

方式2. Descriptor方式

Descriptor方式の場合の処理は下記のようになります。

  1. DynamicMessageFactoryを生成する
  2. DynamicMessageFactory::GetPrototypeで、Descriptorからconst Messageを取得
  3. Message::Newで、mutableなMessageインスタンスを生成する
  4. 生成したMessageインスタンスのParseFromXXXを呼び出し、deserializeする
  5. 「ユーザ定義Message型」にコピーして参照する

DescriptorからMessageを生成するためには、
DynamicMessageFactory::GetPrototypeを使います。

ただし、注意点が2つあります。

1つめは、GetPrototypeで生成したMessageおよび、そこからNewしたMessageは、
DynamicMessageFactoryが破棄されたあとは参照してはいけません

2つめは、GetPrototypeで生成したMessageは、DynamicMessage型にユーザ定義Message型と同じReflectionをもたせたものです。
つまり、振る舞いとしてはユーザ定義Message型と同じですが、
型としては異なるのでユーザ定義Message型へのdown castはできません
厳密にユーザ定義Message型として使うためには、
ユーザ定義Message型のインスタンスにCopyFromする必要があります。

これらを加味し、方式1と同等の処理を行った実装が下記です。
返すMessage以上に生存期間の長いDynamicMessageFactoryが必要なので、引数で渡してもらっています。

static std::unique_ptr<Message> LoadMessageBasedOnDescriptor(
    DynamicMessageFactory* factory, const Descriptor* descriptor) {
  if ((factory == nullptr) || (descriptor == nullptr)) {
    return nullptr;
  }

  // 1. DynamicMessageFactory::GetPrototypeで、Descriptorからconst Messageを取得
  const Message* prototype = factory->GetPrototype(descriptor);
  if (prototype == nullptr) {
    return nullptr;
  }

  // 2. Message::Newで、mutableなMessageを生成する
  //    得たMessageは呼び出し元が解放しなければならないので、unique_ptrで受ける
  auto message = std::unique_ptr<Message>(prototype->New());
  if (message == nullptr) {
    return nullptr;
  }

  // 3. Message::ParseFromXXXで、Messageをdeserialize
  message->ParseFromString(serialized_message);

  return message;
}

呼び出し元は以下のような感じです。

down castができないので、Message::CopyFromしています。

Reflection経由でしかプロパティにアクセスせず、
かつDynamicMessageFactroryの生存期間をMessageより長くできるのであれば、
4.のコピーを避けることもできると思います。

    {
      // 0. DynamicMessageFactoryを生成
      DynamicMessageFactory factory;

      std::unique_ptr<Message> message =
          LoadMessageBasedOnDescriptor(&factory, SampleMessage::descriptor());

      // 4. ユーザ定義Message型にコピー
      sample_message.CopyFrom(*message);

      // 5. ここでDynamicMessageFactory解放
      //    以降、factoryから生成したmessageは参照してはいけない
      //    sample_messageは引き続き使用できる
    }

性能比較

それぞれ1,000,000回実行した時間の平均を比較すると、
方式2の方が約2倍時間がかかっています。

方式 時間[ns]
方式1 1,927
方式2 3,716

コピーが1回→2回に増えているので、およそ想定通りです。

計測環境は以下のとおりです。

$ grep 'model name' /proc/cpuinfo | uniq -c
      8 model name  : Intel(R) Core(TM) i5-8259U CPU @ 2.30GHz
$ grep MemTotal /proc/meminfo 
MemTotal:       32752704 kB
$ uname -r
5.5.8
$ lsb_release -d
Description:    Ubuntu 18.04.5 LTS

終わりに

今回、DynamicMessageFactory::GetPrototypeのドキュメントを読んでいて、
以下の文言を見つけたのが調べるきっかけでした。
Newしたら独立したインスタンスが返ってくると思い込んでいたので、驚きました。

Also, any objects created by calling the prototype's New() method share some data with the prototype, so these must be destroyed before the DynamicMessageFactory is destroyed.

protobufではC++のような静的型付け言語でもかなり動的に型を処理できるよう工夫してくれています。
一方で効率も求められるので、どうしてもトリッキーな処理や直感に背く振る舞いも出てきてしまうのだと思います。

あらためて、protobufで動的に型を処理する場合は慎重にならないとと感じました。

蛇足ですが、今回DynamicMessageFactoryDynamicMessageの関係を確認するのに
valgrind --leak-check=full --show-leak-kinds=allがとても役に立ってくれました。

実験コード

今回の記事で使った実験コードは、以下に置いています。

takeoverjp/protobuf-sandbox

参考

Message::DynamicCastToGenerated
DynamicMessageFactory

4
2
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
4
2