Rust 1.75でtraitでasync fnが書けるようになったらしいよ
Rust 1.75
2023年12月28日にRust1.75.0がリリースされました。
今回のアップデート、実は個人的にはものすごく待ち望んでいた機能が実装されたものになっています。
それは、記事のタイトルにもある通り、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_moduleがasyncであるため、トレイトをオブジェクトにできません
といった事が書かれています。
さらに、
the following types implement the trait, consider defining an enum where each variant holds one of these types, implementing
WildDocScriptfor 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でオブジェクトを格納しようとしている今回のケースに符合します。
纏めると、
という事になりますが、これら二つの理由に関連性があるのか無いのかがよくわかりません。
オブジェクトセーフについてのリンク先の詳細
を確認しても、asyncを含むtraitがオブジェクトアウト(?)であるというような記述は見当たりません。しかし、これについてはドキュメントが実装に追いついてない、という可能性もあります。
asyncがあるからオブジェクトにできない=オブジェクトセーフではない。と仰っているのであれば、この二つエラーメッセージは同じ事を言っている事になります。
まとめ
今のところ、async fnを含むtraitについては、静的ディスパッチできるものに限って使用できるもので、動的ディスパッチには対応していない、という状態だと思われます。
enumを使え、というコンパイラからのアドバイスは、間接的には静的ディスパッチ化しろと言っているようなものです。
エラーメッセージの内容からして、async fnがあるとvtableが構築できない?という状態なのかな、と考えています。
※静的ディスパッチ、動的ディスパッチについては何となくの理解に留まっているので、いずれまた詳細に調べて記事にしてみようかなと思います。
dynを使うケースはそれほど多くないにしても、プラグイン的な機構を仕込みたい時などは需要として存在するはずなので、今後のアップデートに期待したいところです。
Discussion
Rust 1.75で
async fnが書けるようになったのは、トレイト内impl Traitが安定化したためです。そもそも、
という関数は
とほぼ等価であり、
は実際には
のような関数として扱われます。
したがって、
というトレイトは、実際には
のようなトレイトとして扱われ、オブジェクトセーフになることはありません。
オブジェクトセーフでかつawaitableな関数を持つトレイトを定義したいときは、たとえば
とすれば
SafeAはオブジェクトセーフなトレイトになり、とすれば任意の
Aを実装した型をdyn SafeAにして扱うことができます。コメントありがとうございます。
この事から、
公式ドキュメント
の記載にある
に該当してオブジェクトセーフではない、という事ですね。