Rustのtonicクライアントで構造体を直接渡せる理由を調べてみた
はじめに
tonicでは、クライアントのメソッドを呼び出す際にリクエスト構造体をRequest::new
でラップせずにそのまま渡すことができます。
pub struct Point {}
pub struct Client {}
impl Client {
fn get_feature(&self, r: impl tonic::IntoRequest<Point>) {}
}
use tonic::Request;
// 以下の2つの呼び出しはどちらも有効です
client.get_feature(Point {});
client.get_feature(Request::new(Point {}));
私は最初、この理由を「Point構造体から、Request<Point>へ変換するためのトレイトを実装しているから」だと考えていました。しかし、実際に調べたところ、実態は異なっていました。
この記事では、その仕組みについて紹介します。
プロトコルファイルから生成されるコード
以下のようなプロトコルファイル(.proto)があります。
syntax = "proto3";
package sample.v1;
message Point {}
message Feature {}
service ExampleService {
rpc GetFeature(Point) returns (Feature);
}
このプロトコルファイルからコードを生成すると、次のようなコードが得られます。
(見やすくするため、一部を省略しています)
pub struct Point {}
pub struct Feature {}
impl<T> ExampleServiceClient<T>
where
//...中略...
{
//...中略...
pub async fn get_feature(
&mut self,
request: impl tonic::IntoRequest<super::Point>,
) -> std::result::Result<tonic::Response<super::Feature>, tonic::Status> {
//...中略...
}
}
get_feature
は引数にIntoRequestトレイトを満たす任意の型を受け入れることができます。
IntoRequestトレイトの仕組み
IntoRequestトレイトは次のように定義されています。
pub trait IntoRequest<T>: Sealed {
// Required method
fn into_request(self) -> Request<T>;
}
このトレイトを実装した型は、into_requestメソッドでRequest<T>に変換することができます。
最初、私はPoint構造体が自動生成される過程でIntoRequestトレイトを実装しているのだと思っていました。しかし、生成されたコードのどこを見ても、そのような実装は見当たりませんでした。
ブランケット実装によるトレイトの実装
答えはtonicクレート内のIntoRequestトレイトの実装にありました。
tonicでは、以下のようなブランケット実装を行っています。
impl<T> IntoRequest<T> for T {
fn into_request(self) -> Request<Self> {
Request::new(self)
}
}
impl<T> IntoRequest<T> for Request<T> {
fn into_request(self) -> Request<T> {
self
}
}
ブランケット実装とは、トレイト境界を満たす全ての型に対してトレイトを実装することです。この場合、任意の型Tに対してIntoRequest<T>を実装しています。
このブランケット実装により、Point構造体は自動的にIntoRequest<Point>トレイトを実装していることになります。したがって、get_featureメソッドにPoint構造体をそのまま渡すことができます。
まとめ
tonicでは、ブランケット実装により任意の型に対してIntoRequestトレイトを実装しているため、リクエスト構造体をRequest::newでラップせずにそのまま渡すことができます。
この仕組みを理解することで、tonicを用いたgRPCクライアントの実装がよりスムーズになります。
感想
Rustのこのあたりの話は、書籍を読むだけでは理解が難しいですが、実際の実装を見ると理解が深まりますね〜
Discussion