3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

どこでも使えるWebassemnly! Pythonでも使えるWITについて

3
Posted at

要点

本記事の要点

  • 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 モジュールには i32i64f32f64 の4種類のデータ型しか存在しません。また文字列やユーザー定義型のような構造を持つデータの表現にも標準が存在せず、データをどのようにメモリ上に配置するかはプログラミング言語の処理系、またはプログラマーが決めるものとされていました。

WebAssembly Interface Type 入門(zenn.dev)

つまり、文字列・構造体・リストといった型をやり取りするためのルールがなく、言語間での相互運用が非常に難しい状態だったのです。
言語そのものがC++を触ったことがあるような超玄人向けだった時代もあったみたいですね。

wasm-bindgen:Rust ↔ JavaScript専用の橋渡し

この問題をRust ↔ TypeScript(JavaScript)間に絞って解決したのが wasm-bindgen です。
このツールの良い部分は以下の通り。

  • liststringrecord のような高レベルな型を渡せるようになった
  • ビルド時に.d.tsファイルが自動生成され、TypeScriptの型安全を確保できる
  • Rust側からDOMを操作することも可能

もはや完璧ともいえる性能ですが、唯一「wasm-bindgenRust ↔ 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を使う時のイメージ補完になるはずです。

tools-diagram.png

まとめ

基本的な情報はここまでです。まとめると、

  • WebAssemblyは、Web上で動く機械語に近い言語、コンパイル済で高速
  • WITは、Wasm Component Modelを実現するためのインターフェース定義言語
  • WITファイルに型と関数シグネチャを定義し、各言語のツール(cargo-componentjcoなど)がバインディングを自動生成する
  • まだエコシステムは発展途上だが、マルチ言語での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」を入力してボタンを押すと、同じメモリ集中計算を両方で実行してタイムを表示します。

result-02.png

(※ブラウザの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()

result-01.png

補足情報

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やファイル書き込みができない

ビルドターゲットの表記で確認できます。

ターゲット名 意味
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相当)も言語の壁を越えて使える
推奨度 安定・互換性重視 今後の標準。新規プロジェクトはこちらを検討

参考資料

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?