rust的魔法参数

初看axum 时,对于它的示例代码惊为天人:一个静态强类型语言竟然可以做出比肩动态语言的效果,可以写出如此灵活,且做了严格类型检查的代码

不同于actix-webrocket这些rust下的web框架,axum的路由处理设计并没有使用到过程宏。路由使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(root))
.route("/:id", get(get_user));

let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}

async fn root() -> &'static str {
"Hello, World!"
}

async fn get_user(Path(id): Path<u64>) -> (StatusCode, Json<User>) {
let user = User {
id,
username: "user".to_string(),
};
(StatusCode::OK, Json(user))
}

get方法竟然可以接受root/get_user两个具有不同签名的函数,乍看之下属实使人匪夷所思

继续阅读route方法的签名

1
2
3
4
5
pub fn route(self, path: &str, method_router: MethodRouter<S>) -> Self {
self.tap_inner_mut(|this| {
panic_on_err!(this.path_router.route(path, method_router));
})
}

它接受path & method_router 两个参数,那么get(root) & get(get_user) 的类型一定是 MethodRouter<S>

进一步找到get方法的定义

1
2
3
4
5
6
7
8
9
pub fn get<H, T, S>(handler: H) -> MethodRouter<S, Infallible>
where
H: Handler<T, S>,
T: 'static,
S: Clone + Send + Sync + 'static,
{
on(MethodFilter::GET
, handler)
}

所以get(root) 会被展开为 on(MethodFilter::GET, root),所以root / get_user 必然都实现了 Handler<T, S>,否则不能通过编译

Handler trait中最为重要的方法是call

1
fn call(self, req: Request, state: S) -> Self::Future;

call方法要求handler实现 request -> response的处理流程
对于无参的root方法,是通过以下代码实现,这在rust中称为blanket implementation:

1
2
3
4
5
6
7
8
9
10
11
12
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() })
}
}

对于所有实现了 FnOnce() -> Fut + Clone + Send + 'static 的类型,均实现了Handlerroot函数实现了FnOnce trait,因而也实现了Handler
对于1-16个参数的函数,axum通过过程宏提供了一个统一的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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()
})
}
}
};
}

其中

1
2
3
4
5
6
$(
let $ty = match $ty::from_request_parts(&mut parts, state).await {
Ok(value) => value,
Err(rejection) => return rejection.into_response(),
};
)*

是对每个 $ty(T1, T2, …)依次调用 from_request_parts(),得到 handler 需要的变量
还可以发现,impl_handler,对于最后一个参数$last进行了区分处理,之前的参数要求实现**FromRequestParts**,最后一个参数要求实现 FromRequest。这两个trait的区别在于实现FromRequestParts 的extractor不会消耗request的body,在handler中可以按任意顺序排列,而FromRequest会消耗request body,在handler中只能执行一次。所以像Json 这样的类型只能作为函数的最后一个参数

blanket implementation这个特性是axum能够实现魔法参数的核心。其实像给函数实现接口这种设计,在其他语言也颇为常见,比如golang的HandlerFunc,但由于其没有blanket implementation,HandlerFunc只能接受固定的ResponseWriterRequest
两个参数,无法像axum这样自由灵活的对handler参数进行组合

分析完request,可以举一反三的理解response。和response相关的trait是 IntoResponse

1
2
3
4
5
pub trait IntoResponse {
/// Create a response.
#[must_use]
fn into_response(self) -> Response;
}

在 axum 中,很多类型都已经实现了 IntoResponse trait,包括 &’static strJson<T>get_user 返回的 (StatusCode, Json<User>) 同样也实现了它:

1
2
3
4
5
6
7
8
9
10
impl<R> IntoResponse for (StatusCode, R)
where
R: IntoResponse,
{
fn into_response(self) -> Response {
let mut res = self.1.into_response();
*res.status_mut() = self.0;
res
}
}

通过trait + macros + blanket implement,可以在保持静态检查及安全性的同时,大大提升用户代码的灵活性、易用性以及可组合性。不使用过程宏来实现路由处理,也方便了用户在测试时只需关注一个个独立的handler function。但凡事皆有两面,基于泛型的抽象设计带来极大灵活性的同时,也增加了错误消息的复杂度。如果输入的参数不符合 FromRequestParts / FromRequest trait,那么,axum 编译期产生的错误会非常晦涩难懂,为此axum还专门提供了debug_handler macro帮助排查此类问题