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, ) } }
svc6
は myoslib::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::NewWindow
は enum で 6
番と定義されているので 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
にダイナミックリンクされます。 svc0
〜 svc6
の実体は実は全て同じです。
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(¶ms), _ => 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種類のプリミティブ型しか存在しません