アルパカ三銃士

〜アルパカに酔いしれる獣たちへ捧げる〜

Perl で Protobuf を扱うために Google::ProtocolBuffers::Dynamic を使ってみる

こちらは Perl Advent Calendar 2018 の 25 日目です。

gRPC を Perl でやってみようと思ったのですがインターネット上に Perl で Protobuf を扱うような文献がなかったので、まずはここから書かないとだめでしょう!ということで内容を変更してお届けします。

Perl で Protobuf を扱うのに適したモジュールは 2018 年現在だと Google::ProtocolBuffers::Dynamic が一番良さげっぽい。しかし、proto2 には対応しているが proto3 の機能に一部対応できてないとのこと*1

metacpan.org

Synopsis を試す

Synopsis に記述されていたコードを proto3 で試してみる。

use feature qw/say/;
use Data::Dumper;
use Google::ProtocolBuffers::Dynamic;


my $dynamic = Google::ProtocolBuffers::Dynamic->new;
$dynamic->load_string("person.proto", <<'...');
syntax = "proto3";
 
package humans;
 
message Person {
  string name = 1;
  int32 id = 2;
}
...
 
$dynamic->map({ package => 'humans', prefix => 'Humans' });
 
# encoding/decoding
my $person = Humans::Person->decode_json('{"id":31,"name":"John Doe"}');
say Dumper $person; 

# field accessors
$person->set_id(77);
say $person->get_id;

実行結果は下記の通りになった。

$VAR1 = bless( {
                 'name' => 'John Doe',
                 'id' => 31
               }, 'Humans::Person' );

77

動きとして

  1. load_string メソッドの第一引数に proto のファイル名を記述して、第二引数に protobuf のコードを渡す
  2. map メソッドを活用すると protobuf の内容に沿った Perl のオブジェクトが作成される
  3. decode_json へ渡された json をベースに値をセットする

また、アクセサも提供されているため値を変更することが可能である。

map メソッドのドキュメントを読んでみると to => "perl_package" とできるし、マッピングのオプションとして options => { ... } ということも可能らしい。

使えるシンタックス

すべて可能なわけではないらしいので、現実的によく使われるであろう構文を読み込ませてみる。

repeated

message Person {
  repeated int32 id = 1;
}

実行結果

$VAR1 = bless( {
                 'id' => [
                           31,
                           10
                         ]
               }, 'Humans::Person' );

message

message Person {
  Info info = 1;
}

message Info {
  int32 age = 1;
}

実行結果

$VAR1 = bless( {
                 'info' => bless( {
                                    'age' => 10
                                  }, 'Humans::Info' )
               }, 'Humans::Person' );

個人的にこの結果はとても好きです。簡単にオブジェクトを作れるのが凄い。

oneof

message Person {
  oneof sex {
    bool man   = 1;
    bool woman = 2;
  }
}

{"man":true} といった json を渡してあげた結果がこちら

$VAR1 = bless( {
                 'man' => 1
               }, 'Humans::Person' );

{"man":true,"woman":false} の文字列を渡した場合

$VAR1 = bless( {
                 'woman' => ''
               }, 'Humans::Person' );

と一つのフィールドだけ出現することが確認できた。

enum

message Person {
  enum Type {
    MAN       = 0;
    WOMAN     = 1;
    NO_GENDER = 2;
  }
  Type type = 1;
}

{"type":2} の文字列を渡した場合

$VAR1 = bless( {
                 'type' => 2
               }, 'Humans::Person' );

が出力された。しかし、type に 2 よりも大きい数値を渡すとどうなるのだろうか...

{"type":3} の文字列を渡してみる。結果は以下の通りになった。

$VAR1 = bless( {}, 'Humans::Person' );

ENUMERATIONS を読んでみると、デフォルトで check_enum_values というオプションは有効になっている。有効になっていると、デコード時にもし該当しない enum の数値が渡されるとそれは無視する。エンコードする時はエラーを出力するとのこと。

ちなみに

my %tbl = %Humans::Person::Type::;
say Dumper \%tbl;

というコードを追記して何が定義されているかを確認すると

$VAR1 = {
          'WOMAN' => *Humans::Person::Type::WOMAN,
          'enum_descriptor' => *Humans::Person::Type::enum_descriptor,
          'MAN' => *Humans::Person::Type::MAN,
          'NO_GENDER' => *Humans::Person::Type::NO_GENDER
        };

という結果が得られた。これはドキュメントに記載されてる通り、下記は同じ関係であることが読み取れる。

enum Foo {
    BAR = 1;
    BAZ = 2;
}
use constant {
    BAR => 1,
    BAZ => 2,
};

services

service Echo {
  rpc Say(SayRequest) returns (SayResponse);
}

のような構文も解析可能らしい。が、色々準備が必要な感じだったため今回はパス。

感想

正直ここまでちゃんとパースして Perl のコードへ落とし込めるとは思ってなかった。想像以上の出来だったのでとても嬉しい。

しかし services に関して、ちゃんと動かすために Grpc::XS が必要っぽい。

metacpan.org

これを実行するための環境を用意するのが少しハードル高い気がする。今日はここまで!

それにしても YAPC::Tokyo 楽しみですなー

yapcjapan.org