自作Cコンパイラから出力するWASMのバイナリコードをWASIに準拠させたいが、WASIのAPIに関する情報が探せずに困ってる、というはなし。
WASIの不満な点は:
- 情報がどこにあるかわからない
- APIの仕様がWASMのバイナリ用ではなく、Rustの関数の形式でしか書かれてない
- 引数に与える値がわからない
- 戻り値がわからない
概要
WASMはバイナリフォーマットが決められているがそれだけだと単なる演算しかできなくて、入出力などの外部とのやり取りはインポートした関数の呼び出し経由て行うことになる。
どのような関数を用意してやるかはまったく自由なので、現状はPOSIX風なオレオレ定義でやっている。
しかしそれだと生成した.wasmバイナリの可搬性が悪いので困ることになる。
そのような経緯でWASI(WebAssembly System Interface)というものが出てきた(たぶん)。
WASIはAPI(インポートされる関数の仕様)の規定で、それに従えばバイナリをWASI準拠の別の環境に持っていっても動かすことができる。
自作Cコンパイラからの出力もそれに準拠したほうが可搬性が上がるのでよいだろう、と思ってるんだが情報が不足していて苦労している。
やったこと
ハローWASIワールド
ひとまずハローワールド、ということで文字列出力から探る。
インポートのモジュール名は wasi_snapshot_preview1
(これもどうかと思うが…)、
エントリポイントは _start
(_start
はWASIのシステム予約で、ユーザ側は main
からってんじゃないんだ)、
ファイル出力は fd_write
(Iov
ってなによ?複数渡したいケースってあるんか?)
などいろいろ戸惑いながら。
コンパイルで出力した.wasmをWasmerやWasmtimeで動かすことでWASI相当かどうかを確認できる。
さてじゃあファイルオープンをやってみるか、ではたと困った。
該当するAPIはつらつら見ていったところ path_open
だと思うが、これを動かせるようになるまでにかなり苦労した。
ファイルオープン
path_openの定義を見てもわからないことだらけ。
path
以外の引数がどれもなにを与えるのかわからない。
- 渡す
fd
が不明、「POSIXの openat
と似せている」と書かれているのでググって、カレントディレクトリからの相対にしたい場合には AT_FDCWD
(Linuxだと-100
)を渡す?
- フラグは不明なので適当に0を渡してみる
- Rustの形式で書かれても、WASMには
string
もResult<>
もないからどうすんのよ?
結局string
は文字列へのポインタと長さ、戻り値のResult<>
はfd
の格納先アドレスを渡して、WASM側からは戻り値がAPIの成功/失敗が取得できることはわかった。
しかしあれこれ引数をいじっても成功しなかった。
サンプルを解析
推測で動かしても埒があかなかったので、RustからWASI用にコンパイルして動くバイナリを作成して、それを解析することにした。
Rust で WASI 対応の WebAssembly を作成して実行 - なんとなくな Developer のメモ を参考に、Rustからwasmを出力できるようにする。
まずは環境セットアップ:
$ rustup target add wasm32-wasi
|
ファイル操作のRustのコードはRust By Example > 標準ライブラリのその他 > ファイルI/O > open を参考に:
use std::fs::File; use std::io::prelude::*;
fn main() { let mut file = match File::open("README.md") { Err(why) => panic!("couldn't open: {}", why), Ok(file) => file, };
let mut s = String::new(); match file.read_to_string(&mut s) { Err(why) => panic!("couldn't read: {}", why), Ok(size) => (), } }
|
上記をコンパイル:
$ rustc --target wasm32-wasi sample1.rs
|
して実行すると、WasmerやWasmtimeでもファイルは開けず実行時エラーが出る。
ググってくとWASIでは開けるファイルが制限されていて、コマンドラインオプションの --dirs=
や --mapdir=
でプリオープンとして許可する必要があるとのこと。
そんなことすら分かってなかったぜ。
でひとまず動くようになった。
このバイナリを解析しようとWabtの wasm-objdump
の -d
オプションで逆アセンブルしてみたが、読むのはまあ辛い。
--section import --details
でインポートしている関数が見れるが、字面で追えるのはその程度:
$ wasm-objdump -d sample1.wasm
$ wasm-objdump --section import --details sample1.wasm
sample1.wasm: file format wasm 0x1
Section Details:
Import[11]: - func[0] sig=5 <_ZN4wasi13lib_generated22wasi_snapshot_preview115fd_filestat_get17hec7d68c8947be618E> <- wasi_snapshot_preview1.fd_filestat_get - func[1] sig=8 <_ZN4wasi13lib_generated22wasi_snapshot_preview17fd_read17hea1016e83fd563b4E> <- wasi_snapshot_preview1.fd_read - func[2] sig=9 <_ZN4wasi13lib_generated22wasi_snapshot_preview17fd_seek17hcce9e3a4c7fb667eE> <- wasi_snapshot_preview1.fd_seek - func[3] sig=8 <_ZN4wasi13lib_generated22wasi_snapshot_preview18fd_write17haadc9440e6dddc5cE> <- wasi_snapshot_preview1.fd_write - func[4] sig=10 <_ZN4wasi13lib_generated22wasi_snapshot_preview19path_open17hbf8e6e2761772feeE> <- wasi_snapshot_preview1.path_open - func[5] sig=5 <__imported_wasi_snapshot_preview1_environ_get> <- wasi_snapshot_preview1.environ_get - func[6] sig=5 <__imported_wasi_snapshot_preview1_environ_sizes_get> <- wasi_snapshot_preview1.environ_sizes_get - func[7] sig=4 <__imported_wasi_snapshot_preview1_fd_close> <- wasi_snapshot_preview1.fd_close - func[8] sig=5 <__imported_wasi_snapshot_preview1_fd_prestat_get> <- wasi_snapshot_preview1.fd_prestat_get - func[9] sig=7 <__imported_wasi_snapshot_preview1_fd_prestat_dir_name> <- wasi_snapshot_preview1.fd_prestat_dir_name - func[10] sig=1 <__imported_wasi_snapshot_preview1_proc_exit> <- wasi_snapshot_preview1.proc_exit
|
APIログからAPIの使い方を推測
どうしようかと思ってると、Wasmtimeだと RUST_LOG=wasi_common=trace
でWASIの呼び出しに関するログ?が見れることがわかった:
$ RUST_LOG=wasi_common=trace wasmtime --dir=. sample1.wasm TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > wiggle abi; module="wasi_snapshot_preview1" function="environ_sizes_get" TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > result=Ok((0, 0)) TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > wiggle abi; module="wasi_snapshot_preview1" function="fd_prestat_get" TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > fd=Fd(3) TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > result=Ok(Dir(PrestatDir { pr_name_len: 1 })) TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > wiggle abi; module="wasi_snapshot_preview1" function="fd_prestat_dir_name" TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > fd=Fd(3) path=*guest 0x102fe0 path_len=1 TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > result=Ok(()) TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > wiggle abi; module="wasi_snapshot_preview1" function="fd_prestat_get" TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > fd=Fd(4) TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > result=Err(Badf: Bad file descriptor) DEBUG wasi_common::snapshots::preview_1 > Error: Badf: Bad file descriptor TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > wiggle abi; module="wasi_snapshot_preview1" function="path_open" TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > fd=Fd(3) dirflags=SYMLINK_FOLLOW path=*guest 0x103290/9 oflags=(empty) fs_rights_base=FD_READ | FD_SEEK | FD_FDSTAT_SET_FLAGS | FD_SYNC | FD_TELL | FD_ADVISE | PATH_CREATE_DIRECTORY | PATH_CREATE_FILE | PATH_LINK_SOURCE | PATH_LINK_TARGET | PATH_OPEN | FD_READDIR | PATH_READLINK | PATH_RENAME_SOURCE | PATH_RENAME_TARGET | PATH_FILESTAT_GET | FD_FILESTAT_GET | FD_FILESTAT_SET_TIMES | PATH_SYMLINK | PATH_REMOVE_DIRECTORY | PATH_UNLINK_FILE | POLL_FD_READWRITE fs_rights_inheriting=FD_READ | FD_SEEK | FD_FDSTAT_SET_FLAGS | FD_SYNC | FD_TELL | FD_ADVISE | PATH_CREATE_DIRECTORY | PATH_CREATE_FILE | PATH_LINK_SOURCE | PATH_LINK_TARGET | PATH_OPEN | FD_READDIR | PATH_READLINK | PATH_RENAME_SOURCE | PATH_RENAME_TARGET | PATH_FILESTAT_GET | FD_FILESTAT_GET | FD_FILESTAT_SET_TIMES | PATH_SYMLINK | PATH_REMOVE_DIRECTORY | PATH_UNLINK_FILE | POLL_FD_READWRITE fdflags=(empty) TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > result=Ok(Fd(4)) TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > wiggle abi; module="wasi_snapshot_preview1" function="fd_filestat_get" TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > fd=Fd(4) TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > result=Ok(Filestat { dev: 16777234, ino: 27948073, filetype: RegularFile, nlink: 1, size: 668, atim: 1665472872893286124, mtim: 1665470700671229914, ctim: 1665470493204932262 }) TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > wiggle abi; module="wasi_snapshot_preview1" function="fd_seek" TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > fd=Fd(4) offset=0 whence=Cur TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > result=Ok(0) TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > wiggle abi; module="wasi_snapshot_preview1" function="fd_read" TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > fd=Fd(4) iovs=*guest 0xffe60/1 TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > result=Ok(668) TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > wiggle abi; module="wasi_snapshot_preview1" function="fd_read" TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > fd=Fd(4) iovs=*guest 0xffe58/1 TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > result=Ok(0) TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > wiggle abi; module="wasi_snapshot_preview1" function="fd_close" TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > fd=Fd(4) TRACE wasi_common::snapshots::preview_1::wasi_snapshot_preview1 > result=Ok(())
|
fd_prestat_get
を使ってプリセットとして与えられたファイルデスクリプタを調べてる?
path_open
の fd
にプリセットであろう3
を渡している
dirflags
やfs_rights
はちゃんとフラグを渡す
てな具合で動作するっぽい。
POSIXとの繋ぎ込み
上記によって解読した、POSIXのopen
からWASIのpath_open
への繋ぎ込みは以下の感じ(ひとまず読み込みオープンのみ):
#include "fcntl.h" #include "stdint.h" #include "unistd.h"
extern int path_open(int fd, int dirflags, const char *path, int path_len, int oflags, uint64_t fs_rights_base, uint64_t fs_rights_inherting, uint16_t fdflags, uint32_t *opend_fd);
int max_preopen_fd = 3;
int open(const char *fn, int flag, ...) { const int LOOKUPFLAGS_SYMLINK_FOLLOW = 0x01; const int FDFLAGS_RSYNC = 0x08; const uint64_t FD_DATASYNC = 0x01; const uint64_t FD_READ = 0x02; const uint64_t FD_SEEK = 0x04; const uint64_t FD_FDSTAT_SET_FLAGS = 0x08; const uint64_t FD_SYNC = 0x10; const uint64_t FD_TELL = 0x20; const uint64_t FD_ADVISE = 0x80; const uint64_t PATH_CREATE_DIRECTORY = 0x200; const uint64_t PATH_CREATE_FILE = 0x400; const uint64_t PATH_LINK_SOURCE = 0x800; const uint64_t PATH_LINK_TARGET = 0x1000; const uint64_t PATH_OPEN = 0x2000; const uint64_t FD_READDIR = 0x4000; const uint64_t PATH_READLINK = 0x8000; const uint64_t PATH_RENAME_SOURCE = 0x10000; const uint64_t PATH_RENAME_TARGET = 0x20000; const uint64_t PATH_FILESTAT_GET = 0x40000; const uint64_t FD_FILESTAT_GET = 0x200000; const uint64_t FD_FILESTAT_SET_TIMES = 0x800000; const uint64_t PATH_SYMLINK = 0x1000000; const uint64_t PATH_REMOVE_DIRECTORY = 0x2000000; const uint64_t PATH_UNLINK_FILE = 0x4000000; const uint64_t POLL_FD_READWRITE = 0x8000000;
int dirflags = LOOKUPFLAGS_SYMLINK_FOLLOW; int oflags = 0; uint64_t fs_rights_base = FD_READ | FD_SEEK | FD_FDSTAT_SET_FLAGS | FD_SYNC | FD_TELL | FD_ADVISE | PATH_CREATE_DIRECTORY | PATH_CREATE_FILE | PATH_LINK_SOURCE | PATH_LINK_TARGET | PATH_OPEN | FD_READDIR | PATH_READLINK | PATH_RENAME_SOURCE | PATH_RENAME_TARGET | PATH_FILESTAT_GET | FD_FILESTAT_GET | FD_FILESTAT_SET_TIMES | PATH_SYMLINK | PATH_REMOVE_DIRECTORY | PATH_UNLINK_FILE | POLL_FD_READWRITE; uint64_t fs_rights_inheriting = fs_rights_base; uint32_t fdflags = 0;
for (int base_fd = max_preopen_fd - 1; ;) { uint32_t opened = -1; uint32_t result = path_open(base_fd, dirflags, fn, strlen(fn), oflags, fs_rights_base, fs_rights_inheriting, fdflags, &opened); if (result == 0) return opened; if (--base_fd < 3) break; } return -1; }
|
プリオープンからの相対で探すために、自前のスタートアップルーチン内で max_preopen_fd
は探しておく:
typedef uint8_t Preopentype;
typedef struct { size_t pr_name_len; } PrestatDir;
typedef struct { PrestatDir dir; } PrestatU;
typedef struct { Preopentype pr_type; PrestatU u; } Prestat;
extern int fd_prestat_get(int fd, Prestat *prestat);
static int find_preopens(void) { for (int fd = 3; ; ++fd) { Prestat prestat; int result = fd_prestat_get(fd, &prestat); if (result != 0) return fd; } }
void _start() {
max_preopen_fd = find_preopens();
}
|
でようやくファイルオープンが成功した。
node.js上で動かす(Wasmer JS)(未完)
コンパイルした.wasmバイナリをブラウザ上やnode.jsで動かすためにインポートするWASI関数を自分で書くという手段もあり得るが、手っ取り早くWasmer JSを使うという手もある:
'use strict'
const fs = require('fs') const { init, WASI } = require('@wasmer/wasi')
;(async () => { await init()
if (process.argv.length < 3) { console.error('argv < 3') process.exit(1) }
const wasmFileName = process.argv[2] const wasi = new WASI({ args: process.argv.slice(2), env: process.env, })
try { const wasmBin = fs.readFileSync(wasmFileName) const module = await WebAssembly.compile(wasmBin) await wasi.instantiate(module, {}) const exitCode = wasi.start() process.stdout.write(wasi.getStdoutString()) process.exit(exitCode) } catch (e) { console.error(e) } })()
|
たぶん同様にプリオープンとして読み込めるディレクトリを設定する必要があると思うが与え方がわからず…
new WASI()
に与えるオプションにpreopens
を指定するとエラーが出る:
Error: Failed to create the WasiState: wasi filesystem creation error: `Could not get metadata for file ".": fd not a directory``
|
Hello World - Wasmer Docs を見ると、
bindings
内の fs
にnode.jsのfs
を与えるっぽいんだけど、require('@wasmer/wasi/lib/bindings/node')
できなくて確認できず…。
追記・上記の続き:【WASM】WASIランタイム(JS用)が混沌としている
いいたいこと
- WASIは仕様ということならちゃんとバイナリレベルでドキュメント化して欲しい
- ソースを見たり推測しないで済むようにして欲しい
ドキュメント