🪄

Rustでasync_traitを使わずに非同期なメソッドを定義する方法

2024/01/17に公開
3

Rustではtraitでasyncなメソッドを定義したい場合、async_traitを使う方法が一般的かと思います。

しかし変にクレートを増やしたくなく、非同期なメソッドをasync_traitを使わずに定義するにはどうすればよいのでしょうか?

結論から書くとFutureを返す形で定義することができます。

use std::future::Future;
use std::pin::Pin;

trait MyTrait {
    // 非同期メソッドを定義
    fn async_method(&self) -> Pin<Box<dyn Future<Output = String> + Send>>;
}

struct MyStruct;

impl MyTrait for MyStruct {
    fn async_method(&self) -> Pin<Box<dyn Future<Output = String> + Send>> {
        // 非同期処理を実装
        Box::pin(async {
            // 非同期処理の内容
            "Hello, async world!".to_string()
        })
    }
}

#[tokio::main] // Tokioランタイムを使用
async fn main() {
    let my_struct = MyStruct;

    // asyncメソッドを呼び出し、Futureを取得
    let future = my_struct.async_method();

    // asyncブロック内で.awaitを使ってFutureを実行
    let result = future.await;

    println!("{}", result);
}

このような形でasync_traitを使わずに非同期なメソッドをtraitで定義し、実際にasyncブロックで実行することができます。

Pin<Box<dyn Future<Output = String> + Send>>

これは何をしているのかといえば、それぞれ以下のような意味合いがあります。

  • Pin: 特定の型のインスタンスがメモリ内で移動されないことを保証するために使用されます。
  • Box<dyn>: ヒープ、コンパイル時点でサイズが決まらないものを動的にヒープ領域で扱うときに使います。詳しくはこちら見てください
  • Future<>: 「何かの準備ができたら実行を再開できる」状態を表しています。要は後で実行できる状態を示しています。
  • Output = String: OutputはFutureのアソシエート型です。詳細は後述します
  • Send: マーカートレイトと呼ばれるもので、つけることで値が複数のスレッドから安全にアクセスされることをコンパイル時点で保証することができます。必ずしもつける必要はないです(実際に例のコードから+ Sendの部分を削除しても動きます)。マルチスレッドで扱いたい場合にはつけることが推奨されます。

アソシエート型は例を見るのが早いと思うので以下を見てください。

trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

このようなイテレータトレイトはトレイト時点では具体的な型を決めたくないわけで、実装時に具体的な型を注入する形が理想です。

アソシエート型はそれを実現するもので、Future<Output = String>のように実装時に型を注入することで、具体的な型を決めることができます。

Future<Output = String>は実行結果として文字列を返すというもので、例えばFuture<Output = ()>であれば何も返さないということになりますし、Future<Output = i32>であればi32を返すということになります。

Pin<Box<dyn Future<Output = String> + Send>>はFutureが返す型としてStringを注入し、さらに非同期であるためコンパイル時点ではサイズを決定できないためBox<dyn>で囲み、さらにPinでメモリ内で移動されないことを保証したものを返す。といったことになります。

追記: 非同期であるためコンパイル時点ではサイズを決定できないためは誤りでしたすみません。

Pinを使わなければいけない理由は正直私もそこまで理解度高くありませんが、Futureはポーリングによって状態が進行する都合、適時現在の自身の状態を確認する必要が出てくる場合があり、そのときに自己参照を行う可能性があります。

もしメモリ内で移動されないことを保証できていない場合、例えば関数の引数として渡すなどした場合にメモリが移動されてしまい、自己参照に失敗してしまいます。

そういった背景からメモリ安全性を保障するためにPinで囲う必要があるという理解です(詳しい方いればよければ教えてください!)

Box::pin(async {})

Box::pin(async {
    // 非同期処理の内容
    "Hello, async world!".to_string()
})

冒頭のコードの上記部分について何をしているかですが、まずFutureを返す必要があるためasyncブロックで囲まれた値を返す必要があります。

また今回のケースではFuture<Output = String>ということでアソシエート型にStringを指定しているため、asyncブロック内で文字列を返すような形で書きます。

Box::pinは見た目的にもなんとなくわかりそうですが、Pin<Box<dyn>>の形式で値を返すときに使う感じです。

今回冒頭で紹介したコードはRust Playgroundで実行することが可能ですので興味ある方は試してみてください。

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021

宣伝

不定期ですが、RustのOSS開発のライブコーディングをYoutubeでやっています。ただの作業ですがよければ気軽に見てもらえれば嬉しいです。

https://www.youtube.com/@torohash

Discussion

白山風露白山風露

非同期であるためコンパイル時点ではサイズを決定できないため

非同期であることとコンパイル時にサイズが決定できないことには特に因果関係がないですね。 dyn Future だとトレイトオブジェクトなので Sized ではなくなりますが、単純に Future を実装した Sized な型を作ることは可能です(そもそも async {} ブロック自体、内部的には impl Future + Sized であるユニークな型を持つ)

ちなみに Rust 1.75で async_fn_in_trait と return_position_impl_trait_in_trait が安定化されたので、

trait MyTrait {
    // async_fn_in_trait
    async fn async_method(&self) -> String;
}

trait MyTrait {
    // return_position_impl_trait_in_trait 
    fn async_method(&self) -> Future<Output = String> + Send;
}

といった書き方が可能です。
ただし、通常のasync関数は内部で保持する値がSendの場合auto traitでSendも付きますが、async_fn_in_trait ではSend境界は明示できないので、 return_position_impl_trait_in_trait を使う必要があります。

torohashtorohash

Rust 1.75.0の情報を記事作成時にキャッチしておらず、traitでasync関数を実装する場合Pin<Box<dyn>>で定義する必要がある認識で、そこから動的に作る必要がある -> コンパイル時にサイズ決定できないと理解していました。指摘通り「非同期であることとコンパイル時にサイズが決定できないことには特に因果関係がない」ですね。。
該当箇所については取り消し線で対応します、指摘ありがとうございました!

白山風露白山風露

あー、1.75以前だとtrait内では実質dyn Futureを使わざるを得なかったので因果関係があると言えばありましたね。asyncブロックの型を表記する方法がなかったので……