完全な小ネタです。使ってみた記事です。
Rust ではトレイトの関数を async にできない
Rust では、現状トレイトのメソッドに async をつけることはできません*1。つまり、下記のようなコードはコンパイルエラーとなります。
trait AsyncTrait { async fn f() { println!("Couldn't compile"); } }
async-trait-sandbox is 📦 v0.1.0 via 🦀 v1.44.0 on ☁️ ap-northeast-1
❯ cargo check
Checking async-trait-sandbox v0.1.0
error[E0706]: functions in traits cannot be declared `async`
--> src/main.rs:8:5
|
8 | async fn f() {
| ^----
| |
| _____`async` because of this
| |
9 | | println!("Couldn't compile");
10 | | }
| |_____^
|
= note: `async` trait functions are not currently supported
= note: consider using the `async-trait` crate: https://crates.io/crates/async-trait
async-trait
トレイトのメソッドに async をつけるためには、async-trait というクレートを利用する必要があります。
試す前の準備
この記事では、次のクレートを用いて実装を行います。
- async-trait: 今回のメインテーマです。
- futures: 後ほど async で定義された関数を実行するために使用します。
使用したバージョンは下記です。
[dependencies] async-trait = "0.1.36" futures = "0.3.5"
async-trait とは
async 化したいトレイトに対して #[async_trait] というマクロを付け足すことで async fn ... という記法を可能にしてくれるスグレモノです。次のコードはコンパイルが通るようになり、自身のアプリケーションで利用可能になります。
use async_trait::async_trait; #[async_trait] trait AsyncTrait { async fn f() { println!("Could compile"); } }
中身はマクロ
どのような仕組みで動いているのでしょうか。#[async_trait] アトリビュートの中身を少し確認してみましょう。
// (...) extern crate proc_macro; // (...) use crate::args::Args; use crate::expand::expand; use crate::parse::Item; use proc_macro::TokenStream; use quote::quote; use syn::parse_macro_input; #[proc_macro_attribute] pub fn async_trait(args: TokenStream, input: TokenStream) -> TokenStream { let args = parse_macro_input!(args as Args); let mut item = parse_macro_input!(input as Item); expand(&mut item, args.local); TokenStream::from(quote!(#item)) }
Procedural Macros のオンパレード*2のようです。ということで、マクロがどう展開されているのかを見てみましょう。cargo-expand という cargo のプラグインを利用すると、展開後のマクロの状況を知ることができます。
実際に使ってみると:
❯ cargo expand
Checking async-trait-sandbox v0.1.0
Finished check [unoptimized + debuginfo] target(s) in 0.18s
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std;
use async_trait::async_trait;
fn main() {}
pub trait AsyncTrait {
#[must_use]
fn f<'async_trait>() -> ::core::pin::Pin<
Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>,
> {
#[allow(
clippy::missing_docs_in_private_items,
clippy::needless_lifetimes,
clippy::ptr_arg,
clippy::type_repetition_in_bounds,
clippy::used_underscore_binding
)]
async fn __f() {
{
::std::io::_print(::core::fmt::Arguments::new_v1(
&["Could compile\n"],
&match () {
() => [],
},
));
};
}
Box::pin(__f())
}
}
async fn f() というメソッドは、マクロによって fn f<'async_trait>() -> ::core::pin::Pin<Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>> へと展開されています。では肝心の async はどこに行ってしまったのかというと、関数の中にて async ブロックとして処理されています。そもそも async 自体が Future のシンタックスシュガーなので、こういった結果になっているわけです*3。
呼び出し
実際に関数を呼び出しをしてみましょう。次のようなコードを書くと、呼び出しのチェックをできます。
use async_trait::async_trait; use futures::executor; fn main() { let runner = Runner {}; executor::block_on(runner.f()); } #[async_trait] pub trait AsyncTrait { async fn f(&self); } struct Runner {} #[async_trait] impl AsyncTrait for Runner { async fn f(&self) { println!("Hello, async-trait"); } }
これでコンパイルを通せるようになります。実行してみると、
❯ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.06s
Running `target/debug/async-trait-sandbox`
Hello, async-trait
しっかり意図したとおりの標準出力を出してくれました。便利ですね。
*1:一応 RFC は出ています→https://github.com/rust-lang/rfcs/issues/2739
*2:この記事の主題ではなくなってしまうので Procedural Macros に関する解説はしませんが、この記事で使い方をかなり詳しく解説してくれています。
*3:かなり説明を端折ってしまっています。こちらのドキュメントに詳細が書いてあります。