🍢

Rust 1.75でtraitでasync fnが書けるようになったらしいよ

2024/01/06に公開2

Rust 1.75

2023年12月28日にRust1.75.0がリリースされました。
https://blog.rust-lang.org/2023/12/28/Rust-1.75.0.html

今回のアップデート、実は個人的にはものすごく待ち望んでいた機能が実装されたものになっています。

それは、記事のタイトルにもある通り、trait内のメソッドでasync fnを書けるようになるというものです。

Rust 1.75以前

今回のアップデート以前はtraitにasyncなfnを直接書く事ができず、async_traitというcrateを使う事で、traitにasync fnを書く事ができるようになっていました。

この度のアップデートによって、async_traitは不要になるはずでした。

だがしかし

結論から言うと、私のユースケースにおいては、そのまますんなりと移行できるものではありませんでした。ぴえん。

確かにtrait内でasync fnを直接書けるようにはなっていますし、大多数のケースでは問題なく使えるはずなので、多くの人が待ち望んでいたアップデートである事に間違いありません。

使用できないケース

traitにasync fnを書く事はできますし、それをimplするところまではできました。

以下、実際のコードから抜粋します。

pub trait WildDocScript {
    fn new(
        include_adaptor: Arc<Mutex<Box<dyn IncludeAdaptor + Send>>>,
        cache_dir: PathBuf,
        stack: &Stack,
    ) -> Result<Self>
    where
        Self: Sized;
    async fn evaluate_module(&mut self, file_name: &str, src: &str, stack: &Stack) -> Result<()>;
    async fn eval(&mut self, code: &str, stack: &Stack) -> Result<WildDocValue>;
}

そして、これをimplしてオブジェクトを作るところまではできました。(=コンパイルが通ります)

impl WildDocScript for Deno {
    fn new(
        include_adaptor: Arc<Mutex<Box<dyn IncludeAdaptor + Send>>>,
        cache_dir: PathBuf,
        stack: &Stack,
    ) -> Result<Self> {
    	    //細かい処理は割愛
    }
    async fn evaluate_module(&mut self, file_name: &str, src: &str, _: &Stack) -> Result<()> {
	    //細かい処理は割愛
    }
    async fn evaluate_module(&mut self, file_name: &str, src: &str, _: &Stack) -> Result<()> {
	    //細かい処理は割愛
    }
}

↓WildDocScriptをimplしたDenoオブジェクトを作成

let deno = Deno::new(
	Arc::clone(&include_adaptor),
	cache_dir.to_owned(),
	&stack,
);

ここまではコンパイルは通ります。

問題はここからです。

pub struct Parser {
    scripts: HashMap<String, Box<dyn WildDocScript>>,
}

このように、structにdynでtraitオブジェクトを持たせようとしたら死にました。

なぜdynだとダメなのか?

error[E0038]: the trait `WildDocScript` cannot be made into an object
  --> wild-doc\wild-doc\src\parser.rs:49:34
   |
49 |     scripts: HashMap<String, Box<dyn WildDocScript>>,
   |                                  ^^^^^^^^^^^^^^^^^ `WildDocScript` cannot be made into an object
   |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
  --> wild-doc\wild-doc-script\src\lib.rs:25:14
   |
25 |     async fn evaluate_module(&mut self, file_name: &str, src: &str, stack: &Stack) -> Result<()>;
   |              ^^^^^^^^^^^^^^^ the trait cannot be made into an object because method `evaluate_module` is `async`
26 |     async fn eval(&mut self, code: &str, stack: &Stack) -> Result<WildDocValue>;
   |              ^^^^ the trait cannot be made into an object because method `eval` is `async`
   = help: the following types implement the trait, consider defining an enum where each variant holds one of these types, implementing `WildDocScript` for this new enum and using it instead:
             wild_doc_script_deno::Deno
             wild_doc_script_python::WdPy
             script::var::Var

エラーメッセージを翻訳して読むと、

メソッド evaluate_moduleasync であるため、トレイトをオブジェクトにできません

といった事が書かれています。

さらに、

the following types implement the trait, consider defining an enum where each variant holds one of these types, implementing WildDocScript for this new enum and using it instead:
wild_doc_script_deno::Deno
wild_doc_script_python::WdPy
script::var::Var

