【Rust】axumでHandlerとして関数を渡せる理由
はじめに
Rust の Web フレームワークである axum で、Handler として関数を渡すことができますが、その仕組みどのように実現されているのか気になったので調べてみました。
扱う内容
- トレイト境界
- 宣言的マクロ
tl;dr
- axum では関数を
Handler
として扱える - 関数を
Handler
として扱えるのは、関数定義型に対するHandler
の実装があるため - 関数定義型に対する
Handler
の実装は宣言的マクロを使って展開されている - 宣言的マクロは引数の数に応じて
Handler
の実装を展開している
※ 関数を渡せるといっても何でもかんでもではない → Handler の実装参照
axum とは
axum は Rust の Web アプリケーションフレームワークです。
本題ではないので、詳しくはReadme等を参照してください。
axum で hello world
まずは axum で "Hello, World!"を返すエンドポイントと、クエリパラメータを受け取って "Hello, {name}!" を返すエンドポイントの実装例を見てみます。
use std::net::SocketAddr;
use axum::{extract::Query, response::IntoResponse, routing::get, Router};
use serde::Deserialize;
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(root))
.route("/hello", get(hello));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("Server listening on http://{}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
#[derive(Deserialize)]
struct HelloRequest {
name: String,
}
async fn root() -> &'static str {
"Hello, World!"
}
async fn hello(Query(HelloRequest { name }): Query<HelloRequest>) -> impl IntoResponse {
format!("Hello, {}!", name)
}
$ curl -X GET 'http://127.0.0.1:3000'
Hello, World!
$ curl -X GET 'http://127.0.0.1:3000/hello?name=takusan'
Hello, takusan!
この実装例では、ルーティングのところでroot
関数とhello
関数をget
関数に渡しています。ここで、get
の引数のトレイト境界を見ると以下のようにHandler
を求めていることが分かります。しかし、実装例では関数を渡しているだけで、Handler
を実装しているわけではありません。そこで、なんで関数をHandler
として扱えるようになっているのかという疑問が浮かびました。
pub fn get<H, T, S>(handler: H) -> MethodRouter<S, Infallible>
where
H: Handler<T, S>,
T: 'static,
S: Clone + Send + Sync + 'static,
Handler Trait を見に行こう!
Handler
はcall
、layer
、with_state
の 3 つのメソッドを持っていますがcall
以外はデフォルト実装があります。そのためcall
は必ず実装する必要があります。
pub trait Handler<T, S>: Clone + Send + Sized + 'static {
type Future: Future<Output = Response> + Send + 'static;
fn call(self, req: Request, state: S) -> Self::Future;
fn layer<L>(self, layer: L) -> Layered<L, Self, T, S>
where
L: Layer<HandlerService<Self, T, S>> + Clone,
L::Service: Service<Request>,
{
Layered {
layer,
handler: self,
_marker: PhantomData,
}
}
fn with_state(self, state: S) -> HandlerService<Self, T, S> {
HandlerService::new(self, state)
}
}
call
は必ず実装する必要があるけど、実装例で渡した関数とはシグネチャが違うな、、、
impl Handler
ここまでの内容をまとめると、「get
関数の引数はHandler
を満たす必要があり、Handler
はcall
メソッドを実装する必要がある。だけど、call
メソッドと同じシグネチャの関数を自分で実装した記憶はない」ということになります。これはどういうことなのでしょうか?
その鍵を握るのが、Handler の関数定義型に対する実装です。
引数を取らない関数に対する実装
impl<F, Fut, Res, S> Handler<((),), S> for F
where
F: FnOnce() -> Fut + Clone + Send + 'static,
Fut: Future<Output = Res> + Send,
Res: IntoResponse,
{
type Future = Pin<Box<dyn Future<Output = Response> + Send>>;
fn call(self, _req: Request, _state: S) -> Self::Future {
Box::pin(async move { self().await.into_response() })
}
}
細かいトレイト境界は一旦無視すると、この実装は「引数に何も取らず、戻り値がFuture
であるような関数」をHandler
として扱えるようにしています。つまり、実装例においてget
関数の引数に渡した関数root
は、Handler
を実装しているわけではなく、関数定義型に対するHandler
の実装があるためHandler
として扱えるようになっているのです。
ただ、実装例で扱ったのもう 1 つhello
関数は引数とるから上記の実装では対応されていません。では、引数を取る場合も見ていきましょう。
引数を取る関数に対する実装
macro_rules! impl_handler {
(
[$($ty:ident),*], $last:ident
) => {
#[allow(non_snake_case, unused_mut)]
impl<F, Fut, S, Res, M, $($ty,)* $last> Handler<(M, $($ty,)* $last,), S> for F
where
F: FnOnce($($ty,)* $last,) -> Fut + Clone + Send + 'static,
Fut: Future<Output = Res> + Send,
S: Send + Sync + 'static,
Res: IntoResponse,
$( $ty: FromRequestParts<S> + Send, )*
$last: FromRequest<S, M> + Send,
{
type Future = Pin<Box<dyn Future<Output = Response> + Send>>;
fn call(self, req: Request, state: S) -> Self::Future {
Box::pin(async move {
let (mut parts, body) = req.into_parts();
let state = &state;
$(
let $ty = match $ty::from_request_parts(&mut parts, state).await {
Ok(value) => value,
Err(rejection) => return rejection.into_response(),
};
)*
let req = Request::from_parts(parts, body);
let $last = match $last::from_request(req, state).await {
Ok(value) => value,
Err(rejection) => return rejection.into_response(),
};
let res = self($($ty,)* $last,).await;
res.into_response()
})
}
}
};
}
これは宣言的マクロというものを使って実装されています。分かりにくい部分について補足します。
-
[$($ty:ident),*], $last:ident:
- この部分は、複数の型(
$ty
)と最後の型($last
)を指定します。$ty
は 0 個以上の型を表し、$last
は常に 1 つの型です。
- この部分は、複数の型(
-
$( $ty: FromRequestParts<S> + Send, )*
- FromRequestParts は Request のパーツ(例えばヘッダーや URL パラメータ)から値を取り出すためのトレイトであり、リクエストの異なる部分を処理するための関連トレイトです。例えば、
axum::extract::Path
やaxum::extract::Query
はFromRequestParts
を実装しています。
- FromRequestParts は Request のパーツ(例えばヘッダーや URL パラメータ)から値を取り出すためのトレイトであり、リクエストの異なる部分を処理するための関連トレイトです。例えば、
-
$last: FromRequest<S, M> + Send,
- この部分は、
$last
がFromRequest<S, M> + Send
を満たすことを表しています。FromRequest
はRequest
から値を取り出すトレイトです。axum を利用しているときに引数の順番によっては、FromRequest
を実装している型がないというエラーが出ることがありますが、それはこの実装によるものみたいですね。
- この部分は、
実装の具体内容については本記事の関心から外れるので詳しくは解説しませんが、先ほどと同様に引数を取る関数に対するHandler
の実装があるため、関数定義型に対するHandler
の実装があることになります。
以上の宣言的マクロは次の宣言的マクロ利用して展開されています。
all_the_tuples!(impl_handler);
macro_rules! all_the_tuples {
($name:ident) => {
$name!([], T1);
$name!([T1], T2);
$name!([T1, T2], T3);
$name!([T1, T2, T3], T4);
$name!([T1, T2, T3, T4], T5);
$name!([T1, T2, T3, T4, T5], T6);
$name!([T1, T2, T3, T4, T5, T6], T7);
$name!([T1, T2, T3, T4, T5, T6, T7], T8);
$name!([T1, T2, T3, T4, T5, T6, T7, T8], T9);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9], T10);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10], T11);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11], T12);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12], T13);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13], T14);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14], T15);
$name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15], T16);
};
}
ここでは、引数の数に応じてHandler
の実装を展開しています。例えば、
$name!([T1, T2], T3);
は
impl_handler!([T1, T2], T3);
として展開されます。ここで、T1,T2 などはジェネリック型パラメーターで、impl_handler
の$ty
に対応します。また、T3 はimpl_handler
の$last
に対応します。
まとめ
- axum では関数を
Handler
として扱える - 関数を
Handler
として扱えるのは、関数定義型に対するHandler
の実装があるため - 関数定義型に対する
Handler
の実装は宣言的マクロを使って展開されている - 宣言的マクロは引数の数に応じて
Handler
の実装を展開している
※ 関数を渡せるといっても何でもかんでもではない → Handler の実装参照
コードを読む中でとても Rust に対する理解度が向上した実感があります。ただ、ここで使われている様々なメタ的な要素は理解しきれていないし、使いこなせる気はしていないので引き続きインプットとアウトプットを続けていかないとですね。自分もいつかこんなふうに効率的なコードを書いてみたいです。。
参考
すべて axum のソースコードです。
Discussion