WebAssembly は多くのプログラム言語からライブラリの様に呼び出す事が出来る。
WebAssembly のコードを書く方法は多くの記事で書かれているが、その使い方はフレームワーク等のツールに頼っている事が多い様だ。
本記事では、そのような便利ツールに出来るだけ頼らず JavaScript, Rust, Python, Ruby から WebAssembly を実行する方法を記載する。
WebAssembly は新しい技術である。
目先の最先端ツールに飛びつくのもよいが、その基礎を学んで長く使える知識を身に着けないか?
本記事はシリーズの第3回である。シリーズ記事の一覧は 第1回 の #シリーズ記事一覧 に載せている。シリーズの記事を追加する時は 第1回 の一覧を更新して通知を行うので興味の有る人は 第1回 をストックして欲しい。
本記事の概要と過去記事の復習
第3回 と 第4回 は WebAssembly のコードに状態を持たせる方法について記載した。
今回は WebAssembly に「クラス変数」の様な物を追加するコードを紹介する。
( 第4回 の最後に「文字列を保持する可変長配列」について予告の様な事をしましたが、その記事は説明の都合上後回しにします。)
「クラス変数」とは「そのクラスの名前空間に保存された変数」の様な物だ。
多くの言語で「そのクラスのインスタンス間で共有する変数」の様な扱いが出来る。
筆者の知る限り、JavaScript にはクラス変数に相当する物は無い。
しかしモジュールの中で定義されたグローバル変数はクラス変数と少し似ているかもしれない。
例えば、以下の様な JavaScript のモジュールを考えてみる。
let next_id = 0;
const FIRST_ID = 0;
class Counter {
constructor() {
this.__id = next_id;
next_id += 1;
}
get_id() {
return this.__id;
}
is_first() {
return this.__id === FIRST_ID;
}
}
Counter
というクラスの全インスタンスは next_id
, FIRST_ID
という 2 個の変数を共有している。
WebAssembly も JavaScript と同様にクラス変数に相当する物は存在しない。
しかし、工夫次第で「クラス変数の様な物」を作る事は可能である。
本記事では上記の様な 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 ファイルを用意しよう。
テキストエディタで以下の様な内容のファイルを作成し /tmp/counter.wat
という名前で保存する。
(module
(import "host" "next_id" (global $next_id (mut i64)))
(import "host" "FIRST_ID" (global $FIRST_ID i64))
(global $id (mut i64) (i64.const -1))
(func $init
;; Initialize the
global.get $next_id
global.set $id
;; Increment $next_id
global.get $next_id
i64.const 1
i64.add
global.set $next_id)
(func (export "get_id") (result i64)
global.get $id)
(func (export "is_first") (result i32)
global.get $FIRST_ID
global.get $id
i64.eq)
(start $init))
上記は WebAssembly Text Format という wasm ファイルのソースコードだ。
多くの読者に内容を何となく理解してもらうには、これが良いと思って選択した。
上記コードは大まかに以下の事を行っている。
(以下で i64
とは「符号付 64 bit 整数」、 i32
とは「符号付 32 bit 整数」の事である。)
- 外部プログラムから "host.next_id" というグローバル変数を import。このグローバル変数は可変の
i64
型である。この WebAssembly 内部からは$next_id
という名前でアクセス可能にしている。 - 外部プログラムから "host.FIRST_ID" というグローバル変数を import。このグローバル変数は不変の
i64
型である。この WebAssembly 内部からは$FIRST_ID
という名前でアクセス可能にしている。 - グローバル変数を宣言。このグローバル変数は可変の
i64
型であり、-1
で初期化される。この WebAssembly 内部からは$id
という名前でアクセス可能にしている。 - 関数を定義。この関数は引数をとらず、戻り値も無い。この WebAssembly 内部からは
$init
という名前でアクセス可能にしている。 - 関数を定義。この関数は引数を取らず、
i64
型を 1 個返す。外部プログラムからget_id
という名前でアクセス可能にしている。 - 関数を定義。この関数は引数をとらず、
i32
型を 1 個返す。外部プログラムからis_first
という名前でアクセス可能にしている。 - インスタンス作成時に関数
$init
を実行する。
補足すると、WebAssembly には「Boolean 型」と言われる型は存在しない。 i32
型の 0
を False
の様に、 1
を True
の様に使用する。
関数 $init
は $id
に $next_id
の値を代入し、その後 $next_id
の値を 1
増加させる。 関数 get_id
は変数 $id
の値を返す。関数 is_first
は、 $id
が $FIRST_ID
と等しければ 1
を、異なれば 0
を返す。
この wat ファイルを以下の様に wat2wasm コマンドでコンパイルすると /tmp/counter.wasm
というバイナリファイルが出来る。
$ wat2wasm -o /tmp/counter.wasm /tmp/counter.wat
$ ls /tmp/counter.wasm
/tmp/counter.wasm
Vanilla JS から実行
説明の都合上、本章は JavaScript に興味の無い人でも斜めに読んで欲しい。
筆者の JavaScript 環境
- nodejs v18.16.1
Vanilla JS のコード
nodejs から WebAssembly を実行するコードは、例えば下記の様になる。
// Build Module
const fs = require('fs');
const wasm = fs.readFileSync('/tmp/counter.wasm');
const mod = new WebAssembly.Module(wasm);
// Build Instance
const importObject = {
host: {
next_id: new WebAssembly.Global({value: "i64", mutable: true}, BigInt(0)),
FIRST_ID: new WebAssembly.Global({value: "i64", mutable: false}, BigInt(0)),
}
};
const counter0 = new WebAssembly.Instance(mod, importObject);
const counter1 = new WebAssembly.Instance(mod, importObject);
// Show the id of counter0 and counter1.
console.log(`The id of counter0 is ${counter0.exports.get_id()}`);
console.log(`The id of counter1 is ${counter1.exports.get_id()}`);
// Show the first instance
if (counter0.exports.is_first()) {
console.log("counter0 is the first instance.");
}
if (counter1.exports.is_first()) {
console.log("counter1 is the first instance.");
}
上記のファイルを call_wasm.js
という名前で保存し実行すると、下記の様な結果を得る。
$ node call_wasm.js
The id of counter0 is 0
The id of counter1 is 1
counter0 is the first instance.
JavaScript のコードを少しずつ読んでみる。
(以下の 太字リンク は JavaScript のクラス名(コンストラクター名)である事を示す。リンク先は MDN Web Docs 。)
最初に以下を読んでみる。
// Build Module
const fs = require('fs');
const wasm = fs.readFileSync('/tmp/counter.wasm');
const mod = new WebAssembly.Module(wasm);
ここまでは 第1回 と wasm ファイルのパス以外同じだ。
続いて以下の行を読んでみる。
// Build Instance
const importObject = {
host: {
next_id: new WebAssembly.Global({value: "i64", mutable: true}, BigInt(0)),
FIRST_ID: new WebAssembly.Global({value: "i64", mutable: false}, BigInt(0)),
}
};
const counter0 = new WebAssembly.Instance(mod, importObject);
const counter1 = new WebAssembly.Instance(mod, importObject);
ここも importObject
に Global を持たせている事以外は 第2回 と同じだ。
私達の作成した wasm ファイルは next_id
, FIRST_ID
, id
という 3 個のグローバル変数を持っている。
ここでは counter0
と counter1
の 2 個のインスタンスを作成しているが、どちらも同じ importObject
を使用しているので、 next_id
と FIRST_ID
は共通の物を参照する。( id
は、それぞれ別の変数となる。)
そのため counter0
の初期化時に next_id
を更新するが、その更新は counter1
にも反映される。
例えばこの部分を下記の様なコードにすると、 counter0
による next_id
の更新が counter1
に反映されず、結果として両者の id
は 0
となるだろう。
// Build Instance
const importObject = {
host: {
next_id: new WebAssembly.Global({value: "i64", mutable: true}, BigInt(0)),
FIRST_ID: new WebAssembly.Global({value: "i64", mutable: false}, BigInt(0)),
}
};
const counter0 = new WebAssembly.Instance(mod, importObject);
importObject.host.next_id = new WebAssembly.Global({value: "i64", mutable: true}, BigInt(0));
const counter1 = new WebAssembly.Instance(mod, importObject1);
次に以下の行を読んでみる。
// Show the id of counter0 and counter1.
console.log(`The id of counter0 is ${counter0.exports.get_id()}`);
console.log(`The id of counter1 is ${counter1.exports.get_id()}`);
// Show the first instance
if (counter0.exports.is_first()) {
console.log("counter0 is the first instance.");
}
if (counter1.exports.is_first()) {
console.log("counter1 is the first instance.");
}
ここは 第1回 と同様の方法で関数 get_id
と is_first
を実行している。
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_person というプロジェクトを作成する。
$ cargo new call_counter
$ cd call_counter
Cargo.toml の [dependencies]
セクションに wasmtime をの設定を加える。
筆者の環境では Cargo.toml は下記の様になった。
[package]
name = "call_person"
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 wasm = fs::read("/tmp/counter.wasm").unwrap();
let module = Module::new(&engine, wasm)?;
// Build `Store`
let mut store = Store::new(&engine, ());
// Create Global `next_id` and `first_id`
let next_id_ty = GlobalType::new(ValType::I64, Mutability::Var);
let next_id = Global::new(&mut store, next_id_ty, Val::I64(0))?;
let first_id_ty = GlobalType::new(ValType::I64, Mutability::Const);
let first_id = Global::new(&mut store, first_id_ty, Val::I64(0))?;
// Build `Instance`. (`Linker` is builder class.)
let mut linker = Linker::new(&engine);
linker.define(&store, "host", "next_id", next_id)?;
linker.define(&store, "host", "FIRST_ID", first_id)?;
let counter0 = linker.instantiate(&mut store, &module)?;
let counter1 = linker.instantiate(&mut store, &module)?;
// Show the `id` of `counter0` and `counter1`.
let get_id = counter0.get_typed_func::<(), i64>(&mut store, "get_id")?;
println!("The id of counter0 is {}", &get_id.call(&mut store, ())?);
let get_id = counter1.get_typed_func::<(), i64>(&mut store, "get_id")?;
println!("The id of counter1 is {}", &get_id.call(&mut store, ())?);
// Show the first instance.
let is_first = counter0.get_typed_func::<(), i32>(&mut store, "is_first")?;
if is_first.call(&mut store, ())? == 1 {
println!("counter0 is the first instance.");
}
let is_first = counter1.get_typed_func::<(), i32>(&mut store, "is_first")?;
if is_first.call(&mut store, ())? == 1 {
println!("counter1 is the first instance.");
}
Ok(())
}
上記の内容で src/main.rs
を上書きして実行すると、下記の様な結果を得る。
$ cargo run
Compiling call_counter v0.1.0 (/home/wbcchsyn/tmp/class_variable/call_counter)
Finished dev [unoptimized + debuginfo] target(s) in 3.44s
Running `target/debug/call_counter`
The id of counter0 is 0
The id of counter1 is 1
counter0 is the first instance.
上記の Rust コードを少しずつ解説していく。
以下で 太字のリンク は wasmtime で定義されたクラスである事を示す。リンク先は Rust 版 wasmtime の公式ドキュメント 。
まずはコードの全体像を把握しよう。
use std::fs;
use wasmtime::*;
fn main() -> wasmtime::Result<()> {
...
}
見ての通り、本コードは main 関数が定義してあるだけである。
以下、main 関数の中身を上から少しずつ読んでみる
まずは以下の行からだ。
// Build `Engine`.
let engine = Engine::default();
// Build `Module`.
let wasm = fs::read("/tmp/counter.wasm").unwrap();
let module = Module::new(&engine, wasm)?;
// Build `Store`.
let mut store = Store::new(&engine, ());
ここは 第1回 と wasm ファイルのパス以外同じだ。
続いて以下の行を読んでみる。
// Create Global `next_id` and `first_id`
let next_id_ty = GlobalType::new(ValType::I64, Mutability::Var);
let next_id = Global::new(&mut store, next_id_ty, Val::I64(0))?;
let first_id_ty = GlobalType::new(ValType::I64, Mutability::Const);
let first_id = Global::new(&mut store, first_id_ty, Val::I64(0))?;
// Build `Instance`. (`Linker` is builder class.)
let mut linker = Linker::new(&engine);
linker.define(&store, "host", "next_id", next_id)?;
linker.define(&store, "host", "FIRST_ID", first_id)?;
let counter0 = linker.instantiate(&mut store, &module)?;
let counter1 = linker.instantiate(&mut store, &module)?;
ここも 第2回 とほとんど同じだ。
違いは linker.define
の引数に Global を渡している事だけである。
(first_id
を大文字では無く小文字にした事に大きな理由は無い。大文字にすると Rust のコンパイラーが警告を出したので小文字にした。)
JavaScript の所でも説明したが、 counter0
と counter1
は同じ Global を参照している。そのため counter0
の初期化時に next_id
の値を更新するが、その更新は counter1
にも反映される。
ところで、ここで Store に関する制限が有る。
Global と Instance の各インスタンスは many-to-many の関係で紐づいている。
それとは別に、 Store と Global や Instance は one-to-many の関係で紐づいている。
ここで関連する(紐づいている) Global や Instance の各インスタンスは、全て同じ Store に紐づいている必要がある。
つまり、今回作成した next_id
, first_id
, counter0
, counter1
は、同一の Store に紐づいている必要が有る。
続いて以下の行を読んでみる。
// Show the `id` of `counter0` and `counter1`.
let get_id = counter0.get_typed_func::<(), i64>(&mut store, "get_id")?;
println!("The id of counter0 is {}", &get_id.call(&mut store, ())?);
let get_id = counter1.get_typed_func::<(), i64>(&mut store, "get_id")?;
println!("The id of counter1 is {}", &get_id.call(&mut store, ())?);
// Show the first instance.
let is_first = counter0.get_typed_func::<(), i32>(&mut store, "is_first")?;
if is_first.call(&mut store, ())? == 1 {
println!("counter0 is the first instance.");
}
let is_first = counter1.get_typed_func::<(), i32>(&mut store, "is_first")?;
if is_first.call(&mut store, ())? == 1 {
println!("counter1 is the first instance.");
}
ここでは 第1回 と同じ方法で関数 get_id
と is_first
を実行している。
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, Global, GlobalType, Linker, Module, Store, Val,
ValType)
# Build Engine.
engine = Engine()
# Build Module.
wasm = open('/tmp/counter.wasm', 'rb').read()
module = Module(engine, wasm)
# Build Store.
store = Store(engine)
# Create Global next_id and FIRST_ID.
next_id_ty = GlobalType(ValType.i64(), True)
next_id = Global(store, next_id_ty, Val.i64(0))
FIRST_ID_ty = GlobalType(ValType.i64(), False)
FIRST_ID = Global(store, FIRST_ID_ty, Val.i64(0))
# Build Instance. (Linker is builder class.)
linker = Linker(engine)
linker.define(store, "host", "next_id", next_id)
linker.define(store, "host", "FIRST_ID", FIRST_ID)
counter0 = linker.instantiate(store, module)
counter1 = linker.instantiate(store, module)
# Show the id of counter0 and counter1.
print("The id of counter0 is %d" % counter0.exports(store)['get_id'](store))
print("The id of counter1 is %d" % counter1.exports(store)['get_id'](store))
# Show the first instance.
if counter0.exports(store)['is_first'](store):
print("counter0 is the first instance.")
if counter1.exports(store)['is_first'](store):
print("counter1 is the first instance.")
上記の内容を call_wasm.py というファイル名で保存すると、実行結果は以下の様になる。
$ python call_wasm.py
The id of counter0 is 0
The id of counter1 is 1
counter0 is the first instance.
上記の Python コードを少しずつ解説していく。
以下では 太字のリンク は wasmtime で定義されたクラスである事を示す。リンク先は Python 版 wasmtime の公式ドキュメント である。
最初に以下の行を読んでみる。
from wasmtime import (Engine, Global, GlobalType, Linker, Module, Store, Val,
ValType)
# Build Engine.
engine = Engine()
# Build Module.
wasm = open('/tmp/counter.wasm', 'rb').read()
module = Module(engine, wasm)
# Build Store.
store = Store(engine)
ここでは 第1回 と wasm ファイルのパス以外は同じだ。
続いて以下の行を読んでみる。
# Create Global next_id and FIRST_ID.
next_id_ty = GlobalType(ValType.i64(), True)
next_id = Global(store, next_id_ty, Val.i64(0))
FIRST_ID_ty = GlobalType(ValType.i64(), False)
FIRST_ID = Global(store, FIRST_ID_ty, Val.i64(0))
# Build Instance. (Linker is builder class.)
linker = Linker(engine)
linker.define(store, "host", "next_id", next_id)
linker.define(store, "host", "FIRST_ID", FIRST_ID)
counter0 = linker.instantiate(store, module)
counter1 = linker.instantiate(store, module)
ここも 第2回 とほとんど同じだ。
違いは linker.define()
の引数に Global を渡している事だけである。
JavaScript の所でも説明したが、 counter0
と counter1
は同じ Global を参照している。そのため counter0
の初期化時に next_id
の値を更新するが、その更新は counter1
にも反映される。
ところで、ここで Store に関する制限が有る。
Global と Instance の各インスタンスは many-to-many の関係で紐づいている。
それとは別に、 Store と Global や Instance は one-to-many の関係で紐づいている。
ここで関連する(紐づいている) Global や Instance の各インスタンスは、全て同じ Store に紐づいている必要がある。
つまり、今回作成した next_id
, FIRST_ID
, counter0
, counter1
は、同一の Store に紐づいている必要が有る。
続いて以下の行を読んでみる。
# Show the id of counter0 and counter1.
print("The id of counter0 is %d" % counter0.exports(store)['get_id'](store))
print("The id of counter1 is %d" % counter1.exports(store)['get_id'](store))
# Show the first instance.
if counter0.exports(store)['is_first'](store):
print("counter0 is the first instance.")
if counter1.exports(store)['is_first'](store):
print("counter1 is the first instance.")
ここでは 第1回 と同じ方法で関数 get_id
と is_first
を実行している。
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
wasm = open('/tmp/counter.wasm', 'rb').read
mod = Wasmtime::Module.new(engine, wasm)
# Build Store
store = Wasmtime::Store.new(engine)
# Create Global next_id and FIRST_ID.
next_id = Wasmtime::Global.var(store, :i64, 0)
FIRST_ID = Wasmtime::Global.const(store, :i64, 0)
# Build instance (Linker is builder Class)
linker = Wasmtime::Linker.new(engine)
linker.define(store, "host", "next_id", next_id)
linker.define(store, "host", "FIRST_ID", FIRST_ID)
counter0 = linker.instantiate(store, mod)
counter1 = linker.instantiate(store, mod)
# Show the id of counter0 and counter1.
puts("The id of counter0 is %d" % counter0.export('get_id').to_func.call)
puts("The id of counter1 is %d" % counter1.export('get_id').to_func.call)
# Show the first instance.
if counter0.export('is_first').to_func.call == 1 then
puts("counter0 is the first instance.")
end
if counter1.export('is_first').to_func.call == 1 then
puts("counter1 is the first instance.")
end
上記の内容を call_wasm.rb というファイル名で保存すると、実行結果は以下の様になる。
$ ruby call_wasm.rb
The id of counter0 is 0
The id of counter1 is 1
counter0 is the first instance.
上記の Ruby コードを少しずつ解説していく。
以下では 太字のリンク は wasmtime で定義されたクラスである事を示す。リンク先は Ruby 版 wasmtime の公式ドキュメント である。
最初に以下の行を読んでみる。
require 'wasmtime'
# Build Engine
engine = Wasmtime::Engine.new
# Build Module
wasm = open('/tmp/counter.wasm', 'rb').read
mod = Wasmtime::Module.new(engine, wasm)
# Build Store
store = Wasmtime::Store.new(engine)
ここでは 第1回 と wasm ファイルのパス以外は同じだ。
続いて以下の行を読んでみる。
# Create Global next_id and FIRST_ID.
next_id = Wasmtime::Global.var(store, :i64, 0)
FIRST_ID = Wasmtime::Global.const(store, :i64, 0)
# Build instance (Linker is builder Class)
linker = Wasmtime::Linker.new(engine)
linker.define(store, "host", "next_id", next_id)
linker.define(store, "host", "FIRST_ID", FIRST_ID)
counter0 = linker.instantiate(store, mod)
counter1 = linker.instantiate(store, mod)
ここも 第2回 とほとんど同じだ。
違いは linker.define
の引数に Global を渡している事だけである。
JavaScript の所でも説明したが、 counter0
と counter1
は同じ Global を参照している。そのため counter0
の初期化時に next_id
の値を更新するが、その更新は counter1
にも反映される。
ところで、ここで Store に関する制限が有る。
Global と Instance の各インスタンスは many-to-many の関係で紐づいている。
それとは別に、 Store と Global や Instance は one-to-many の関係で紐づいている。
ここで関連する(紐づいている) Global や Instance の各インスタンスは、全て同じ Store に紐づいている必要がある。
つまり、今回作成した next_id
, FIRST_ID
, counter0
, counter1
は、同一の Store に紐づいている必要が有る。
続いて以下の行を読んでみる。
# Show the id of counter0 and counter1.
puts("The id of counter0 is %d" % counter0.export('get_id').to_func.call)
puts("The id of counter1 is %d" % counter1.export('get_id').to_func.call)
# Show the first instance.
if counter0.export('is_first').to_func.call == 1 then
puts("counter0 is the first instance.")
end
if counter1.export('is_first').to_func.call == 1 then
puts("counter1 is the first instance.")
end
ここでは 第1回 と同じ方法で関数 get_id
と is_first
を実行している。
まとめ
今回は 2 個以上の wasm ファイルのインスタンス間でグローバル変数を共有する方法について説明した。
WebAssembly はグローバル変数の他にも関数、抽象ポインター、線形メモリを import できる。
つまり、これらの値は複数のインスタンス間で共有できる。
( 第2回 では関数の import を、 第4回 では線形メモリの import を紹介した。)
これらを上手く活用すれば「メタプログラミング」と呼ばれる様な強力なコーディングを行う事が可能だ。
フレームワークの作成などで便利だろう。
一方で import や export を多用すると、それは WebAssembly と外部プログラムが密結合になってしまう。
一般的に平凡でシンプルな方法で実装できるなら、その方が良いと筆者は個人的には考えている。
「知識をひけらかす」事を目的にコーディングすると、悪い結果につながるかもしれない。
次回以降はメタプログラミング的な方法を紹介した後で、 第4回 の続きとして線形メモリの実践的な使い方を紹介する予定だ。