という事が書かれており、
これはつまり、(試してはいませんが)

enum WildDocScriptEnum{
	Deno(Deno),
	WdPy(WdPy),
	Var(Var),
}
pub struct Parser {
    scripts: HashMap<String, WildDocScriptEnum>,
}

のような事をすれば解決できそうではあります。

もう一つ、重要なエラーメッセージが書かれています。

note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit https://doc.rust-lang.org/reference/items/traits.html#object-safety

(日本語訳)

注: トレイトを「オブジェクトセーフ」にするには、呼び出しを動的に解決できるように vtable を構築できるようにする必要があります。詳細については、https://doc.rust-lang.org/reference/items/traits.html#object-safety をご覧ください。

呼び出しを動的に解決できるように vtable を構築できるようにする必要があります。
との事なので、このままではvtableが構築できずに、オブジェクトセーフではない、と判断されているような言い回しです。

vtableというのは動的ディスパッチに使用されるもののはずですので、dynでオブジェクトを格納しようとしている今回のケースに符合します。

纏めると、

という事になりますが、これら二つの理由に関連性があるのか無いのかがよくわかりません。

オブジェクトセーフについてのリンク先の詳細
https://doc.rust-lang.org/reference/items/traits.html#object-safety
を確認しても、asyncを含むtraitがオブジェクトアウト(?)であるというような記述は見当たりません。しかし、これについてはドキュメントが実装に追いついてない、という可能性もあります。
asyncがあるからオブジェクトにできない=オブジェクトセーフではない。と仰っているのであれば、この二つエラーメッセージは同じ事を言っている事になります。

まとめ

今のところ、async fnを含むtraitについては、静的ディスパッチできるものに限って使用できるもので、動的ディスパッチには対応していない、という状態だと思われます。
enumを使え、というコンパイラからのアドバイスは、間接的には静的ディスパッチ化しろと言っているようなものです。
エラーメッセージの内容からして、async fnがあるとvtableが構築できない?という状態なのかな、と考えています。

※静的ディスパッチ、動的ディスパッチについては何となくの理解に留まっているので、いずれまた詳細に調べて記事にしてみようかなと思います。

dynを使うケースはそれほど多くないにしても、プラグイン的な機構を仕込みたい時などは需要として存在するはずなので、今後のアップデートに期待したいところです。

Discussion

白山風露白山風露

Rust 1.75で async fn が書けるようになったのは、トレイト内 impl Trait が安定化したためです。

そもそも、

async fn f() -> T {
    todo!()
}

という関数は

fn f() -> impl std::future::Future<Output = T> {
    async { todo!() }
}

とほぼ等価であり、

fn f() -> impl Trait;

は実際には

struct T{ /* ... */ }
impl Trait for T { /* ... */ }
fn f() -> T;

のような関数として扱われます。

したがって、

trait A {
    async fn f(&self) -> T;
}

というトレイトは、実際には

trait A {
    type Output: std::future::Future<Output = T>;
    fn f(&self) -> Self::Output;
}

のようなトレイトとして扱われ、オブジェクトセーフになることはありません。

オブジェクトセーフでかつawaitableな関数を持つトレイトを定義したいときは、たとえば

trait SafeA {
    fn f(&self) -> std::pin::Pin<Box<dyn '_ + std::future::Future<Output = T>>>;
}

とすれば SafeA はオブジェクトセーフなトレイトになり、

impl<T: A> SafeA for T {
    fn f(&self) -> std::pin::Pin<Box<dyn  '_ + std::future::Future<Output = T>>> {
        Box::pin(A::f(self))
    }
}

とすれば任意の A を実装した型を dyn SafeA にして扱うことができます。

ODENODEN

コメントありがとうございます。

trait A {
    type Output: std::future::Future<Output = T>;
    fn f(&self) -> Self::Output;
}

のようなトレイトとして扱われ、オブジェクトセーフになることはありません。

この事から、

公式ドキュメント
https://doc.rust-lang.org/reference/items/traits.html#object-safety

の記載にある

It must not have any associated constants.

に該当してオブジェクトセーフではない、という事ですね。