Rust(with Iced)でGUIのタイマーを作ってみた

3 min読了の目安(約2200字TECH技術記事

対局時計を作ろうと思い、その手始めとしてタイマーを作ってみたのですが非同期が絡む都合で案外大変だったので共有します。なお以下のことは基本的に、IcedのGitHubのexamples内にあるstopwatchを文章化したような内容になっています。

環境

  • rustc: 1.46.0
  • cargo: 1.46.0
  • iced: 0.1.1
  • futures-timer: 3.0.2

ライブラリ選定

今回はGUIライブラリとしてパッと見、使いやすそうだったのでIcedを選びました。IcedはElm風のクロスプラットフォームなGUIライブラリです。またIcedは非同期ライブラリをiced_futuresという形でラッピングしているため、非同期ライブラリをfutures、tokio、async-stdの中から好きに選べるのですが、今回はデフォルトのfuturesを選びました。

Icedで非同期

Icedでは非同期な処理をする際の基本の形は以下のような感じです。

pub struct App;

impl iced::Application for App {
    fn subscription(&self) -> iced::Subscription<Self::Message> {
        iced::Subscription::from_recipe(/* impl iced_futures::subscription::Recipe な何か */)
            .map(/* 後述のTimerRecipe::OutputからApp::Messageへの変換 */)
    }

    /* その他newなど要求されるもの */
}

このRecipeというのが非同期処理の部分で、適当なstructimpl Recipeをしてその中でRecipe::stream(self, input: BoxStream<Event>) -> BoxStream<Self::Output>に非同期処理をfutures::stream::Streamの形で実装します。

futuresでタイマー

tokioやasync-stdを使う場合、intervalのような関数があり、またiced_futures::timeが増える(futuresのみの場合は存在しない)ので苦労はないのですが、futuresには残念ながらありません。今回は単にタイマーを使いたかっただけなのでasync-stdやtokioなど大層なものを入れる気にならず、代わりにfutures-timerというそのものな名前のものがあったのでこれを使います。
futures-timerは非常にシンプルで、futures_timer::Delayというimpl core::future::Futureなstructが有るだけです。今回はこれをBoxStreamにしなければいけず、またtokioのintervalのように繰り返さなければいけないので、ここでfuturesのstream::unfoldを使います。具体的には以下のようになります。

pub struct TimerRecipe(std::time::Duration);

impl<H: std::hash::Hash, Event> iced_futures::subscription::Recipe<H, Event> for TimerRecipe {
    fn stream(self: Box<Self>, _input: BoxStream<Event>) -> BoxStream<Self::Output> {
        stream::unfold(self.0, |d| async move {
            Delay::new(d).await;
            let now = Instant::now();
            Some((now, d))
        })
        .boxed()
    }

    /* その他hash等要求されるもの */
}

ここまで作れば後は(特にiced::Application::subscriptionを明示的に呼ぶこと無く)普通に実行すればTimerRecipeに指定したDurationで定期的にApp::Messageが通知されApp::updateで受け取ることができます。

あとがき

今回は単にタイマーを作りたかっただけなのでfuturesで頑張って書きましたが、もう少しいろいろと非同期で行う場合には素直にtokioやasync-std(ただしasync-stdの場合はunstable featureを有効化する必要があります)を使うと良いかもしれないと感じました。Rustの非同期ライブラリは決定版というものが(恐らく)ないですが、tokioが最有力な感じでしょうか。