借り初めのひみつきち

仮ブログです。

MYOS のウィンドウのつくりかた

MYOS/TOE のアプリでウィンドウを作るところを少し詳しく見てみましょう。

※ 執筆時点のバージョンの TOE をもとに記述しています。まだ API 安定化してないのでバージョンごとに細かい違いがあったり MYOS と TOE の間でも微妙に違うことがあります。

アプリから WindowBuilder

まずはアプリの中で WindowBuilder のメソッドでウィンドウの情報を設定して最後に build() を呼び出すと Windowインスタンスが作られます。

    let window = WindowBuilder::new()
        .size(Size::new(200, 200))
        .bg_color(WindowColor::BLACK)
        .build("bball");

WindowBuilder の中身

myoslib::window::WindowBuilder はウィンドウの作成に必要なパラメータをまとめたビルダーパターンと呼ばれる構造体のひとつで、現在は以下のような内容になっています。

pub struct WindowBuilder {
    size: Size,
    bg_color: WindowColor,
    flag: u32,
}

ウィンドウを作るにはいろいろな情報が必要なので、 myoslib::window::WindowBuilder.build() の中で self にまとめた情報をもとに os_new_window2() を呼び出してウィンドウハンドルを取得し、ウィンドウハンドルから Windowインスタンスを作成します。

pub fn build(self, title: &str) -> Window {
    let handle = WindowHandle(os_new_window2(
        title,
        self.size.width() as usize,
        self.size.height() as usize,
        self.bg_color.0 as usize,
        self.flag as usize,
    ));
    Window { handle }
}

システムコール

myoslib::syscall::os_new_window2() の中ではパラメータの型を調整して svc6() を呼び出します。

pub fn os_new_window2(
    title: &str,
    width: usize,
    height: usize,
    bg_color: usize,
    flag: usize,
) -> usize {
    unsafe {
        svc6(
            Function::NewWindow,
            title.as_ptr() as usize,
            title.len(),
            width,
            height,
            bg_color,
            flag,
        )
    }
}

svc6myoslib::syscall の中で定義された外部関数で、 WebAssembly の外の世界の関数を呼び出すことを意味します。

#[link(wasm_import_module = "megos-canary")]
extern "C" {
    pub fn svc0(_: Function) -> usize;
    (中略)
    pub fn svc6(_: Function, _: usize, _: usize, _: usize, _: usize, _: usize, _: usize) -> usize;
}

myosabi::svc::Function::NewWindowenum6 番と定義されているので svc6 の最初の引数はコンパイル結果のバイナリでは数値 6 になります。

pub enum Function {
    (中略)
    NewWindow = 6,
    (中略)
}

壮大な茶番

実はここまでのほとんどの処理はリリースビルドでは Rust の優れた最適化によって消滅します。(デバッグビルドなら残るかもしれません) 以下のように svc6 に直接引数を渡して呼び出すようにコンパイルされて、 WindowBuilder の構造体をゴニョゴニョするくだりは通常のアプリではビルド結果に残りません。

 0000ac: 41 06                      | i32.const 6
 0000ae: 41 80 80 82 80 00          | i32.const 32768
 0000b4: 41 05                      | i32.const 5
 0000b6: 41 c8 01                   | i32.const 200
 0000b9: 41 c8 01                   | i32.const 200
 0000bc: 41 00                      | i32.const 0
 0000be: 41 00                      | i32.const 0
 0000c0: 10 80 80 80 80 00          | call 0 <_ZN7myoslib7syscall4svc617h2ba79744244edc14E>

svc6 は WebAssembly の外の世界にあるので、最終的に svc6 を呼び出す部分だけが残ります。

WebAssembly の外へ

svc6 は WebAssembly のインスタンスを作るときに kernel::rt::megos::arle::ArleBinaryLoader::load() にわたすリゾルバの中で kernel::rt::megos::arle::ArleRuntime::syscall にダイナミックリンクされます。 svc0svc6 の実体は実は全て同じです。

fn load(&mut self, blob: &[u8]) -> Result<(), ()> {
    self.loader
        .load(blob, |mod_name, name, _type_ref| match mod_name {
            ArleRuntime::MOD_NAME => match name {
                "svc0" | "svc1" | "svc2" | "svc3" | "svc4" | "svc5" | "svc6" => {
                    Ok(ArleRuntime::syscall)
                }
                _ => Err(WasmDecodeError::DynamicLinkError),
            },
            _ => Err(WasmDecodeError::DynamicLinkError),
        })
        .map_err(|_| ())
}

ArleRuntime::syscall

kernel::rt::megos::arle::ArleRuntime::syscall は第1引数として WebAssembly モジュールのインスタンス、第2引数として実際に関数呼び出しで渡された引数の配列スライスを受け取ります。

fn syscall(_: &WasmModule, params: &[WasmValue]) -> Result<WasmValue, WasmRuntimeError> {
    Scheduler::current_personality(|personality| match personality.context() {
        PersonalityContext::Arlequin(rt) => rt.dispatch_syscall(&params),
        _ => unreachable!(),
    })
    .unwrap()
}

実際の API 実装ではネイティブのハンドルとアプリ固有のハンドルを仲介したりスレッドに紐付いたデータを管理するためにコンテキスト情報が欲しいので、 kernel::task::scheduler::Scheduler::current_personality() を呼び出して kernel::rt::megos::arle::ArleRuntime のスレッドに紐付いたランタイムインスタンスを取得します。 MYOS/TOE でスレッドにランタイムのインスタンスを紐付ける仕組みを Personality と言います。 ランタイムのインスタンスが取得できたら kernel::rt::megos::arle::ArleRuntime::dispatch_syscall() を呼び出します。

また、ここでやりとりしている WasmValue というのは以下のような enum で WebAssembly のプリミティブの値を型情報と一緒にラッピングしたものです。*1 WasmValue の配列スライスを受け渡すことで実際の関数の引数がどんな型でも同様に扱うことができます。

pub enum WasmValue {
    Empty,
    I32(i32),
    I64(i64),
    F32(f32),
    F64(f64),
}

ArleRuntime::dispatch_syscall

kernel::rt::megos::arle::ArleRuntime::dispatch_syscall() の中では svc6 を呼び出した際の引数のリストをデコードして最初の値を myosabi::svc::Function 型に復元し、 match 文で一致する機能番号を探します。

fn dispatch_syscall(&mut self, params: &[WasmValue]) -> Result<WasmValue, WasmRuntimeError> {
    let mut params = ParamsDecoder::new(params);
    let memory = self.module.memory(0).ok_or(WasmRuntimeError::OutOfMemory)?;
    let func_no = params.get_u32().and_then(|v| {
        svc::Function::try_from(v).map_err(|_| WasmRuntimeError::InvalidParameter)
    })?;

    match func_no {

myosabi::svc::Function::NewWindow が一致したので、残りの引数からネイティブのウィンドウビルダーを呼び出してネイティブのウィンドウハンドルを取得し、アプリ固有のハンドルに変換して最終的に WasmValue::I32 でラッピングしてアプリに返します。

        (中略)
        svc::Function::NewWindow => {
            let title = params.get_string(memory).unwrap_or("");
            let size = params.get_size()?;
        (中略)
            let window = WindowBuilder::new(title)
                .size(size)
                .build();
        (中略)
            self.windows.insert(handle, window);
            return Ok(WasmValue::I32(handle as i32));
        }

ここまで実行してアプリケーションがウィンドウを作成してハンドルを受け取ることができました。

*1:WebAssembly MVP には i32 i64 f32 f64 の数値型と空の5種類のプリミティブ型しか存在しません