Rustでprotobufのゼロ値がどうやって実現されているか

以前、以下の記事たちを書きました。

cipepser.hatenablog.com

cipepser.hatenablog.com

Rustで0byteを読み込むとどうなるのか(Option型になるのか)気になったので検証してみます。

0byteをrust-protobufで読み込む

protobufを読み込むコードは、Golangで出力したprotobufバイナリをRustで読み込む - 逆さまにしたとほぼ同じです。一点変更しているのは、読み込むファイル名で、main.rsgo_user.binzero.binに変更しました。

ちなみに.protoの定義は以下の通りです。

syntax = "proto3";
package user;

message User {
  string name = 1;
  int32 age = 2;
}

空ファイルを作成し、それを読み込んだ実行結果は以下の通りです。 Goでいうゼロ値でデコードできています。

❯ touch zero.bin

❯ cargo run
Name:
Age: 0

というのもproto3の仕様に以下のように書いてあるのです。

Unknown fields are well-formed protocol buffer serialized data representing fields that the parser does not recognize. For example, when an old binary parses data sent by a new binary with new fields, those new fields become unknown fields in the old binary.

つまりprotobufでは、unknow fieldsも含めて仕様化されており、パーサーは値が存在しないのか、フィールドが定義されていないのかは区別できないのです。
ゼロ値ならバイナリにすらエンコードされないので、送るデータそのものを無くすことができる効率性があります。

protocで生成したコードの実装

protobufの仕様からGoのゼロ値のようにデフォルト値が使われることがわかりました。 もう一歩踏み込んで、protoc --rustoutでgenerateした実装を見ていきましょう。

わかりやすところで、Golangで出力したprotobufバイナリをRustで読み込む - 逆さまにしたで利用した以下のgetメソッドの実装を見ていきます。

println!("Name: {}", u.get_name());
println!("Age: {}", u.get_age());

protoc --rustoutでgenerateした実装を確認すると、以下のようになっています。

pub fn get_name(&self) -> &str {
    &self.name
}

pub fn get_age(&self) -> i32 {
    self.age
}

返り値がOptionに包まれていないですね。 Goの場合はゼロ値がありますが、Rustではどうやって実現されているんでしょうか。 ということでnewの実装をみてみます。

impl User {
    pub fn new() -> User {
        ::std::default::Default::default()
    }
}

予想通りですが、defaultが使われています。
なお、Userの定義でもDefaultがattributeに含まれています。

#[derive(PartialEq,Clone,Default)]
pub struct User {
    // message fields
    pub name: ::std::string::String,
    pub age: i32,
    // special fields
    pub unknown_fields: ::protobuf::UnknownFields,
    pub cached_size: ::protobuf::CachedSize,
}

References