Rust の組込み用非同期フレームワーク Embassy (2)

Embassy をゆるーく眺めるシリーズです。 前回 README を見てみて Timer どうなっているのか気になりました。 なので実装をおいかけてみましょう。

アプリコードは大体こんな感じでした。 さてどこから行きましょうかね?

use embassy_executor::Spawner;
use embassy_time::{Duration, Timer};

#[embassy_executor::task]
async fn blink(pin: AnyPin) {
    let mut led = Output::new(pin, Level::Low, OutputDrive::Standard);

    loop {
        // Timekeeping is globally available, no need to mess with hardware timers.
        led.set_high();
        Timer::after(Duration::from_millis(150)).await;
        led.set_low();
        Timer::after(Duration::from_millis(150)).await;
    }
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = embassy_nrf::init(Default::default());

    // Spawned tasks run in the background, concurrently.
    spawner.spawn(blink(p.P0_13.degrade())).unwrap();
}

気になるところ片っ端からいってみましょう。 まずは main についてるマクロ!

#[embassy_executor::main]

embassy-macros が中身っぽいですね。

embassy-macros

README を見ます。

The task and main macros require the type alias impl trait (TAIT) nightly feature in order to compile.

なるほど? type alias impl trait ? これか。

rust-lang.github.io

type alias つくるときに impl Trait が指定できるように、ということのようですね。 確かにこれは便利そうです。

type Foo = impl Bar;

何がブロッカーなのかと言うと…?けっこう色々 TODO 残ってますね。 もうちょっと時間かかりそうです。残念。

github.com

マクロの展開結果

とりあえずマクロの実装いきなり見る前に軽く展開結果を見ておきましょう。cargo-expand があれば…!

cargo install cargo-expand

あとは、 examples/std の下のホストでビルドできるバイナリを指定してみます。 examles/std/bin/tick.rs 小さくてちょうど良いのがありました。

#![feature(type_alias_impl_trait)]

use embassy_executor::Spawner;
use embassy_time::{Duration, Timer};
use log::*;

#[embassy_executor::task]
async fn run() {
    loop {
        info!("tick");
        Timer::after(Duration::from_secs(1)).await;
    }
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    env_logger::builder()
        .filter_level(log::LevelFilter::Debug)
        .format_timestamp_nanos()
        .init();

    spawner.spawn(run()).unwrap();
}

cargo-expand でマクロ展開します。

cargo expand --bin tick
#![feature(prelude_import)]
#![feature(type_alias_impl_trait)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use embassy_executor::Spawner;
use embassy_time::{Duration, Timer};
use log::*;
#[doc(hidden)]
async fn __run_task() {
    loop {
        {
            let lvl = ::log::Level::Info;
            if lvl <= ::log::STATIC_MAX_LEVEL && lvl <= ::log::max_level() {
                ::log::__private_api_log(
                    format_args!("tick"),
                    lvl,
                    &("tick", "tick", "src/bin/tick.rs", 10u32),
                    ::log::__private_api::Option::None,
                );
            }
        };
        Timer::after(Duration::from_secs(1)).await;
    }
}
fn run() -> ::embassy_executor::SpawnToken<impl Sized> {
    type Fut = impl ::core::future::Future + 'static;
    const POOL_SIZE: usize = 1;
    static POOL: ::embassy_executor::raw::TaskPool<Fut, POOL_SIZE> = ::embassy_executor::raw::TaskPool::new();
    unsafe { POOL._spawn_async_fn(move || __run_task()) }
}
#[doc(hidden)]
async fn ____embassy_main_task(spawner: Spawner) {
    {
        env_logger::builder()
            .filter_level(log::LevelFilter::Debug)
            .format_timestamp_nanos()
            .init();
        spawner.spawn(run()).unwrap();
    }
}
fn __embassy_main(spawner: Spawner) -> ::embassy_executor::SpawnToken<impl Sized> {
    type Fut = impl ::core::future::Future + 'static;
    const POOL_SIZE: usize = 1;
    static POOL: ::embassy_executor::raw::TaskPool<Fut, POOL_SIZE> = ::embassy_executor::raw::TaskPool::new();
    unsafe { POOL._spawn_async_fn(move || ____embassy_main_task(spawner)) }
}
unsafe fn __make_static<T>(t: &mut T) -> &'static mut T {
    ::core::mem::transmute(t)
}
fn main() -> ! {
    let mut executor = ::embassy_executor::Executor::new();
    let executor = unsafe { __make_static(&mut executor) };
    executor
        .run(|spawner| {
            spawner.must_spawn(__embassy_main(spawner));
        })
}

そこまで黒魔術じゃなさそうですが…。 一旦気にせず読んでみます。

#[embassy_executor::main] をつけると、その内容は ____embassy_main_task に rename されて、前段に __embassy_mainmain が挟まっています。 このあたりの main をトランポリンするのは組込み Rust だと頻出のテクニックですね。

で、一番最初の main を見ると executor を作って、 executor を static にして、executor.run() から __embassy_main() を呼び出す、という実装になっています。

fn main() -> ! {
    let mut executor = ::embassy_executor::Executor::new();
    let executor = unsafe { __make_static(&mut executor) };
    executor
        .run(|spawner| {
            spawner.must_spawn(__embassy_main(spawner));
        })
}

async の main としては main から戻ることがないので、main 関数のスコープで作ったローカル変数を static にしてしまっても良い、と。 main のシグニチャmain() -> ! となっているので、executor.run() は無限ループになっているのでしょう。 そのうち読む時のために頭の片隅に置いておきましょう。

あと、spawner の詳細も気になるところですが、一旦おいておきましょう。

で、__embassy_main はこう。 ここで、TAIT 使ってますね。

fn __embassy_main(spawner: Spawner) -> ::embassy_executor::SpawnToken<impl Sized> {
    type Fut = impl ::core::future::Future + 'static;
    const POOL_SIZE: usize = 1;
    static POOL: ::embassy_executor::raw::TaskPool<Fut, POOL_SIZE> = ::embassy_executor::raw::TaskPool::new();
    unsafe { POOL._spawn_async_fn(move || ____embassy_main_task(spawner)) }
}

戻り値の型が ::embassy_executor::SpawnToken<impl Sized> 今の知識じゃわからないですね。 pool を作って、async fn を spawn していますが、このあたりも読んでいかないと、ですね。 embassy-executor/src/raw/mod.rs に TaskPool の実装がありました。 次はこのあたり読んでいきましょう。

ではでは。