ベアメタルWASIなバイナリを作って動かしてみる

## 前置き

現在だとWASIでプログラムを書くときは[wasi-libc](https://github.com/WebAssembly/wasi-libc)を[ここからダウンロード](https://github.com/WebAssembly/wasi-sdk/releases)してきて静的リンクして使うことが一般的と思われます。

ただまあ基本ホスト環境にないものなので(そのうち各distroでパッケージ用意してくれるようになるかもしれませんが)、毎回毎回ダウンロードしてくるのもだるいなあという気がします。
もちろんlibcの上にあるからこそ移植が簡単になっているとかその上でプログラムが書きやすいとかあったりするので結論からいうとlibcを使わないことによるメリットはほとんどない(サイズを小さくできるかもしれないくらい？)ですが、ちょろっとwasmtime/wasmer試すのにもう少し手軽にならないかな？ということでベアメタル路線(これをベアメタルという言い方をしていいのか疑問がありますが)を試してみます。

libcを経由せずにホスト環境のAPIを触る方法を考えてみましょう。
もちろんlibcはそういうふうに中でやっているわけなので、コードをみてみると[__wasilibc_real.c](https://github.com/WebAssembly/wasi-libc/blob/2b7e73ae7ac0bad6391d89cc3274a28412243389/libc-bottom-half/sources/__wasilibc_real.c)というファイルがあります。
これをみると`wasm-import-module`と`wasm-import-name`を与えてやればホスト環境にアクセスできそうです。wasmtimeやwasmerはこれらのAPIを露出させてやることでwasmバイナリからでも標準入出力を扱えるようになっているのでしょう。
[wasmtimeの実装](https://github.com/bytecodealliance/wasmtime/blob/6db24fd08fa6f675e1b4ef818f8684602fd58730/crates/wasi-common/src/snapshots/wasi_snapshot_preview1.rs)を見ると、たとえば`fd_write`はゲストから受け取ったメモリに対してランタイム側で書き込み権限があるかどうかをチェックして実際にホスト環境への書き込みが行われる、というような感じになっているようです。

`wasm-import-module`について少しみてみましょう。`wasi_snaphot_preview1`という値が指定されていますが、これは現在WASIのAPIはまだpreview段階であることを意味します。ただし[snapshotの定義](https://github.com/WebAssembly/WASI/tree/main/phases)をみるにここで破壊的変更が行われる心配はしなくても大丈夫そうです(正式な仕様策定が実装されたらpreview版がごっそり消されてしまう、などはありそうですが)。

というわけで、`wasm-import-module`および`wasm-import-name`を言語機能として呼び出せるような言語であればベアメタルなWASI環境で動くwasmバイナリが作れそうなことがわかりました。ここではD言語(LDC)を使ってみます。

## 環境

```console
$ ldc2 --version| head -2
LDC - the LLVM D compiler (1.26.0):
  based on DMD v2.096.1 and LLVM 11.0.1
$ wasmtime --version
wasmtime 0.28.0
```

## 以下実装

LDCでは`@llvmAttr`を使ってAttributesを指定できます。
というわけで実装してみたのですが、やはりというべきかlibcを使わない場合と比較してだいぶ冗長になってしまいました。

```d
/**
 based on https://github.com/WebAssembly/wasi-libc/blob/2b7e73ae7ac0bad6391d89cc3274a28412243389/libc-bottom-half/sources/__wasilibc_real.c
 */
import ldc.attributes;

extern (C):
nothrow:
@nogc:
@system:

alias __wasi_fd_t = int;
alias __wasi_size_t = size_t;
alias __wasi_errno_t = ushort;
alias __wasi_exitcode_t = uint;

struct __wasi_ciovec_t
{
    const(ubyte)* buf;
    __wasi_size_t buf_len;
}
static assert(__wasi_ciovec_t.sizeof == 8);
static assert(__wasi_ciovec_t.alignof == 4);
static assert(__wasi_ciovec_t.buf.offsetof == 0);
static assert(__wasi_ciovec_t.buf_len.offsetof == 4);

@llvmAttr("wasm-import-module", "wasi_snapshot_preview1")
@llvmAttr("wasm-import-name", "fd_write")
extern int __imported_wasi_snapshot_preview1_fd_write(int arg0, int arg1, int arg2, int arg3) @trusted;

__wasi_errno_t __wasi_fd_write(__wasi_fd_t fd, const(__wasi_ciovec_t)* iovs, size_t iovs_len, __wasi_size_t* retptr0) @trusted
{
    int ret = __imported_wasi_snapshot_preview1_fd_write(cast(int) fd, cast(int) iovs, cast(int) iovs_len, cast(int) retptr0);
    return cast(ushort) ret;
}

void _start() @trusted
{
    const(ubyte)* s = cast(const(ubyte)*) "Hello, World!\n".ptr;
    int fd = 1;
    __wasi_ciovec_t iovs = { s, 14 };
    __wasi_size_t retp;
    __wasi_fd_write(fd, &iovs, 1, &retp);
}
```

## ビルド・実行

WASI向けにビルドする必要があるので当然クロスコンパイルが必要になります。
D言語(LDC)であれば以下のように簡単にクロスコンパイルすることができます。

```console
$ ldc2 -mtriple=wasm32-unknown-wasi \
 -betterC \
 -fvisibility=hidden \
 -defaultlib= \
 -of=app.wasm \
 app.d
```

実行はwasmtimeを使っています。

```console
$ wasmtime app.wasm
Hello, World!
```

## 結論

というわけでわりと手軽にベアメタルなWASI対応バイナリは作れるんだな、というのがわかりました。
メリットはほとんどないですが、たとえば複数の`wasm-import-module`でAPIを使い分けたいような場合には便利になるかもしれません。