要点
本記事の要点
- Web上でコンパイル済のプログラムを高速で走らせられるのがWebAssembly(Wasm)
- 今まではWasmを使おうとしたら、ほぼRust⇒JavaScript一択
- しかしWITを使うことで、Python含め様々なプラットフォームで使用できる!
- 各種ツールの特徴や関連性を解説
- 最後に、実際にWITを使ってプログラムを動かしてみる
対象読者
- WebAssemblyとはなんなのか?
- WebAssemblyは知っているけど、WITは知らない
- 実際に動かす手順を知りたい
WebAssemblyとは
そもそもAssembly言語とはなんだ? から整理します。
まずPC上に存在するプログラム関連の言語を、大きく3つに分けて考えましょう。
| 機械語 | 0と1のみで記述されるCPU命令 |
| Assembly言語 |
ADD MOV などの命令で人間がギリギリ読める低水準言語 |
| 高水準言語 | C、Rust、JavaScriptなど |
Assembly言語とは、コンパイラが機械語に翻訳するときに経由する中間形式のようなものです。
C言語コンパイルの中間ファイルとして作成されるAssamblyを読んで、ADDやMOVでポインタがどのように操作されているか確認する。そんな経験がある人ほとんどいないと思いますが、実は私は学生時代に読まざるを得なかったことがあります。その時の経験を踏まえると、アセンブリ言語はギリギリ読めます。
WebAssembly(Wasm)
次に WebAssembly(Wasm) ですが、こちらは「Web上で動くアセンブリ的なもの」というイメージです。
- 特徴
- 事前にコンパイルされたバイナリをブラウザ上で実行する
- JavaScriptがインタプリタ(JIT含む)方式なのに対し、コンパイル済みのマシンに近い命令を直接実行するため高速
- JavaScriptから呼び出せるので、重い計算処理をWebアプリに組み込める
実際に使われているサービスとして、複雑な演算をWasmに任せることによってサクサク動くようになった例もあります。
- Figma – ベクターグラフィック演算
- Adobe Photoshop – 画像処理
- Unity – ゲームエンジン
計算が重ければコンパイル済コードに任せればいい! という筋肉的な発想ですが、トレードオフとして技術的に難しいという面もあります。
生のWebAssemblyの問題点
初期のWebAssemblyには大きな問題がありました。
Wasm モジュールには
i32、i64、f32、f64の4種類のデータ型しか存在しません。また文字列やユーザー定義型のような構造を持つデータの表現にも標準が存在せず、データをどのようにメモリ上に配置するかはプログラミング言語の処理系、またはプログラマーが決めるものとされていました。
つまり、文字列・構造体・リストといった型をやり取りするためのルールがなく、言語間での相互運用が非常に難しい状態だったのです。
言語そのものがC++を触ったことがあるような超玄人向けだった時代もあったみたいですね。
wasm-bindgen:Rust ↔ JavaScript専用の橋渡し
この問題をRust ↔ TypeScript(JavaScript)間に絞って解決したのが wasm-bindgen です。
このツールの良い部分は以下の通り。
-
list、string、recordのような高レベルな型を渡せるようになった - ビルド時に
.d.tsファイルが自動生成され、TypeScriptの型安全を確保できる - Rust側からDOMを操作することも可能
もはや完璧ともいえる性能ですが、唯一「wasm-bindgenはRust ↔ JavaScript専用のツール」という点で柔軟性が欠ける点もありました。
「Pythonからも使いたい」「GoやCで書いたコードも組み合わせたい」となると、また別の仕組みが必要になったのです。
本題:WITとは何か
Wasm Component Model
wasm-bindgen が言語ペアを固定した解決策だったのに対し、あらゆる言語で作られ・あらゆる言語から呼び出せることを目指した仕様がWasm Component Model という考え方です。
本記事の主題であるWIT(WebAssembly Interface Type)は、このComponent Modelを実現するためのインターフェース定義言語(IDL)なわけです。
将来的にWasmで作ったロジックを様々な言語から使い回したい! といった需要が生まれる場合、wasm-bindgenではなくWITを選択するケースも増えてきています。
ただし、Rust⇒JavaScriptの密結合な仕様は持ち込めなかったので、wasm-bindgenで使えていた様々なオプションや、RustからのDOM操作などはできなくなっています。
関連ツール早見表
Wasm周りではいろいろなツールが散在しているので、メジャーなツールについてまとめておきます。
| 役割 | ツール | 対応言語 |
|---|---|---|
| 言語 → Wasm(ビルド) | cargo-component |
Rust |
| 言語 → Wasm(ビルド) |
componentize-py(実験段階) |
Python |
| WIT → バインディング生成 | wit-bindgen |
各種言語 |
| WIT → JS/TSバインディング生成+WASI対応 | jco |
JavaScript / TypeScript |
| Wasm実行ランタイム |
wasmtime / wasmtime-py
|
Rust / Python |
| Wasm実行 | ブラウザ | JavaScript |
それぞれのツールの関係を表した図をNanobananaで作成しました。この図があれば実際にWasmやWITを使う時のイメージ補完になるはずです。
まとめ
基本的な情報はここまでです。まとめると、
- WebAssemblyは、Web上で動く機械語に近い言語、コンパイル済で高速
- WITは、Wasm Component Modelを実現するためのインターフェース定義言語
- WITファイルに型と関数シグネチャを定義し、各言語のツール(
cargo-component、jcoなど)がバインディングを自動生成する - まだエコシステムは発展途上だが、マルチ言語でのWasm相互運用の標準として注目度が高まっている
といった感じです。
これ以降では、実際にWITを動かしてみます。
実際に作ってみた
Rustでコンポーネントを作り、JavaScript(Vite + TypeScript)から呼び出すデモを作成しました。さらに、同じ計算をTypeScriptネイティブとも比較して速度計測を行っています。
1. WITファイルを書く
まず wit/world.wit に、このコンポーネントが提供する型と関数を定義します。
package component:wasm-perf-wit;
world example {
// enum の定義
enum group-id {
himawari,
ajisai,
higanbana,
}
// record(構造体)の定義
record user {
name: string,
age: u32,
group: group-id,
income: option<u32>, // Optional な値も表現できる
}
// エクスポートする関数
export hello-world: func() -> string;
export list-data: func() -> list<u8>;
export get-user: func() -> user;
export fibonacci: func(n: u32) -> u32;
export heavy-memory-work: func(n: u32) -> u32;
}
ポイントは、このWITファイルさえあれば、どの言語のツールチェーンでも対応するバインディングを生成できることです。
2. Rust実装
cargo-componentがWITから自動生成したトレイト(Guest)を実装するだけです。
#[allow(warnings)]
mod bindings;
use bindings::{GroupId, User};
use bindings::Guest;
struct Component;
impl Guest for Component {
fn hello_world() -> String {
"Hello, World!".to_string()
}
fn list_data() -> Vec<u8> {
vec![1, 2, 3, 4, 5]
}
fn get_user() -> User {
User {
name: "Alice".to_string(),
age: 30,
group: GroupId::Himawari,
income: Some(800),
}
}
fn fibonacci(n: u32) -> u32 {
if n <= 1 { return n; }
Self::fibonacci(n - 1) + Self::fibonacci(n - 2)
}
fn heavy_memory_work(size: u32) -> u32 {
let mut arr = vec![0u32; size as usize];
for _ in 0..100 {
for i in 0..(size as usize) {
arr[i] = arr[i].wrapping_add((i % 256) as u32);
}
}
arr.into_iter().fold(0, |acc, x| acc.wrapping_add(x))
}
}
bindings::export!(Component with_types_in bindings);
WITに定義したgroup-id(ケバブケース)がRustではGroupId(パスカルケース)に、income: option<u32>がOption<u32>にマッピングされるなど、各言語の慣習に合った形へと変換してくれます。勝手に変換されるので混乱するケースもありますが……
3. Rust環境構築 & ビルド
# Rust のアップデート(必要に応じて)
rustup update stable
# cargo-component のインストール(少し時間がかかります)
cargo install cargo-component
# プロジェクト作成
cargo component new --lib wasm-perf-wit
cd wasm-perf-wit
# wasm へビルド
cargo component build --release
出力先: target/wasm32-wasip1/release/wasm_perf_wit.wasm
4. JavaScript / TypeScriptからの呼び出し
jcoを使って、.wasmからTypeScriptバインディングを自動生成します。
HTMLはAIに作ってもらいました。
cd javascript
npm init -y
npm install -D vite @bytecodealliance/jco
# WIT → TypeScript バインディング生成
npx jco transpile ../rust/wasm-perf-wit/target/wasm32-wasip1/release/wasm_perf_wit.wasm \
-o ./pkg --name wasm_perf
生成された./pkg/wasm_perf.js(と.d.ts)をTypeScriptからそのままインポートできます。
import { heavyMemoryWork, getUser, helloWorld, listData } from './pkg/wasm_perf';
// 文字列を返す
const msg = helloWorld(); // "Hello, World!"
// 構造体を返す(WIT の record が JS オブジェクトにマッピング)
const user = getUser();
// { name: "Alice", age: 30, group: "himawari", income: 800 }
// バイト列を返す(WIT の list<u8> が Uint8Array にマッピング)
const data = listData(); // Uint8Array([1, 2, 3, 4, 5])
// 計算処理
const result = heavyMemoryWork(1000000);
起動はViteを使います。
npx vite
5. 速度計測デモのUI
TypeScript vs WebAssemblyの速度を比較するデモ画面を作りました。「計算する数値 n」を入力してボタンを押すと、同じメモリ集中計算を両方で実行してタイムを表示します。
(※ブラウザのJIT最適化が入るため、回数を重ねるほどTypeScript側も速くなる傾向があります)
pythonから呼び出す
WITはPythonからも呼び出せるのが良いという話をしていたので、実際に動いたPythonコードも記載しておきます。
import time
from wasmtime import Config, Engine, Store, WasiConfig
from wasmtime.component import Component, Linker
# ==========================================
# 1. 純粋なPythonによる実装 (比較用・激遅)
# ==========================================
def fibonacci_py(n: int) -> int:
if n <= 1:
return n
return fibonacci_py(n - 1) + fibonacci_py(n - 2)
# ==========================================
# 2. メイン処理
# ==========================================
def main():
n = 40 # 計算対象の数値(40だと純粋なPythonは数十秒かかります)
print(f"=== フィボナッチ計算(n={n}) 速度対決(Python環境) ===\n")
# --- 準備: Wasmtimeのセットアップ ---
# コンポーネントを手動ロード
config = Config()
config.wasm_component_model = True
engine = Engine(config)
# WASI設定
wasi_config = WasiConfig()
wasi_config.inherit_stdout()
wasi_config.inherit_stderr()
wasi_config.inherit_env()
store = Store(engine, wasi_config)
try:
import os
# スクリプトのディレクトリからの相対パスで解決(python/src/main.py から見て ../../rust/...)
script_dir = os.path.dirname(os.path.abspath(__file__))
# WASI依存回避のため、wasm32-unknown-unknownビルドを使用
wasm_path = os.path.join(script_dir, "../../rust/wasm-perf-wit/target/wasm32-unknown-unknown/release/wasm_perf_wit.wasm")
component = Component.from_file(engine, wasm_path)
linker = Linker(engine)
instance = linker.instantiate(store, component)
# WASMコンポーネントのエクスポート関数を取得
fibonacci_func = instance.get_func(store, "fibonacci")
if fibonacci_func is None:
raise KeyError("Function 'fibonacci' not found in WASM exports.")
except Exception as e:
print(f"Error loading WASM: {e}")
return
# ------------------------------------------------
# 計測1: WebAssembly (Rustコンポーネント)
# ------------------------------------------------
print("[2/3] WebAssembly (Rust)で計算中...")
start_time = time.perf_counter()
ans_wasm = fibonacci_func(store, n)
time_wasm = time.perf_counter() - start_time
print(f" -> 答え: {ans_wasm}, 時間: {time_wasm:.4f} 秒\n")
# ------------------------------------------------
# 計測2: 純粋なPython (※最後にしないと待ち時間が辛いため)
# ------------------------------------------------
print("[2/2] Pythonで計算中...")
start_time = time.perf_counter()
ans_py = fibonacci_py(n)
time_py = time.perf_counter() - start_time
print(f" -> 答え: {ans_py}, 時間: {time_py:.4f} 秒\n")
# ------------------------------------------------
# 結果発表
# ------------------------------------------------
print(f"1. 純粋なPython : {time_py:.4f} 秒")
print(f"2. WebAssembly : {time_wasm:.4f} 秒")
if __name__ == "__main__":
main()
補足情報
wasm-bindgenとWITはどちらを使えばいい?
| 観点 | wasm-bindgen | WIT(Component Model) |
|---|---|---|
| 対象言語ペア | Rust ↔ JavaScript専用 | 言語非依存 |
| 使いやすさ | Rust + JSに特化した高い使い勝手 | 汎用的だがツールチェーンがやや複雑 |
| 型サポート | DOM 操作なども含む豊富な API | WITで定義した型のみ |
| 将来性 | JSエコシステム内では引き続き有力 | マルチ言語相互運用の標準化が進む |
結論: Rust ↔ JavaScriptに閉じるならwasm-bindgenの方が扱いやすいです。複数言語をまたいでコンポーネントを使いまわしたい場合はWITを選ぶとよいでしょう。なお、wasm-bindgenで作ったコードもラッパーを書けばWITに変換できます。
WASIとは?
wasmと紛らわしいので、概念を整理します。
- Wasm … バイトコード形式の仕様。ブラウザやランタイムで実行できる
-
WASI(WebAssembly System Interface) … OSの機能(ファイル I/O、標準出力など)をWasmから呼び出すためのインターフェース
- WASIを組み込まないと
console.logやファイル書き込みができない
- WASIを組み込まないと
ビルドターゲットの表記で確認できます。
| ターゲット名 | 意味 |
|---|---|
wasm32-unknown-unknown |
32bit Wasm・ベンダー/OS機能なし |
wasm32-wasip1 |
32bit Wasm + WASI v1(今回の例) |
wasm32-wasip2 |
32bit Wasm + WASI v2(新仕様) |
wasip1 vs wasip2
| wasip1 | wasip2 | |
|---|---|---|
| 普及状況 | 広く使われてきた(現在も主流) | サポート拡大中 |
| 機能 | ファイル I/O等の基本OS機能 | 非同期(Promise相当)も言語の壁を越えて使える |
| 推奨度 | 安定・互換性重視 | 今後の標準。新規プロジェクトはこちらを検討 |


