WebAssembly は多くのプログラム言語からライブラリの様に呼び出す事が出来る。
WebAssembly のコードを書く方法は多くの記事で書かれているが、その使い方はフレームワーク等のツールに頼っている事が多い様だ。
本記事では、そのような便利ツールに出来るだけ頼らず JavaScript, Rust, Python, Ruby から WebAssembly を実行する方法を記載する。
WebAssembly は新しい技術である。
目先の最先端ツールに飛びつくのもよいが、その基礎を学んで長く使える知識を身に着けないか?
本記事はシリーズの第6回である。シリーズ記事の一覧は 第1回 の #シリーズ記事一覧 に載せている。シリーズの記事を追加する時は 第1回 の一覧を更新して通知を行うので興味の有る人は 第1回 をストックして欲しい。
本記事の概要と過去記事の復習
第5回 では 1 個のグローバル変数を複数のインスタンス間で共有する方法について紹介した。
今回は、外部プログラムから共有する関数を切り替える事で WebAssembly の挙動を変更する方法について記載する。
オブジェクト指向のプログラム言語では「委譲」と呼ばれる方法を使う事が良くある。
(「is-a ならば継承、has-a ならば委譲」と良く言われる、あれだ。)
WebAssembly のコードをオブジェクト指向のプログラム言語で記載すれば委譲でも継承でも出来るのだが、それとは別に WebAssembly 自体がクラスの様な物だ。
複数の WebAssembly で関数を共有する事で、外部プログラムから動的に「委譲」の様な事が出来る。
他にも Ruby on Rails に代表されるような Web フレームワークでは、ライブラリを切り替える事でバックの DB を Sqlite3 から MySQL に変更するような事が可能だ。WebAssembly でも同様に、ライブラリの切り替えの様な事が出来る。
この様な機能を乱用すると悪夢の様な状況になるのは目に見えているのだが、知識として知っておく事は悪い事ではないので紹介する。
イメージとしては、JavaScript の下記の様なコードを想定する。
class EvenCounter {
constructor() {
this.counter = 0;
}
increment() {
this.counter += 2;
}
get() {
return this.counter;
}
}
class OddCounter {
constructor() {
this.counter = 1;
}
increment() {
this.counter += 2;
}
get() {
return this.counter;
}
}
class Counter {
constructor() {
this.counter = null;
}
increment() {
this.counter.increment();
}
get() {
return this.counter.get();
}
}
let evenCounter = new EvenCounter();
let oddCounter = new OddCounter();
let counter = new Counter();
counter.counter = evenCounter;
console.log(counter.get());
counter.increment();
console.log(counter.get());
counter.counter = oddCounter;
console.log(counter.get());
counter.increment();
console.log(counter.get());
EvenCounter
や OddCounter
は、それぞれ counter
というプロパティを持っている。
Counter
は上記のどちらかのインスタンスをプロパティに持ち、メソッド increment
や get
を呼ばれた時にその処理を委譲する。
本記事では上記の様な WebAssembly のコードを実行する下記の外部プログラムを作成する。
- Vanilla JS (フレームワークやライブラリを使わない、そのままの JavaScript)から WebAssembly を実行
- Rust, Python, Ruby から低レイヤーのライブラリ wasmtime だけを使って WebAssembly を実行
なお、説明の都合上 Vanilla JS から実行 の章は JavaScript に興味が無くとも読んでほしい。
(そんなに難しい事はやっていないので JavaScript を知らなくとも本質的な部分は理解できるはずだ。)
それ以外の興味の無い言語の章については読み飛ばして問題無い。
また、最後に 注意点 についても少し書いた。
大した事は書いていないのだが、ここも読んで欲しい。
WebAssembly コードの準備
過去記事同様、WebAssembly Text Format で動作確認をする WebAssembly のコードを作成し、wat2wasm でコンパイルする。
wabt のインストール
github の README.md を読んでコンパイルする。
C++ のコンパイル環境が必要なので注意。
wat2wasm というコマンドがコンパイルされるので、必要に応じて Path を通しておくと良いだろう。
wasm ファイルの準備
wasm ファイルとは、WebAssembly の規格に沿ったバイナリファイルの事である。
最初に各種言語から動かして動作確認するための wasm ファイルを用意しよう。
今回は 3 個のファイルを用意する。
最初にテキストエディタで以下の様な内容のファイルを作成し /tmp/even_counter.wat
という名前で保存する。
(module
(global $counter (mut i32) (i32.const 0))
(func $increment
global.get $counter
i32.const 2
i32.add
global.set $counter)
(func $get (result i32)
global.get $counter)
(table (export "tbl") 2 funcref)
(elem (i32.const 0) $get $increment))
上記は WebAssembly Text Format という wasm ファイルのソースコードだ。
多くの読者に内容を何となく理解してもらうには、これが良いと思って選択した。
上記コードは大まかに以下の事を行っている。
(以下で i32
とは「符号付 32 bit 整数」の事である。)
- グローバル変数を宣言。この WebAssembly 内部から、このグローバル変数に
$counter
という名前でアクセス可能。この変数は可変なi32
型。インスタンス作成時に0
で初期化される。 - 関数を宣言。この WebAssembly 内部から、この関数に
$increment
という名前でアクセス可能。この関数は引数も戻り値も無い。この関数は、グローバル変数$counter
の値を 2 増加させる。 - 関数を宣言。この WebAssembly 内部から、この関数に
$get
という名前でアクセス可能。この関数は引数を取らず、1 個のi32
型の値を返す。この関数は、グローバル変数$counter
の値を返す。 - 抽象ポインターのテーブルを宣言。このテーブルは外部から
tbl
という名前でアクセス可能。このテーブルは長さ 2 で関数ポインターを保持する。 - 抽象ポインターテーブルの 0 番目から順に、
$get
,$increment
のアドレスを保存。
同様に、下記の内容の /tmp/odd_counter.wat
というファイルを作成する。
/tmp/even_counter.wat
との違いは、グローバル変数 $counter
の初期値が 1
である事だけだ。
(module
(global $counter (mut i32) (i32.const 0))
(func $increment
global.get $counter
i32.const 2
i32.add
global.set $counter)
(func $get (result i32)
global.get $counter)
(table (export "tbl") 2 funcref)
(elem (i32.const 0) $get $increment))
最後に、下記の内容で /tmp/counter.wat
というファイルを作成する。
(module
(table (export "tbl") 2 funcref)
(type $get_type (func (result i32)))
(type $increment_type (func))
(func (export "get") (result i32)
i32.const 0
call_indirect (type $get_type))
(func (export "increment")
i32.const 1
call_indirect (type $increment_type)))
この wat ファイルは以下の事を行っている。
- 抽象ポインターのテーブルを宣言。このテーブルは外部から
tbl
という名前でアクセス可能。このテーブルは長さ 2 で関数ポインターを保持する。 - 関数の型を宣言。この型には、この WebAssembly 内部から
$get_type
という名前でアクセス可能。この型は、引数を取らずi32
型の値を 1 個返す。 - 関数の型を宣言。この型には、この WebAssembly 内部から
$increment_type
という名前でアクセス可能。この型は、引数も戻り値も無い。 - 関数を宣言。この関数には外部から
get
という名前でアクセス可能。この関数は引数を取らずi32
型の値を 1 個返す。この関数は抽象ポインターテーブルの0
番目の関数を実行する。 - 関数を宣言。この関数には外部から
increment
という名前でアクセス可能。この関数は引数も戻り値も無い。この関数は抽象ポインターテーブルの1
番目の関数を実行する。
関数の型を変数に保存するのは、関数 get
や increment
の中で使うためだ。
抽象ポインター経由で関数を実行するには、型情報を教える必要が有る。
この wat ファイルを以下の様に wat2wasm コマンドでコンパイルすると /tmp
以下に even_counter.wasm
, odd_counter.wasm
, counter.wasm
という 3 個のバイナリファイルが出来る。
$ wat2wasm -o /tmp/even_counter.wasm /tmp/even_counter.wat
$ wat2wasm -o /tmp/odd_counter.wasm /tmp/odd_counter.wat
$ wat2wasm -o /tmp/counter.wasm /tmp/counter.wat
Vanilla JS から実行
説明の都合上、本章は JavaScript に興味の無い人でも斜めに読んで欲しい。
筆者の JavaScript 環境
- nodejs v18.16.1
Vanilla JS のコード
nodejs から WebAssembly を実行するコードは、例えば下記の様になる。
// Build Module
const fs = require('fs');
const evenCounterWasm = fs.readFileSync('/tmp/even_counter.wasm');
const evenCounterMod = new WebAssembly.Module(evenCounterWasm);
const oddCounterWasm = fs.readFileSync('/tmp/odd_counter.wasm');
const oddCounterMod = new WebAssembly.Module(oddCounterWasm);
const counterWasm = fs.readFileSync('/tmp/counter.wasm');
const counterMod = new WebAssembly.Module(counterWasm);
// Build Instance
const importObject = {};
const evenCounter = new WebAssembly.Instance(evenCounterMod, importObject);
const oddCounter = new WebAssembly.Instance(oddCounterMod, importObject);
const counter = new WebAssembly.Instance(counterMod, importObject);
const GET_IDX = 0;
const INCREMENT_IDX = 1;
// Set evenCounter to Counter
let get = evenCounter.exports.tbl.get(GET_IDX);
let increment = evenCounter.exports.tbl.get(INCREMENT_IDX);
counter.exports.tbl.set(GET_IDX, get);
counter.exports.tbl.set(INCREMENT_IDX, increment);
// Operation check
console.log(counter.exports.get());
counter.exports.increment();
console.log(counter.exports.get());
// Set oddCounter to Counter
get = oddCounter.exports.tbl.get(GET_IDX);
increment = oddCounter.exports.tbl.get(INCREMENT_IDX);
counter.exports.tbl.set(GET_IDX, get);
counter.exports.tbl.set(INCREMENT_IDX, increment);
// Operation check
console.log(counter.exports.tbl.get(GET_IDX)());
counter.exports.tbl.get(INCREMENT_IDX)();
console.log(counter.exports.tbl.get(GET_IDX)());
上記のファイルを call_wasm.js
という名前で保存し実行すると、下記の様な結果を得る。
$ node call_wasm.js
0
2
1
3
JavaScript のコードを少しずつ読んでみる。
(以下の 太字リンク は JavaScript のクラス名(コンストラクター名)である事を示す。リンク先は MDN Web Docs 。)
最初に以下を読んでみる。
const fs = require('fs');
const evenCounterWasm = fs.readFileSync('/tmp/even_counter.wasm');
const evenCounterMod = new WebAssembly.Module(evenCounterWasm);
const oddCounterWasm = fs.readFileSync('/tmp/odd_counter.wasm');
const oddCounterMod = new WebAssembly.Module(oddCounterWasm);
const counterWasm = fs.readFileSync('/tmp/counter.wasm');
const counterMod = new WebAssembly.Module(counterWasm);
// Build Instance
const importObject = {};
const evenCounter = new WebAssembly.Instance(evenCounterMod, importObject);
const oddCounter = new WebAssembly.Instance(oddCounterMod, importObject);
const counter = new WebAssembly.Instance(counterMod, importObject);
ここまでは 第1回 とほとんど同じだ。
違いはファイルパスと、 Module と Instance をそれぞれ 3 個ずつ作っている事だけだ。
続いて以下の行を読んでみる。
const GET_IDX = 0;
const INCREMENT_IDX = 1;
// Set evenCounter to Counter
let get = evenCounter.exports.tbl.get(GET_IDX);
let increment = evenCounter.exports.tbl.get(INCREMENT_IDX);
counter.exports.tbl.set(GET_IDX, get);
counter.exports.tbl.set(INCREMENT_IDX, increment);
ここでは counter
に evenCounter
を持たせている。
とは言っても、今回実際に持たせるのは evenCounter
の export している関数ポインターだ。
( evenCounter
自身を持たせる事も出来るのだが、それは今回は説明しない。)
WebAssembly の関数はそのインスタンスと紐づいている。
(プログラマーにとっては関数というより、メソッドやクロージャーと言った方が直感的かも知れない。)
「関数を渡す」という事は、「その関数を持つ WebAssembly のインスタンスや、そのグローバル変数を渡す」という事でもある。
evenCounter
の抽象ポインターテーブルの index 0 と 1 の値を、それぞれ counter
の抽象ポインターテーブルの index 0 と 1 にコピーしている。
次に、以下の行を見てみる。
// Operation check
console.log(counter.exports.get());
counter.exports.increment();
console.log(counter.exports.get());
ここでは 第1回 と同じ方法で関数 get
と increment
を実行している。
より詳細に説明すると、以下だ。
- 関数
get
を実行。(evenCounter
のグローバル変数$counter
の初期値0
が表示される。) - 関数
increment
を実行。(evenCounter
のグローバル変数$counter
の値が2
増加する。) - 関数
get
を実行。(evenCounter
のグローバル変数$counter
の現在値2
が表示される。)
次に以下の行を読んでみる。
// Set oddCounter to Counter
get = oddCounter.exports.tbl.get(GET_IDX);
increment = oddCounter.exports.tbl.get(INCREMENT_IDX);
counter.exports.tbl.set(GET_IDX, get);
counter.exports.tbl.set(INCREMENT_IDX, increment);
ここは先ほどと同様の方法で counter
に oddCounter
を持たせている。
次に以下の行を読んでみる。
// Operation check
console.log(counter.exports.tbl.get(GET_IDX)());
counter.exports.tbl.get(INCREMENT_IDX)();
console.log(counter.exports.tbl.get(GET_IDX)());
ここでは先ほどと別の方法で counter
の動作確認をしている。
counter
の関数 get
や increment
は、抽象ポインターテーブルに保存されたポインター先の関数を実行しているだけだ。
しかし counter
は抽象ポインターテーブルを export しているので、直接実行する事もできる。
- 抽象ポインターテーブルの index 0 を実行(
oddCounter
のグローバル変数$counter
の初期値1
が表示される。) - 抽象ポインターテーブルの index 1 を実行(
oddCounter
のグローバル変数$counter
の値が2
増加する。) - 抽象ポインターテーブルの index 0 を実行(
oddCounter
のグローバル変数$counter
の初期値3
が表示される。)
この方法は counter
の抽象ポインターテーブルを export しているから出来る事だ。
wasmtime から実行
以降の章では wasmtime を使って Rust から実行, Python から実行, Ruby から実行 方法について、それぞれ説明している。
不要な章は読み飛ばしても大丈夫だ。
Rust から実行
筆者の Rust 環境
- cargo 1.70.0
- rustup 1.26.0
- rustc 1.70.0 stable-x86_64-unknown-linux-gnu
Rust のコード
下記の様に call_counter というプロジェクトを作成する。
$ cargo new call_counter
$ cd call_counter
Cargo.toml の [dependencies]
セクションに wasmtime をの設定を加える。
筆者の環境では Cargo.toml は下記の様になった。
[package]
name = "call_counter"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
wasmtime = "10.0.1"
Rust から WebAssembly を実行するコードは、例えば下記の様になる。
use std::fs;
use wasmtime::*;
fn main() -> wasmtime::Result<()> {
// Build `Engine`.
let engine = Engine::default();
// Build `Module`.
let even_counter_wasm = fs::read("/tmp/even_counter.wasm").unwrap();
let even_counter_module = Module::new(&engine, even_counter_wasm)?;
let odd_counter_wasm = fs::read("/tmp/odd_counter.wasm").unwrap();
let odd_counter_module = Module::new(&engine, odd_counter_wasm)?;
let counter_wasm = fs::read("/tmp/counter.wasm").unwrap();
let counter_module = Module::new(&engine, counter_wasm)?;
// Build `Store`.
let mut store = Store::new(&engine, ());
// Build `Instance`
let even_counter = Instance::new(&mut store, &even_counter_module, &[])?;
let odd_counter = Instance::new(&mut store, &odd_counter_module, &[])?;
let counter = Instance::new(&mut store, &counter_module, &[])?;
const GET_IDX: u32 = 0;
const INCREMENT_IDX: u32 = 1;
// Set even_counter to Counter
let even_counter_table = even_counter.get_table(&mut store, "tbl").unwrap();
let get = even_counter_table.get(&mut store, GET_IDX).unwrap();
let increment = even_counter_table.get(&mut store, INCREMENT_IDX).unwrap();
let table = counter.get_table(&mut store, "tbl").unwrap();
table.set(&mut store, GET_IDX, get)?;
table.set(&mut store, INCREMENT_IDX, increment)?;
// Operation check
let get = counter.get_typed_func::<(), i32>(&mut store, "get")?;
let increment = counter.get_typed_func::<(), ()>(&mut store, "increment")?;
println!("{}", get.call(&mut store, ())?);
increment.call(&mut store, ())?;
println!("{}", get.call(&mut store, ())?);
// Set odd_counter to Counter
let odd_counter_table = odd_counter.get_table(&mut store, "tbl").unwrap();
let get = odd_counter_table.get(&mut store, GET_IDX).unwrap();
let increment = odd_counter_table.get(&mut store, INCREMENT_IDX).unwrap();
table.set(&mut store, GET_IDX, get)?;
table.set(&mut store, INCREMENT_IDX, increment)?;
// Operation check
let get = table
.get(&mut store, GET_IDX)
.unwrap()
.unwrap_funcref()
.unwrap()
.typed::<(), i32>(&store)?;
let increment = table
.get(&mut store, INCREMENT_IDX)
.unwrap()
.unwrap_funcref()
.unwrap()
.typed::<(), ()>(&store)?;
println!("{}", get.call(&mut store, ())?);
increment.call(&mut store, ())?;
println!("{}", get.call(&mut store, ())?);
Ok(())
}
上記の内容で src/main.rs
を上書きして実行すると、下記の様な結果を得る。
$ cargo run
Compiling call_counter v0.1.0 (/home/wbcchsyn/tmp/vtable/call_counter)
Finished dev [unoptimized + debuginfo] target(s) in 2.68s
Running `target/debug/call_counter`
0
2
1
3
上記の Rust コードを少しずつ解説していく。
以下で 太字のリンク は wasmtime で定義されたクラスである事を示す。リンク先は Rust 版 wasmtime の公式ドキュメント 。
まずはコードの全体像を把握しよう。
use std::fs;
use wasmtime::*;
fn main() -> wasmtime::Result<()> {
...
}
見ての通り、本コードは main 関数が定義してあるだけである。
以下、main 関数の中身を上から少しずつ読んでみる
最初は少しまとめて読んで見る。
// Build `Engine`.
let engine = Engine::default();
// Build `Module`.
let even_counter_wasm = fs::read("/tmp/even_counter.wasm").unwrap();
let even_counter_module = Module::new(&engine, even_counter_wasm)?;
let odd_counter_wasm = fs::read("/tmp/odd_counter.wasm").unwrap();
let odd_counter_module = Module::new(&engine, odd_counter_wasm)?;
let counter_wasm = fs::read("/tmp/counter.wasm").unwrap();
let counter_module = Module::new(&engine, counter_wasm)?;
// Build `Store`.
let mut store = Store::new(&engine, ());
// Build `Instance`
let even_counter = Instance::new(&mut store, &even_counter_module, &[])?;
let odd_counter = Instance::new(&mut store, &odd_counter_module, &[])?;
let counter = Instance::new(&mut store, &counter_module, &[])?;
ここは 第1回 とほとんど同じだ。
違いはファイルパスと、 Module, Instance を 3 個ずつ作っている事だ。
続いて以下の行を読んでみる。
const GET_IDX: u32 = 0;
const INCREMENT_IDX: u32 = 1;
// Set even_counter to Counter
let even_counter_table = even_counter.get_table(&mut store, "tbl").unwrap();
let get = even_counter_table.get(&mut store, GET_IDX).unwrap();
let increment = even_counter_table.get(&mut store, INCREMENT_IDX).unwrap();
let table = counter.get_table(&mut store, "tbl").unwrap();
table.set(&mut store, GET_IDX, get)?;
table.set(&mut store, INCREMENT_IDX, increment)?;
ここでは counter
に even_counter
を持たせている。
Vanilla JS のコード 同様、 counter
に even_counter
の関数ポインターを持たせている。
WebAssembly のポインターを渡すという事は、そのインスタンスを渡すという事なのでこれで問題無い。
even_counter
の抽象ポインターテーブルの index 0 と 1 の値を、それぞれ counter
の抽象ポインターテーブルの index 0 と 1 にコピーしている。
続いて以下の行を読んでみる。
// Operation check
let get = counter.get_typed_func::<(), i32>(&mut store, "get")?;
let increment = counter.get_typed_func::<(), ()>(&mut store, "increment")?;
println!("{}", get.call(&mut store, ())?);
increment.call(&mut store, ())?;
println!("{}", get.call(&mut store, ())?);
ここでは 第1回 と同じ方法で関数 get
と increment
を実行している。
より詳細に説明すると、以下だ。
- 関数
get
を実行。(even_counter
のグローバル変数$counter
の初期値0
が表示される。) - 関数
increment
を実行。(even_counter
のグローバル変数$counter
の値が2
増加する。) - 関数
get
を実行。(even_counter
のグローバル変数$counter
の現在値2
が表示される。)
続いて以下の行を読んでみる。
// Set odd_counter to Counter
let odd_counter_table = odd_counter.get_table(&mut store, "tbl").unwrap();
let get = odd_counter_table.get(&mut store, GET_IDX).unwrap();
let increment = odd_counter_table.get(&mut store, INCREMENT_IDX).unwrap();
table.set(&mut store, GET_IDX, get)?;
table.set(&mut store, INCREMENT_IDX, increment)?;
ここは先ほどと同様の方法で counter
に odd_counter
を持たせている。
続いて以下の行を読んでみる。
// Operation check
let get = table
.get(&mut store, GET_IDX)
.unwrap()
.unwrap_funcref()
.unwrap()
.typed::<(), i32>(&store)?;
let increment = table
.get(&mut store, INCREMENT_IDX)
.unwrap()
.unwrap_funcref()
.unwrap()
.typed::<(), ()>(&store)?;
println!("{}", get.call(&mut store, ())?);
increment.call(&mut store, ())?;
println!("{}", get.call(&mut store, ())?);
ここでは先ほどと別の方法で counter
の動作確認をしている。
(型変換とエラー処理が面倒だが、やってる事は簡単だ。)
counter
の関数 get
や increment
は、抽象ポインターテーブルに保存されたポインター先の関数を実行しているだけだ。
しかし counter
は抽象ポインターテーブルを export しているので、直接実行する事もできる。
- 抽象ポインターテーブルの index 0 を実行(
odd_counter
のグローバル変数$counter
の初期値1
が表示される。) - 抽象ポインターテーブルの index 1 を実行(
odd_counter
のグローバル変数$counter
の値が2
増加する。) - 抽象ポインターテーブルの index 0 を実行(
odd_counter
のグローバル変数$counter
の初期値3
が表示される。)
この方法は counter
の抽象ポインターテーブルを export しているから出来る事だ。
Python から実行
筆者の Python 環境
- Python 3.11.2
- pip 23.0.1
Python のコード
最初に Python の version と環境に応じた方法で wasmtime をインストールする。
$ pip install wasmtime
Collecting wasmtime
Downloading wasmtime-9.0.0-py3-none-manylinux1_x86_64.whl (6.6 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.6/6.6 MB 2.1 MB/s eta 0:00:00
Installing collected packages: wasmtime
Successfully installed wasmtime-9.0.0
Python から WebAssembly を実行するコードは、例えば下記の様になる。
from wasmtime import Engine, Module, Store, Instance
# Build Engine
engine = Engine()
# Build Module
even_counter_wasm = open('/tmp/even_counter.wasm', 'rb').read()
even_counter_module = Module(engine, even_counter_wasm)
odd_counter_wasm = open('/tmp/odd_counter.wasm', 'rb').read()
odd_counter_module = Module(engine, odd_counter_wasm)
counter_wasm = open('/tmp/counter.wasm', 'rb').read()
counter_module = Module(engine, counter_wasm)
# Build Store
store = Store(engine)
# Build Instance
even_counter = Instance(store, even_counter_module, ())
odd_counter = Instance(store, odd_counter_module, ())
counter = Instance(store, counter_module, ())
GET_IDX = 0
INCREMENT_IDX = 1
# Set even_counter to Counter
get = even_counter.exports(store)['tbl'].get(store, GET_IDX)
increment = even_counter.exports(store)['tbl'].get(store, INCREMENT_IDX)
counter.exports(store)['tbl'].set(store, GET_IDX, get)
counter.exports(store)['tbl'].set(store, INCREMENT_IDX, increment)
# Operation check
print(counter.exports(store)['get'](store))
counter.exports(store)['increment'](store)
print(counter.exports(store)['get'](store))
# Set odd_counter to Counter
get = odd_counter.exports(store)['tbl'].get(store, GET_IDX)
increment = odd_counter.exports(store)['tbl'].get(store, INCREMENT_IDX)
counter.exports(store)['tbl'].set(store, GET_IDX, get)
counter.exports(store)['tbl'].set(store, INCREMENT_IDX, increment)
# Operation check
print(counter.exports(store)['tbl'].get(store, GET_IDX)(store))
counter.exports(store)['tbl'].get(store, INCREMENT_IDX)(store)
print(counter.exports(store)['tbl'].get(store, GET_IDX)(store))
上記のファイルを call_wasm.py
という名前で保存し実行すると、下記の様な結果を得る。
$ python call_wasm.py
0
2
1
3
上記の Python コードを少しずつ解説していく。
以下で 太字のリンク は wasmtime で定義されたクラスである事を示す。リンク先は Python 版 wasmtime の公式ドキュメント 。
最初は少しまとめて読んで見る。
from wasmtime import Engine, Module, Store, Instance
# Build Engine
engine = Engine()
# Build Module
even_counter_wasm = open('/tmp/even_counter.wasm', 'rb').read()
even_counter_module = Module(engine, even_counter_wasm)
odd_counter_wasm = open('/tmp/odd_counter.wasm', 'rb').read()
odd_counter_module = Module(engine, odd_counter_wasm)
counter_wasm = open('/tmp/counter.wasm', 'rb').read()
counter_module = Module(engine, counter_wasm)
# Build Store
store = Store(engine)
# Build Instance
even_counter = Instance(store, even_counter_module, ())
odd_counter = Instance(store, odd_counter_module, ())
counter = Instance(store, counter_module, ())
ここは 第1回 とほとんど同じだ。
違いはファイルパスと、 Module, Instance を 3 個ずつ作っている事だ。
続いて以下の行を読んでみる。
GET_IDX = 0
INCREMENT_IDX = 1
# Set even_counter to Counter
get = even_counter.exports(store)['tbl'].get(store, GET_IDX)
increment = even_counter.exports(store)['tbl'].get(store, INCREMENT_IDX)
counter.exports(store)['tbl'].set(store, GET_IDX, get)
counter.exports(store)['tbl'].set(store, INCREMENT_IDX, increment)
Vanilla JS のコード 同様、 counter
に even_counter
の関数ポインターを持たせている。
WebAssembly のポインターを渡すという事は、そのインスタンスを渡すという事なのでこれで問題無い。
even_counter
の抽象ポインターテーブルの index 0 と 1 の値を、それぞれ counter
の抽象ポインターテーブルの index 0 と 1 にコピーしている。
続いて以下の行を読んでみる。
# Operation check
print(counter.exports(store)['get'](store))
counter.exports(store)['increment'](store)
print(counter.exports(store)['get'](store))
ここでは 第1回 と同じ方法で関数 get
と increment
を実行している。
より詳細に説明すると、以下だ。
- 関数
get
を実行。(even_counter
のグローバル変数$counter
の初期値0
が表示される。) - 関数
increment
を実行。(even_counter
のグローバル変数$counter
の値が2
増加する。) - 関数
get
を実行。(even_counter
のグローバル変数$counter
の現在値2
が表示される。)
続いて以下の行を読んでみる。
# Set odd_counter to Counter
get = odd_counter.exports(store)['tbl'].get(store, GET_IDX)
increment = odd_counter.exports(store)['tbl'].get(store, INCREMENT_IDX)
counter.exports(store)['tbl'].set(store, GET_IDX, get)
counter.exports(store)['tbl'].set(store, INCREMENT_IDX, increment)
ここは先ほどと同様の方法で counter
に odd_counter
を持たせている。
続いて以下の行を読んでみる。
# Operation check
print(counter.exports(store)['tbl'].get(store, GET_IDX)(store))
counter.exports(store)['tbl'].get(store, INCREMENT_IDX)(store)
print(counter.exports(store)['tbl'].get(store, GET_IDX)(store))
ここでは先ほどと別の方法で counter
の動作確認をしている。
counter
の関数 get
や increment
は、抽象ポインターテーブルに保存されたポインター先の関数を実行しているだけだ。
しかし counter
は抽象ポインターテーブルを export しているので、直接実行する事もできる。
- 抽象ポインターテーブルの index 0 を実行(
odd_counter
のグローバル変数$counter
の初期値1
が表示される。) - 抽象ポインターテーブルの index 1 を実行(
odd_counter
のグローバル変数$counter
の値が2
増加する。) - 抽象ポインターテーブルの index 0 を実行(
odd_counter
のグローバル変数$counter
の初期値3
が表示される。)
この方法は counter
の抽象ポインターテーブルを export しているから出来る事だ。
Ruby から実行
筆者の Ruby 環境
- ruby 3.1.2p20
Ruby のコード
最初に gem で wasmtime をインストールする。
(必要に応じて bundler 等を使うと良いだろう。)
$ gem install wasmtime
Fetching wasmtime-9.0.1-x86_64-linux.gem
Successfully installed wasmtime-9.0.1-x86_64-linux
Parsing documentation for wasmtime-9.0.1-x86_64-linux
Installing ri documentation for wasmtime-9.0.1-x86_64-linux
Done installing documentation for wasmtime after 0 seconds
1 gem installed
Ruby から WebAssembly を実行するコードは、例えば下記の様になる。
require 'wasmtime'
# Build Engine
engine = Wasmtime::Engine.new
# Build Module
even_counter_wasm = open('/tmp/even_counter.wasm', 'rb').read
even_counter_mod = Wasmtime::Module.new(engine, even_counter_wasm)
odd_counter_wasm = open('/tmp/odd_counter.wasm', 'rb').read
odd_counter_mod = Wasmtime::Module.new(engine, odd_counter_wasm)
counter_wasm = open('/tmp/counter.wasm', 'rb').read
counter_mod = Wasmtime::Module.new(engine, counter_wasm)
# Build Store
store = Wasmtime::Store.new(engine)
# Build instance
even_counter = Wasmtime::Instance.new(store, even_counter_mod)
odd_counter = Wasmtime::Instance.new(store, odd_counter_mod)
counter = Wasmtime::Instance.new(store, counter_mod)
GET_IDX = 0
INCREMENT_IDX = 1
# Set even_counter to Counter
get = even_counter.export('tbl').to_table.get(GET_IDX)
increment = even_counter.export('tbl').to_table.get(INCREMENT_IDX)
counter.export('tbl').to_table.set(GET_IDX, get)
counter.export('tbl').to_table.set(INCREMENT_IDX, increment)
# Operation check
puts(counter.export('get').to_func.call())
counter.export('increment').to_func.call()
puts(counter.export('get').to_func.call())
# Set odd_counter to Counter
get = odd_counter.export('tbl').to_table.get(GET_IDX)
increment = odd_counter.export('tbl').to_table.get(INCREMENT_IDX)
counter.export('tbl').to_table.set(GET_IDX, get)
counter.export('tbl').to_table.set(INCREMENT_IDX, increment)
# Operation check
puts(counter.export('tbl').to_table.get(GET_IDX).call())
counter.export('tbl').to_table.get(INCREMENT_IDX).call()
puts(counter.export('tbl').to_table.get(GET_IDX).call())
上記の Ruby コードを少しずつ解説していく。
以下で 太字のリンク は wasmtime で定義されたクラスである事を示す。リンク先は Ruby 版 wasmtime の公式ドキュメント 。
最初は少しまとめて読んで見る。
require 'wasmtime'
# Build Engine
engine = Wasmtime::Engine.new
# Build Module
even_counter_wasm = open('/tmp/even_counter.wasm', 'rb').read
even_counter_mod = Wasmtime::Module.new(engine, even_counter_wasm)
odd_counter_wasm = open('/tmp/odd_counter.wasm', 'rb').read
odd_counter_mod = Wasmtime::Module.new(engine, odd_counter_wasm)
counter_wasm = open('/tmp/counter.wasm', 'rb').read
counter_mod = Wasmtime::Module.new(engine, counter_wasm)
# Build Store
store = Wasmtime::Store.new(engine)
# Build instance
even_counter = Wasmtime::Instance.new(store, even_counter_mod)
odd_counter = Wasmtime::Instance.new(store, odd_counter_mod)
counter = Wasmtime::Instance.new(store, counter_mod)
ここは 第1回 とほとんど同じだ。
違いはファイルパスと、 Module, Instance を 3 個ずつ作っている事だ。
続いて以下の行を読んでみる。
GET_IDX = 0
INCREMENT_IDX = 1
# Set even_counter to Counter
get = even_counter.export('tbl').to_table.get(GET_IDX)
increment = even_counter.export('tbl').to_table.get(INCREMENT_IDX)
counter.export('tbl').to_table.set(GET_IDX, get)
counter.export('tbl').to_table.set(INCREMENT_IDX, increment)
Vanilla JS のコード 同様、 counter
に even_counter
の関数ポインターを持たせている。
WebAssembly のポインターを渡すという事は、そのインスタンスを渡すという事なのでこれで問題無い。
even_counter
の抽象ポインターテーブルの index 0 と 1 の値を、それぞれ counter
の抽象ポインターテーブルの index 0 と 1 にコピーしている。
続いて以下の行を読んでみる。
# Operation check
puts(counter.export('get').to_func.call())
counter.export('increment').to_func.call()
puts(counter.export('get').to_func.call())
ここでは 第1回 と同じ方法で関数 get
と increment
を実行している。
より詳細に説明すると、以下だ。
- 関数
get
を実行。(even_counter
のグローバル変数$counter
の初期値0
が表示される。) - 関数
increment
を実行。(even_counter
のグローバル変数$counter
の値が2
増加する。) - 関数
get
を実行。(even_counter
のグローバル変数$counter
の現在値2
が表示される。)
続いて以下の行を読んでみる。
# Set odd_counter to Counter
get = odd_counter.export('tbl').to_table.get(GET_IDX)
increment = odd_counter.export('tbl').to_table.get(INCREMENT_IDX)
counter.export('tbl').to_table.set(GET_IDX, get)
counter.export('tbl').to_table.set(INCREMENT_IDX, increment)
ここは先ほどと同様の方法で counter
に odd_counter
を持たせている。
続いて以下の行を読んでみる。
# Operation check
puts(counter.export('tbl').to_table.get(GET_IDX).call())
counter.export('tbl').to_table.get(INCREMENT_IDX).call()
puts(counter.export('tbl').to_table.get(GET_IDX).call())
ここでは先ほどと別の方法で counter
の動作確認をしている。
counter
の関数 get
や increment
は、抽象ポインターテーブルに保存されたポインター先の関数を実行しているだけだ。
しかし counter
は抽象ポインターテーブルを export しているので、直接実行する事もできる。
- 抽象ポインターテーブルの index 0 を実行(
odd_counter
のグローバル変数$counter
の初期値1
が表示される。) - 抽象ポインターテーブルの index 1 を実行(
odd_counter
のグローバル変数$counter
の値が2
増加する。) - 抽象ポインターテーブルの index 0 を実行(
odd_counter
のグローバル変数$counter
の初期値3
が表示される。)
この方法は counter
の抽象ポインターテーブルを export しているから出来る事だ。
注意点
今回は抽象ポインターテーブルを用いて外部プログラムから WebAssembly の挙動を変更する方法についてご紹介した。
この抽象ポインターは非常に強力だが、やり方を間違えると危険なので注意して欲しい。
まず WebAssembly に限らずプログラミングの一般論として、筆者は「黒魔術」とか「メタプログラミング」と呼ばれる物があまり好きではない。これらは非常に強力だが、コードの保守性が下がる恐れがある。
そんな物を使わずに普通に書けるのならば、その方が良いと思う。
次に WebAssembly 特有の事情について、簡単にご紹介する。
まず、本記事執筆時(2023 年 7 月 14 日)の WebAssembly の抽象ポインターテーブルの仕様は不完全だと筆者は考えている。
かつては「WebAssembly の抽象ポインター」と言えば、今回の様な「関数を指すポインター」の事だった。
しかし今では「(主に外部プログラムの)オブジェクトを指すポインター」も加わった。
抽象ポインターテーブルは各 WebAssembly に 1 個ずつしか作成できない。
さらに、各抽象ポインターテーブルには特定の型の抽象ポインターしか保存できない。
つまり各 WebAssembly は、「関数を指すポインター」か「(主に外部プログラムの)オブジェクトを指すポインター」の両方を抽象ポインターテーブルに保存する事は出来ないのだ。
次に抽象ポインターを介した関数呼び出しでは、WebAssembly はコンパイル時に型チェックを行う事が出来ない。
counter.wat
では抽象ポインターを介して関数を実行する時に、その型情報を一緒に記載した。
WebAssembly で抽象ポインターを介して関数を実行する時は、この様に「ポインターの指す場所には、与えられた型情報の関数が存在する」と信じて実行するしかない。
ポインターの指す先が違っていた時にどうなるのか、筆者には分からない。
(現状ではランタイムエラーになる事が多い様だが、このエラーチェックがどこまで信じられるのか筆者には分からない。)
一方で、抽象ポインターは非常に強力である事も確かだ。
筆者の知る限り、この方法は作成済みの WebAssembly のインスタンスの関数を切り替える唯一の方法である。
(裏技は存在すると思うが、WebAssembly の仕様上で明記された方法はこれ以外に知らない。)
この方法はフレームワーク作成時などに利用されるだろう。
ユーザーとしてフレームワークを使用する場合も、内部的な構造を想像出来る状態でいると良いと思う。
正しく理解して適切に使いたい物だ。