目次

はじめに
本書の構成
サンプルコードについて
表記関係について
第1章 前提知識
1.1 コマンドツール
1.2 WebAssembly
1.3 セキュリティー(安全性)
1.4 ポータビリティー(移植性・柔軟性)
1.5 WebAssemblyが注目されている理由
1.6 ウェブ以外の環境で実行するために
1.7 WASI(WebAssembly System Interface)
1.8 まとめ
第2章 WASI 0.1
2.1 WASI 0.1(プレビュー1)
2.2 モジュールの種類
2.3 プロセスを終了する
2.4 標準入力と標準出力
2.5 環境変数と引数
2.6 現在時刻(UNIXTIME)を表示する
2.7 乱数を表示する
2.8 ファイルの内容を表示する
2.9 ソケット通信
2.10 WASI 0.1の課題
2.11 まとめ
第3章 コンポーネントモデル
3.1 コンポーネントモデル
3.2 コンポーネントモデルの生まれた背景
3.3 WIT
3.4 example:helloパッケージの実装
3.5 example:componentパッケージの実装
3.6 まとめ
第4章 WASI 0.2
4.1 WASI 0.2(プレビュー2)
4.2 wasi:cliパッケージ
4.3 wasi:ioパッケージ
4.4 wasi:clocksパッケージ
4.5 wasi:randomパッケージ
4.6 wasi:filesystemパッケージ
4.7 wasi:socketsパッケージ
4.8 wasi:httpパッケージ
4.9 WASI 0.2の課題
4.10 まとめ
第5章 今後の展望
5.1 Warg
5.2 WAC
5.3 WIT定義を公開する
5.4 JCO
5.5 まとめ
付録A APPENDIX
A.1 参考文献

はじめに

 WASIはWasm(WebAssembly)のコミュニティーグループによって開発されている、Wasmのシステムインターフェースの仕様です。2023年8月にリリースされたGo 1.21からWASI 0.1へのビルドがサポート1されたり、翌年の2024年1月にWASI 0.2がリリース2されたりと、近年ますます注目されている技術です。

 WASIは、Wasmの標準的なシステムインターフェースを提供することを目的としています。本書がWASIの仕様を理解する手助けとなれば幸いです。

本書の構成

 本書は5つの章から構成されています。興味のある章から読み進めても問題ありません。以下、各章の章の概要を紹介します。

第1章 前提知識

WASIを解説するための前提知識を紹介します。Wasmについての簡単な解説と、WASIが生まれるに至った背景について解説します。また、本書で使用しているコマンドツールも第1章で紹介します。

第2章 WASI 0.1

WASI 0.1(プレビュー1)の基本的な仕様の解説と、WASI 0.1のインターフェースを用いた実装例を紹介します。2024年の1月にWASI 0.2がリリースされましたが、WASI 0.2が普及するにはもう少し時間がかかると思われます。実務でWASI 0.1を使用しているのであれば、本章が理解の助けになるでしょう。

第3章 コンポーネントモデル

WASI 0.2(プレビュー2)の仕様のベースとなるコンポーネントモデルについて解説します。コンポーネントモデルを採用したことによって、WASI 0.2はWASI 0.1とは全く異なるインターフェース仕様となりました。WASI 0.1はWasmの基本的な仕様の上に構築されていますが、WASI 0.2の仕様はコンポーネントモデルの仕様の上に構築されています。そのため、コンポーネントモデルのみを解説する章を設けています。第4章を読む前に、本章から読み進めることをお勧めします。

第4章 WASI 0.2

WASIの最新仕様であるWASI 0.2(プレビュー2)の基本的な仕様の解説と、WASI 0.2のインターフェースを用いた実装例を紹介します。WASI 0.1の実装例(第2章)と同様の実装例を紹介しているため、インターフェースがどのように変化したのか比較したい場合は第2章とあわせて読むことをお勧めします。

第5章 今後の展望

