WASI上でのファイルオープンに悪戦苦闘した話

2022-10-12

自作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_writeIov ってなによ?複数渡したいケースってあるんか?) などいろいろ戸惑いながら。 コンパイルで出力した.wasmをWasmerWasmtimeで動かすことでWASI相当かどうかを確認できる。

さてじゃあファイルオープンをやってみるか、ではたと困った。 該当するAPIはつらつら見ていったところ path_open だと思うが、これを動かせるようになるまでにかなり苦労した。

ファイルオープン

path_openの定義を見てもわからないことだらけ。 path 以外の引数がどれもなにを与えるのかわからない。

  • 渡す fd が不明、「POSIXの openat と似せている」と書かれているのでググって、カレントディレクトリからの相対にしたい場合には AT_FDCWD (Linuxだと-100)を渡す?
  • フラグは不明なので適当に0を渡してみる
  • Rustの形式で書かれても、WASMにはstringResult<>もないからどうすんのよ?

結局stringは文字列へのポインタと長さ、戻り値のResult<>fdの格納先アドレスを渡して、WASM側からは戻り値がAPIの成功/失敗が取得できることはわかった。 しかしあれこれ引数をいじっても成功しなかった。

サンプルを解析

推測で動かしても埒があかなかったので、RustからWASI用にコンパイルして動くバイナリを作成して、それを解析することにした。

Rust で WASI 対応の WebAssembly を作成して実行 - なんとなくな Developer のメモ を参考に、Rustからwasmを出力できるようにする。 まずは環境セットアップ:

$ rustup target add wasm32-wasi  # Rustのコンパイルターゲットに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,
};

// Read the file contents into a string, returns `io::Result<usize>`
let mut s = String::new();
match file.read_to_string(&mut s) {
Err(why) => panic!("couldn't read: {}", why),
Ok(size) => (), //print!("size={}, content=\n{}", size, s),
}
}

上記をコンパイル:

$ rustc --target wasm32-wasi sample1.rs  # 対象ソースをコンパイル

して実行すると、WasmerやWasmtimeでもファイルは開けず実行時エラーが出る。 ググってくとWASIでは開けるファイルが制限されていて、コマンドラインオプションの --dirs=--mapdir=プリオープンとして許可する必要があるとのこと。 そんなことすら分かってなかったぜ。

でひとまず動くようになった。 このバイナリを解析しようとWabtの wasm-objdump-d オプションで逆アセンブルしてみたが、読むのはまあ辛い。 --section import --details でインポートしている関数が見れるが、字面で追えるのはその程度:

$ wasm-objdump -d sample1.wasm
#...(3万行ほどのwat形式)

$ 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 を使ってプリセットとして与えられたファイルデスクリプタを調べてる?
    • 3 で成功、 4 で失敗
  • path_openfd にプリセットであろう3を渡している
  • dirflagsfs_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;
// Rights
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;

// preopen から探す
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 {
/// The length of the directory name for use with `fd_prestat_dir_name`.
size_t pr_name_len;
} PrestatDir;

typedef struct {
PrestatDir dir;
} PrestatU;

typedef struct {
/// The type of the pre-opened capability.
Preopentype pr_type;
/// The contents of the information.
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();

// main呼び出しなど
}

でようやくファイルオープンが成功した。

node.js上で動かす(Wasmer JS)(未完)

コンパイルした.wasmバイナリをブラウザ上やnode.jsで動かすためにインポートするWASI関数を自分で書くという手段もあり得るが、手っ取り早くWasmer JSを使うという手もある:

// runwasi.js

'use strict'

const fs = require('fs')
const { init, WASI } = require('@wasmer/wasi')
// const nodeBindings = require('@wasmer/wasi/lib/bindings/node')

;(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,
// bindings: {
// // ...(nodeBindings.default || nodeBindings),
// fs: fs,
// },
// preopens: {
// '.': '.',
// },
})

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は仕様ということならちゃんとバイナリレベルでドキュメント化して欲しい
  • ソースを見たり推測しないで済むようにして欲しい

ドキュメント