完全な小ネタです。使ってみた記事です。
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:かなり説明を端折ってしまっています。こちらのドキュメントに詳細が書いてあります。