現在開発されているWasmのエコシステムについて紹介します。WasmのレジストリーであるWarg、Wasmコンポーネントを組み合わせるためのツールであるWACなどのエコシステム周りの開発が進んでいます。Wargは現在パブリックベータ版が公開されているため、それらの使用方法についても紹介します。

サンプルコードについて

 特に断りがない場合、本書で紹介しているコマンドおよびサンプルコードは全てLinuxもしくはUNIX(macOS)を前提に記述しています。また、サンプルコードはすべてGitHub上のリポジトリー3に公開しています。記載しているコードの断片から実装の全体像を読み取れない場合などは、ライセンスの範囲で自由にお使いください。

表記関係について

 本書に記載されている会社名、製品名などは、一般に各社の登録商標または商標、商品名です。会社名、製品名については、本文中では©、®、™マークなどは表示していません。

第1章 前提知識

1.1 コマンドツール

 本書で使用しているコマンドツールを簡単に紹介します。

Deno

 Deno1は、TypeScriptとJavaScriptのランタイムです。公式サイトに記載されている下記のコマンドを実行することで、インストールすることができます。

$ curl -fsSL https://deno.land/install.sh | sh

...

Wasmtime

 Wasmtime2は、WASI 0.1とWASI 0.2の両方をサポートしているWasmのランタイムです。GitHubのREADMEに記載されている下記のコマンドを実行することで、インストールすることができます。

$ curl https://wasmtime.dev/install.sh -sSf | bash

...

Rust

 Rust3は、システムプログラミング言語です。公式サイトに記載されている下記のコマンドを実行することで、インストールすることができます。

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

...

 RustはWasmをビルドするためのエコシステムが整っているため、本書ではWasmの実装例にRustを使用しています。RustからWasmにビルドする場合、--targetオプションを用いてビルドターゲット(wasm32-unknown-unknownもしくはwasm32-wasip1)を指定します。これらのビルドターゲットがビルド環境のコンピューターに入っていない場合、次のコマンドでビルドターゲットを追加することができます。

$ rustup target add wasm32-unknown-unknown

...


$ rustup target add wasm32-wasip1

...

wasm-tools

 wasm-tools4はWasmのバイナリーフォーマットとテキストフォーマットの相互変換や、Wasmの解析に用いるコマンドです。Rustのパッケージマネージャーであるcargoコマンドからインストールすることができます。

$ cargo install wasm-tools

...

cargo-component

 cargo-component5は、WASI 0.2やWasmコンポーネントをビルドするのに使用するコマンドです。Rustのパッケージマネージャーであるcargoコマンドのサブコマンドとして提供されており、cargoコマンドからインストールすることができます。

$ cargo install cargo-component --locked

1.2 WebAssembly

 Wasm (WebAssembly)は、ウェブブラウザー上で高速にプログラムを実行することを目的に開発された技術です。JavaScriptが人にとって理解しやすいテキストフォーマットであるのに対して、Wasmはコンピューターにとって理解しやすいバイナリーフォーマットとして設計されています。そのため、Wasmはコンピューター上で高速にプログラムを実行することができます。

WAT

 バイナリーフォーマットがコンピューターにとって理解しやすい一方で、バイナリーフォーマットは人が読み書きするのに適していません。そのため、Wasmにはバイナリーフォーマットと相互変換可能なテキストフォーマットであるWAT(WebAssembly Text Format)6が用意されています。WATはS式と呼ばれる構文を採用しており、次のように書くことができます。

リスト1.1: WATのコード例(example.wat)

