🔥

【Rust】axumでHandlerとして関数を渡せる理由

2024/01/06に公開

はじめに

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}!" を返すエンドポイントの実装例を見てみます。

main.rs
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 を見に行こう!

Handlercalllayerwith_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を満たす必要があり、Handlercallメソッドを実装する必要がある。だけど、callメソッドと同じシグネチャの関数を自分で実装した記憶はない」ということになります。これはどういうことなのでしょうか?

その鍵を握るのが、Handler の関数定義型に対する実装です。

引数を取らない関数に対する実装

axum-0.7.3/src/handler/mod.rs
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関数は引数とるから上記の実装では対応されていません。では、引数を取る場合も見ていきましょう。

引数を取る関数に対する実装

axum-0.7.3/src/handler/mod.rs
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::Pathaxum::extract::QueryFromRequestPartsを実装しています。
  • $last: FromRequest<S, M> + Send,
    • この部分は、$lastFromRequest<S, M> + Sendを満たすことを表しています。FromRequestRequestから値を取り出すトレイトです。axum を利用しているときに引数の順番によっては、FromRequestを実装している型がないというエラーが出ることがありますが、それはこの実装によるものみたいですね。

実装の具体内容については本記事の関心から外れるので詳しくは解説しませんが、先ほどと同様に引数を取る関数に対するHandlerの実装があるため、関数定義型に対するHandlerの実装があることになります。

以上の宣言的マクロは次の宣言的マクロ利用して展開されています。

axum-0.7.3/src/handler/mod.rs
all_the_tuples!(impl_handler);
axum-0.7.3/src/macros.rs
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 のソースコードです。
https://github.com/tokio-rs/axum

GitHubで編集を提案

Discussion