RubyからProtobufを使う

MySQL 5.7.12 から追加された X Protocol は Protobuf というのを使ってるらしいです。 Protobuf というのをそこで初めて知ったので、とりあえず Ruby から Protobuf を利用する方法を調べてみました。

Protobuf はデータ構造をバイト列にエンコードしたり、その逆にバイト列をデータ構造にデコードしたりするライブラリのようです。

Ubuntu で protobuf を使うには、protobuf-compiler パッケージをインストールします。

% sudo apt-get install protobuf-compiler

Ruby から Protobuf を使うには、protobuf gem をインストールします。

% gem install protobuf

データ構造は .proto という拡張子のファイルで定義するようです。

MySQL 5.7.12 では rapid/plugin/x/protocol ディレクトリに置かれていました。

% cd mysql-5.7.12/rapid/plugin/x/protocol
% ls
mysqlx.proto             mysqlx_expect.proto     mysqlx_session.proto
mysqlx_connection.proto  mysqlx_expr.proto       mysqlx_sql.proto
mysqlx_crud.proto        mysqlx_notice.proto
mysqlx_datatypes.proto   mysqlx_resultset.proto

Ruby 用にコンパイル(?)します。

% mkdir /tmp/x
% protoc -I . --ruby_out /tmp/x mysqlx.proto
[libprotobuf WARNING google/protobuf/descriptor.cc:5411] Warning: Unused import: "mysqlx.proto" imports "mysqlx_resultset.proto" which is not used.
[libprotobuf WARNING google/protobuf/descriptor.cc:5411] Warning: Unused import: "mysqlx.proto" imports "mysqlx_session.proto" which is not used.
[libprotobuf WARNING google/protobuf/descriptor.cc:5411] Warning: Unused import: "mysqlx.proto" imports "mysqlx_sql.proto" which is not used.
[libprotobuf WARNING google/protobuf/descriptor.cc:5411] Warning: Unused import: "mysqlx.proto" imports "mysqlx_connection.proto" which is not used.
[libprotobuf WARNING google/protobuf/descriptor.cc:5411] Warning: Unused import: "mysqlx.proto" imports "mysqlx_expect.proto" which is not used.
[libprotobuf WARNING google/protobuf/descriptor.cc:5411] Warning: Unused import: "mysqlx.proto" imports "mysqlx_crud.proto" which is not used.
[libprotobuf WARNING google/protobuf/descriptor.cc:5411] Warning: Unused import: "mysqlx.proto" imports "mysqlx_notice.proto" which is not used.
Suppress tag warning output with PB_NO_TAG_WARNINGS=1.
[WARN] .Mysqlx.Datatypes.Scalar object should have 9 tags (1..9), but found 8 tags.
[WARN] .ColumnMetaData.FieldType object should have 18 tags (1..18), but found 11 tags.
[WARN] .Mysqlx.Crud.Find object should have 10 tags (2..11), but found 9 tags.
[WARN] .SessionStateChanged.Parameter object should have 11 tags (1..11), but found 10 tags.
[WARN] .ClientMessages.Type object should have 25 tags (1..25), but found 14 tags.
[WARN] .ServerMessages.Type object should have 19 tags (0..18), but found 13 tags.

いくつか Warning が出てますが、よくわからないので無視します。

/tmp/x に .proto に対応する .pb.rb ファイルが出来ました。

% cd /tmp/x
% ls
mysqlx.pb.rb             mysqlx_expect.pb.rb     mysqlx_session.pb.rb
mysqlx_connection.pb.rb  mysqlx_expr.pb.rb       mysqlx_sql.pb.rb
mysqlx_crud.pb.rb        mysqlx_notice.pb.rb
mysqlx_datatypes.pb.rb   mysqlx_resultset.pb.rb

mysql.x.pb.rb 中にある Mysqlx::Error で試してみます。

module Mysqlx
...
  class Error < ::Protobuf::Message
    optional ::Mysqlx::Error::Severity, :severity, 1, :default => ::Mysqlx::Error::Severity::ERROR
    required :uint32, :code, 2
    required :string, :sql_state, 4
    required :string, :msg, 3
  end
end
require "mysqlx.pb"

e1 = Mysqlx::Error.new(code: 123, sql_state: "XXXXX", msg: "hoge")
s = e.encode  # => "\x10{\x1A\x04hoge\"\x05XXXXX"

e2 = Mysqlx::Error.decode(s)
e2.severity   # => #<Protobuf::Enum(Mysqlx::Error::Severity)::ERROR=0>
e2.code       # => 123
e2.sql_state  # => "XXXXX"
e2.msg        # => "hoge"

Mysqlx::Error オブジェクトがバイト列にエンコード(シリアライズ)されて、バイト列からオブジェクトにデコード(デシリアライズ)されたことがわかります。

Mysqlx::Error は severity が optional で、その他の code, sql_state, msg が required とされています。

required メンバーを指定せずにシリアライズするとエラーになります。

e = Mysqlx::Error.new(code: 123)
e.encode  # => Required field Mysqlx::Error#msg does not have a value. (Protobuf::SerializationError)

また、定義と異なる型を設定しようとしてもエラーになります。

e = Mysqlx::Error.new
e.code = 123456789    # => Ok
e.code = 12345678901  # => Unacceptable value 12345678901 for field code of type Protobuf::Field::Uint32Field (TypeError)

Mysqlx::Error クラスは Protobuf::Message の継承クラスですが、Protobuf::Message は上記のように型チェックのある構造体のように使えます。

プロトコルのためのデータ構造なので、厳密に型をチェックしているのですね。

とりあえずここまで。