(module
  (func $add (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.add
  )
  (export "add" (func $add))
)

WATを用いることでバイナリーフォーマットを扱いやすくはなりますが、それでもJavaScriptなどと比べると、まだまだ扱いにくい言語であることに変わりはありません。そのため、通常はRustなどの言語を用いてプログラムを書き、Wasmにコンパイルします。

リスト1.2: Rustのコード例(example/src/lib.rs)

#[no_mangle]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

実行する

 Wasmは1ファイル1モジュールの構成になっており、モジュール単位でインスタンス化して実行します。Wasmモジュールを実行するには、モジュールファイルを読み込み、モジュールからインスタンスを作成する必要があります。ウェブ上でWasmを実行する場合、次のようなJavaScriptのコードを書く必要があります。

リスト1.3: JavaScriptのコード例(example.js)

// ファイル名
const filename = new URL("example.wasm", import.meta.url);

// バイナリーを取得
const binary = await fetch(filename).then((res) => res.arrayBuffer());

// モジュールを作成
const module = new WebAssembly.Module(binary);

// インスタンスを作成
const instance = new WebAssembly.Instance(module);

// 実行
console.log(instance.exports.add(1, 2)); // 3

 このJavaScriptのコードをHTMLファイルに埋め込むか、Denoなどのランタイムを用いることで実行することができます。 

$ deno run --allow-read example.js

3

1.3 セキュリティー(安全性)

 Wasmにはウェブ上で高速にプログラムを実行できるという特徴以外にも、セキュリティー(安全性)とポータビリティー(移植性・柔軟性)にも注目すべき特徴があります。JavaScriptのコードを例に、Wasmのセキュリティーについて解説します。

JavaScriptから「Hello, world!」を出力する例

 JavaScriptを用いて「Hello, world!」を標準出力(/dev/stdout)に出力するプログラムを実行する場合、次のようなコードを書きます。

リスト1.4: JavaScriptのコード例(hello.js)

console.log("Hello, world!");

$ deno run hello.js

Hello, world!

 このプログラムを実行すると、ターミナル上に「Hello, world!」と表示されます。この出力先である標準出力は、OSが管理しているリソースです。OSは管理しているリソースにアプリケーション(OS上で実行するプログラム)がアクセスできるように、インターフェース(API)を提供しています。このOSが提供しているインターフェースのことを、システムインターフェースと呼びます。

図1.1: 標準出力「Hello, world!」を出力する例

 JavaScriptのランタイムは、console.logが実行されるとOSのシステムコール7を行い、標準出力に内容を出力します。

WebAssemblyから「Hello, world!」を出力する例

 一方でWasmはサンドボックスとして設計されているため、OSのシステムインターフェースを直接実行することができません。そのため、Wasm単体では「Hello, world!」を標準出力に出力することができないようになっています。

 Wasmから「Hello, world!」を出力するには、Wasmの外側の世界(JavaScriptのランタイム)に代わりに「Hello, world!」を出力してもらう必要があります。

 Rustを用いて、「Hello, world!」を出力するWasmをビルドする場合、次のようなコードを書く8必要があります。

リスト1.5: Rustのコード例(hello/src/lib.rs)

// 実行時にインポートするモジュールの定義
#[link(wasm_import_module = "env")]
extern "C" {
    fn write(ptr: *const u8, len: usize);
}

#[no_mangle]
pub fn _start() {
    let msg = b"Hello, world!";
    unsafe {
        write(msg.as_ptr(), msg.len());
    }
}

このコードからビルドしたWasmを実行するには、次のようなJavaScriptのコードを書く必要があります。

リスト1.6: JavaScriptのコード例(hello/hello.js)

const filename = new URL(
  "target/wasm32-unknown-unknown/release/hello.wasm",
  import.meta.url,
);

// WebAssemblyの代わりに標準出力に出力する関数
function write(ptr, len) {
  console.log(new TextDecoder().decode(
    new Uint8Array(memory.buffer, ptr, len),
  ));
}

const { instance } = await WebAssembly.instantiateStreaming(
  fetch(filename),
  // モジュールのインポート
  { env: { write } },
);

const memory = instance.exports.memory;
instance.exports._start();

ビルドして実行すると、「Hello, world!」がターミナル上に表示されます。

$ cargo build --release --target wasm32-unknown-unknown

   Compiling hello v0.1.0 (/wasi-book-example/第1章/hello)

    Finished `release` profile [optimized] target(s) in 0.20s


$ deno run --allow-read hello.js

Hello, world!

 このようにWasmは直接OSのリソースにアクセスすることができないため、Rustのコード上でWasmのインスタンス作成時にインポートするenvモジュールを定義し、envモジュールのwrite関数を呼び出しています。

 JavaScriptのコード上では、ポインターと文字列長を受け取り、Wasmのメモリー上から文字列を読み込んでconsole.logを呼び出すwrite関数を実装しています。このwrite関数をインスタンス作成時にインポートするenvモジュールの関数として、Wasmに渡しています。

図1.2: WebAssemblyから標準出力へ出力する例

 このようにすることで、Wasmは「Hello, world!」を標準出力に出力することができます。

WebAssemblyの出力を制限する(1)

 インスタンス作成時にwrite関数をインポートしない場合はどうなるでしょうか?

リスト1.7: write関数をインポートしない例(hello/hello.js)

const filename = new URL(
  "target/wasm32-unknown-unknown/release/hello.wasm",
  import.meta.url,
);

const { instance } = await WebAssembly.instantiateStreaming(
  fetch(filename),
  { env: {} },
);

instance.exports._start();

$ deno run --allow-read hello.js

error: Uncaught (in promise) LinkError: WebAssembly.instantiate(): Import #0 "env" "write": function import requires a callable

...

このようにwrite関数をインポートしないようコードを書き換えて実行すると、エラーとなりWasmを実行することができません。

 Wasmでは、実行するランタイム側がアクセスさせたくない機能をインポートしないようにすることで、Wasmの実行を制限することができます。

WebAssemblyの出力を制限する(2)

 write関数をインポートしないようにするとWasm自体を実行できなくなるため、代わりに何もしないwrite関数をインポートすることで機能を制限することもできます。

リスト1.8: write関数を空にする例(hello/hello.js)

const filename = new URL(
  "target/wasm32-unknown-unknown/release/hello.wasm",
  import.meta.url,
);

function write(_ptr, _len) {
  // DO NOTHING
}

const { instance } = await WebAssembly.instantiateStreaming(
  fetch(filename),
  { env: { write } },
);

instance.exports._start();

$ deno run --allow-read hello.js

 このようにWasmにはOSのリソースに直接アクセスする方法が存在しないため、Wasmを実行するランタイム側がWasmの挙動を簡単に制御することができます。

1.4 ポータビリティー(移植性・柔軟性)

 WasmのバイナリーフォーマットはIntelやARMなどのCPUアーキテクチャーや、LinuxやWindowsなどのOSに依存していません。そのため、ランタイムさえあればCPUやOSを問わず、同じバイナリーフォーマットのWasmを実行することができます。

図1.3: CPUやOSに依存しないバイナリーフォーマット

ウェブ上で実行する

 ここまでDenoを用いてWasmを実行する例を紹介しましたが、次のようなHTMLファイルを用意することで、ウェブ上でも同じWasmを実行することができます。

リスト1.9: HTMLのコード例(hello/hello.html)

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Example</title>
    <script type="module" src="hello.js"></script>
  </head>
  <body>
    <p>Open DevTools to see the console output.</p>
  </body>
</html>

$ python3 -m http.server

Serving HTTP on :: port 8000 (http://[::]:8000/) ...

...


図1.4: ウェブ上でWebAssemblyを実行する例

1.5 WebAssemblyが注目されている理由

 ランタイムさえあればどこでも実行できるというのは、Wasmだけの特徴ではありません。Javaも同様に、ランタイムであるJVM(Java Virtual Machine)があれば、OSやCPUアーキテクチャーに依存することなくプログラムを実行することができます。

 近年Wasmが注目されているのはOSやCPUに依存しないバイナリーフォーマットであることだけでなく、軽量で高速かつ安全に実行できるという特徴が揃っているからです。元々はウェブ上で高速にプログラムを実行することを目的に開発された技術でしたが、この特徴を活かすことでエッジコンピューティングのような限られたリソース上でプログラムを実行したり、Linuxコンテナの置き換えといった用途での活用が期待されている技術です。

1.6 ウェブ以外の環境で実行するために

 Wasmから「Hello, world!」を出力するためには、Wasmの代わりに「Hello, world!」を出力するためのモジュールをインポートする必要がありました。

 本書ではenvモジュールのwrite関数をインポートする実装例を紹介しましたが、別の人はconsoleモジュールのlog関数をインポートするWasmを実装するかもしれません。また、同じwrite関数でも、引数や戻り値が異なるかもしれません。

 Wasmは、OSが管理しているリソースへのアクセス方法を定義していません。そのため、実装者によって必要なインポートモジュールやエクスポートする関数が異なる可能性があります。Wasmの実行者は、インポートしないといけないモジュールは何か、エクスポートされている関数は何かを知らないと、Wasmを実行することができません。

 Rustの他にも、Wasmへのビルドをサポートしている言語にGoやDartなどがあります。これらの言語からWasmをビルドし、実行する方法を見てみましょう。

GoからビルドされたWebAssemblyを実行する例

 次のようなGoのコードを用意します。

リスト1.10: Goのコード例(hello-go/hello.go)

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello, world!")
}

GoのコードをWasmにビルドするには、次のコマンドを実行します。

$ env GOARCH=wasm GOOS=js go build -o hello.wasm hello.go

ビルドされたWasmを実行するには、このWasmのインターフェース(インポート/エクスポート)を知る必要があります。GoはWasmを実行するためのJavaScriptのグルーコードを提供しています。次のコマンドを実行し、Goのグルーコードをコピーしましょう。

$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .

最後に、次のようなJavaScriptのコードを書くことで実行することができます。

リスト1.11: JavaScriptのコード例(hello-go/hello.js)

import "./wasm_exec.js";

const go = new Go();

const filename = new URL("./hello.wasm", import.meta.url);

const { instance } = await WebAssembly.instantiateStreaming(
  fetch(filename),
  go.importObject,
);

go.run(instance);

$ deno run --allow-read hello.js

Hello, world!

DartからビルドされたWebAssemblyを実行する例

 次のようなDartのコードを用意します。

リスト1.12: Dartのコード例(hello-dart/hello.dart)

void main() {
  print('Hello, world!');
}

DartのコードをWasmにビルドするには、次のコマンドを実行します。

$ dart compile wasm -o hello.wasm hello.dart

Generated wasm module 'hello.wasm', and JS init file 'hello.mjs'.

 ビルドすると、hello.wasmと一緒にJavaScriptのグルーコード(hello.mjs)も出力されます。Dartの場合は次のような実行用のJavaScriptのコードを書くことで、Wasmを実行することができます。

リスト1.13: JavaScriptのコード例(hello-dart/hello.js)

import { instantiate, invoke } from "./hello.mjs";

const filename = new URL("./hello.wasm", import.meta.url);

const instance = await instantiate(
  WebAssembly.compileStreaming(fetch(filename)),
);

invoke(instance);

$ deno run --allow-read ./hello.js

Hello, world!

GoとDartのWebAssemblyの違い

 GoとDartそれぞれからビルドしたWasmはどちらも同じ「Hello, world!」を出力するプログラムですが、提供されているインターフェースが異なるため、実行するのに必要なJavaScriptのコードも異なります。

 それぞれのWasmがどのようなモジュールをインポートしようとしているのかを確認すると、次のようになります。

$ wasm-tools print hello.wasm | grep '\(import '

  (import "dart2wasm" "_83" ...

  (import "dart2wasm" "_84" ...

  (import "dart2wasm" "_194" ...

...


$ wasm-tools print hello.wasm | grep '\(import '

  (import "gojs" "runtime.scheduleTimeoutEvent" ...

  (import "gojs" "runtime.clearTimeoutEvent" ...

  (import "gojs" "runtime.resetMemoryDataView" ...

...

DartからビルドされたWasmがdart2wasmモジュールをインポートする必要があるのに対して、GoからビルドされたWasmはgojsモジュールをインポートする必要あり、完全に異なるインターフェース仕様であることがわかります。

インターフェースの標準化

 このようにビルドする環境によって必要なインターフェースが異なると、Wasmを実行するために必要なインターフェースを都度調べる必要があります。Goの例では、公式が用意しているグルーコードを使うことで実行できますが、Dartの場合はビルド時に一緒に生成されるグルーコードを使う必要があります。

 Wasmは優れたポータビリティー性を持っていますが、標準化されたインターフェースが仕様に存在しないために、ビルド済みのWasmだけを他者に渡しても簡単に実行することができないという課題があります。この課題を解決するために、Wasmの標準化されたシステムインターフェースの仕様を策定しようという動きがあります。その標準化を目指しているWasmのシステムインターフェース仕様が、WASI(WebAssembly System Interface)です。

1.7 WASI(WebAssembly System Interface)

 WASIを用いて、「Hello, world!」を出力するWasmを作成してみましょう。

GoからビルドされたWebAssemblyを実行する

 先ほどのGoのコードをWASI 0.1にビルドして実行すると、次のようになります。

リスト1.14: Goのコード例(hello-go/hello.go)

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello, world!")
}

$ env GOARCH=wasm GOOS=wasip1 go build -o hello.wasm hello.go


$ wasmtime run hello.wasm

Hello, world!

 このように、GoからビルドされたWasmをWasmtimeを使用して、簡単に実行することができます。

RustからビルドされたWebAssemblyを実行する

 Dartは現状WASIをサポートしていないため、代わりにWASI 0.1をサポートしているRustを使用してWASI 0.1にビルドしてみましょう。

$ cargo new hello

...


リスト1.15: Rustのコード例(hello/src/main.rs)

fn main() {
    println!("Hello, world!");
}

RustからWASI 0.1に対応したWasmをビルドするには、次のコマンドを実行します。

$ cargo build --release --target wasm32-wasip1

   Compiling hello v0.1.0 (/wasi-book-example/第1章/wasi/hello)

    Finished `release` profile [optimized] target(s) in 0.29s

RustからビルドされたWasmを実行するには、Goと同様にWasmtimeを使用します。

$ wasmtime run target/wasm32-wasip1/release/hello.wasm

Hello, World!

GoとRustのWebAssemblyの違い

 このようにGoとRustそれぞれからビルドしたWasmに必要なインターフェースが何かを意識することなく、Wasmtimeを使用するだけで簡単に実行することができました。これはGoとRustがWASI 0.1の仕様を満たしたWasmをビルドし、WasmtimeがWASI 0.1をサポートしているためです。

 それぞれのインターフェースの違いは、次のようになります。

$ wasm-tools print  target/wasm32-wasip1/release/hello.wasm | grep '\(import '

  (import "wasi_snapshot_preview1" "fd_write" ...

  (import "wasi_snapshot_preview1" "environ_get" ...

  (import "wasi_snapshot_preview1" "environ_sizes_get" ...

  (import "wasi_snapshot_preview1" "proc_exit" ...


$ wasm-tools print hello.wasm | grep '\(import '

  (import "wasi_snapshot_preview1" "sched_yield" ...

  (import "wasi_snapshot_preview1" "proc_exit" ...

  (import "wasi_snapshot_preview1" "args_get" ...

...

 インポートしている関数の数が異なるものの、どちらもwasi_snapshot_preview1モジュールをインポートしていることがわかります。このwasi_snapshot_preview1モジュールがWASI 0.1のモジュールです。

 このように共通のインターフェース仕様であるWASI 0.1を用いることで、どの言語からビルドされたWasmかを意識することなく実行することができるようになります。

1.8 まとめ

 第1章ではWASIの仕様を解説するための前提知識として、Wasmとはどういうものでどんな特徴があるのかを紹介しました。また、WASIがなぜ必要とされているのか、WASIを使うとどういったメリットがあるのかを紹介しました。次章からは、WASIの仕様について解説していきます。

試し読みはここまでです。
この続きは、製品版でお楽しみください。