1
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?

More than 1 year has passed since last update.

開発ツールに頼らず 様々な言語から WebAssembly(第2回)

Posted at

WebAssembly は多くのプログラム言語からライブラリの様に呼び出す事が出来る。
WebAssembly のコードを書く方法は多くの記事で書かれているが、その使い方はフレームワーク等のツールに頼っている事が多い様だ。
本記事では、そのような便利ツールに出来るだけ頼らず JavaScript, Rust, Python, Ruby から WebAssembly を実行する方法を記載する。

WebAssembly は新しい技術である。
目先の最先端ツールに飛びつくのもよいが、その基礎を学んで長く使える知識を身に着けないか?

本記事はシリーズの第2回である。シリーズ記事の一覧は 第1回#シリーズ記事一覧 に載せている。シリーズの記事を追加する時は 第1回 の一覧を更新して通知を行うので興味の有る人は 第1回 をストックして欲しい。

本記事の概要と過去記事の復習

第1回 では、最小限の WebAsseembly のコードとして「足し算を行う」関数を作成し、そのコードを実行する外部プログラムを JavaScript, Rust, Python, Ruby で作成した。

本記事では実行する WebAssembly のコードを少し複雑にする。
具体的には、外部プログラムの関数を import する 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/import.wat という名前で保存する。

(module
  (import "host" "display" (func $inner_display (param i64 i64 i64)))

  (func $inner_add (export "add") (param $a i64) (param $b i64) (result i64)
    local.get $a
    local.get $b
    i64.add)

  (func (export "add_and_display") (param $a i64) (param $b i64)
    (local $answer i64)

    ;; Call $inner_add
    local.get $a
    local.get $b
    call $inner_add

    ;; Call $inner_display
    local.set $answer
    local.get $a
    local.get $b
    local.get $answer
    call $inner_display))

上記は WebAssembly Text Format という wasm ファイルのソースコードだ。
多くの読者に内容を何となく理解してもらうには、これが良いと思って選択した。

上記のコードは以下の 3 個の事を行っている。
なお、下記の i64 とは符号付 64 bit 整数の事である。

  • 外部プログラムから host.display という物を import する。これは関数であり、引数に i64 を 3 個取り、戻り値は無い。この関数に、この wasm ファイル内部から $inner_display という名前でアクセスできるようにする。
  • 関数を定義する。これは引数に i64 を 2 個取り、戻り値は i64 1 個である。この関数は外部から add という名前でアクセス出来るように公開し、この wasm ファイル内部から $inner_add という名前でアクセス出来るようにする。
  • 関数を定義する。これは引数に i64 を 2 個取り、戻り値は無い。この関数は外部から add_and_display という名前でアクセス出来るようにする。(wasm ファイル内部からアクセスするための名前は特に付けていない。ただし内部からアクセスするための抜け道は存在する。)

外部の名前と wasm ファイル内部からアクセスする名前を変更したのは、説明の都合上その方が分かりやすいと思ったからだ。(普通は同じ名前にするだろう。)

関数の名前やコメントで分かると思うが、 add ($inner_add) は引数を足し算してその結果を返す。
add_and_display$inner_add に引数をそのまま渡して実行した後で、引数とその実行結果を $inner_display に渡して実行する。

この wat ファイルを以下の様に wat2wasm コマンドでコンパイルすると /tmp/import.wasm というバイナリファイルが出来る。

$ wat2wasm -o /tmp/import.wasm /tmp/import.wat
$ ls /tmp/import.wasm
/tmp/import.wasm

Vanilla JS から実行

説明の都合上、本章は JavaScript に興味の無い人でも斜めに読んで欲しい。

筆者の JavaScript 環境

  • nodejs v18.16.1

Vanilla JS のコード

nodejs から WebAssembly を実行するコードは、例えば下記の様になる。

// Build Module
const fs = require('fs');
const wasm = fs.readFileSync('/tmp/import.wasm');
const mod = new WebAssembly.Module(wasm);

// Build Instance
const importObject = {
    host: {
        display: (a, b, answer) => {
            console.log(`${a} + ${b} = ${answer}`)
        },
    }
};
const instance = new WebAssembly.Instance(mod, importObject);

// Call function add
const add = instance.exports.add;
const answer = add(BigInt(6), BigInt(12));
console.log("Call function add: ", `6 + 12 = ${answer}`);

// Call function add_and_display
const add_and_display = instance.exports.add_and_display;
console.log("Call function add_and_display");
add_and_display(BigInt(5), BigInt(3));

上記のファイルを call_wasm.js という名前で保存して実行すると、下記の様な結果を得る。

$ node call_wasm.js
Call function add:  6 + 12 = 18
Call function add_and_display
5 + 3 = 8

JavaScript のコードは 第1回 とほとんど同じだ。
最初に以下の行を読んでみる。

// Build Module
const fs = require('fs');
const wasm = fs.readFileSync('/tmp/import.wasm');
const mod = new WebAssembly.Module(wasm);

上記において、wasm ファイルのパス以外は 第1回 と全く同じである。

続いて以下の行を読んでみる。

// Build Instance
const importObject = {
    host: {
        display: (a, b, answer) => {
            console.log(`${a} + ${b} = ${answer}`)
        },
    }
};
const instance = new WebAssembly.Instance(mod, importObject);

ここも 第1回 とほとんど同じだが、 importObject が空では無い。

私達の作成した wasm ファイルでは、host.display という名前で引数に i64 を 3 個取る関数を import していた。
なので WebAssembly.Instance の 2 番目の引数に .host.display で上記の様な関数を取得できるオブジェクトを渡している。
第1回 では何も import しなかったので importObject は空 {} だった。)

wasm ファイルの i64 は JavaScript では BigInt に対応するので、 importObject.host.display の引数は 3 個とも BigInt が渡されると想定される。

続いて以下の行を読んでみる。

// Call function add
const add = instance.exports.add;
const answer = add(BigInt(6), BigInt(12));
console.log("Call function add: ", `6 + 12 = ${answer}`);

ここでは 第1回 と全く同じ方法で関数 add を実行している。
このコードは add_and_display 実行との比較のために記述した。

続いて以下の行を読んでみる。

// Call function add_and_display
const add_and_display = instance.exports.add_and_display;
console.log("Call function add_and_display");
add_and_display(BigInt(5), BigInt(3));

ここでは関数 add と同様の方法で関数 add_and_display を実行している。
違いは add_and_display が内部で importObject.host.display を実行しているので、JavaScript で何もしなくとも実行時に 5 + 3 = 8 と表示されることだ。

wasmtime から実行

本章は wasmtime を使うにあたって Rust, Python, Ruby で共通の説明である。
太字リンク は wasmtime で定義されたクラス名である事を示し、「作成」とはそのクラスのインスタンス作成の事である。リンク先は便宜上 Rust 用の公式ドキュメントを指している。)

JavaScript から wasm ファイルを実行する場合は 第1回 とほとんど同じ手順だったが、wasmtime から実行する場合は以下の 2 点に気を付ける必要が有る。

  1. 外部プログラムの関数を wasm ファイルから import するためには、事前に外部プログラムの関数を Func でラップする必要がある
  2. Instance をビルドする際にビルダークラスの Linker を使用すると便利

以降の章では 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_import というプロジェクトを作成する。

$ cargo new call_import
$ cd call_import

Cargo.toml の [dependencies] セクションに wasmtime をの設定を加える。
筆者の環境では Cargo.toml は下記の様になった。

[package]
name = "call_import"
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/import.wasm").unwrap();
    let module = Module::new(&engine, wasm)?;

    // Build `Store`
    let mut store = Store::new(&engine, ());

    // Create function `display`
    let display = Func::wrap(&mut store, |a: i64, b: i64, answer: i64| {
        println!("{} + {} = {}", &a, &b, &answer)
    });

    // Build `Instance`. (`Linker` is builder class.)
    let mut linker = Linker::new(&engine);
    linker.define(&store, "host", "display", display)?;
    let instance = linker.instantiate(&mut store, &module)?;

    // Call function `add`
    let add = instance.get_typed_func::<(i64, i64), i64>(&mut store, "add")?;
    let answer = add.call(&mut store, (6, 12))?;
    println!("Call function add: 6 + 12 = {}", &answer);

    // Call function `add_and_display`
    let add_and_display =
        instance.get_typed_func::<(i64, i64), ()>(&mut store, "add_and_display")?;
    println!("Call function add_and_display");
    add_and_display.call(&mut store, (5, 3))?;

    Ok(())
}

上記の内容で src/main.rs を上書きして実行すると、実行結果は以下の様になる

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/call_import`
Call function add: 6 + 12 = 18
Call function add_and_display
5 + 3 = 8

上記の Rust コードを少しずつ解説していく。
まずはコードの全体像を把握しよう。

use std::fs;
use wasmtime::*;

fn main() -> wasmtime::Result<()> {
...
}

見ての通り、本コードは main 関数が定義してあるだけである。
以下、main 関数の中身を上から少しずつ読んでみる。

最初に少しまとめて読んでみよう。

    // Build `Engine`
    let engine = Engine::default();

    // Build `Module`
    let wasm = fs::read("/tmp/import.wasm").unwrap();
    let module = Module::new(&engine, wasm)?;

    // Build `Store`
    let mut store = Store::new(&engine, ());

上記の部分は wasm ファイルのパス以外 第1回 と全く同じである。

続いて以下の行を読んでみる。

    // Create function `display`
    let display = Func::wrap(&mut store, |a: i64, b: i64, answer: i64| {
        println!("{} + {} = {}", &a, &b, &answer)
    });

ここでは wasm ファイルで import するために Func のインスタンスを作成している。
JavaScript と異なり wasmtime では外部プログラム(今回は Rust)の関数をそのまま wasm ファイルに渡す事は出来ない。

続いて以下の行を読んでみる。

    // Build `Instance`. (`Linker` is builder class.)
    let mut linker = Linker::new(&engine);
    linker.define(&store, "host", "display", display)?;
    let instance = linker.instantiate(&mut store, &module)?;

第1回 は wasm ファイルが何も import しなかったので、インスタンスの作成は
let instance = Instance::new(&mut store, &module, &[])?;
という一行で済んだ。(import するオブジェクトとして &[] を渡している。)

今回も同様の方法で Instance を作成しても良いのだが、import するオブジェクトを作成する事が少々面倒だ。
wasmtime のコア部分は静的型付け言語の Rust で実装されている。JavaScript の場合は .host.display という名前のプロパティを持つオブジェクトを簡単に作成できるが、多くのプログラム言語に対応する事を考慮の上で静的型付け言語の Rust 経由して連想配列の入れ子の様を渡すのは少々面倒である。)

そこでビルダーである Linker を使って Instance を作成する。
Linke ではメソッド define を繰り返し使う事によって(関数だけでなく)複数のアイテムを wasm ファイルで import できる。

続いて以下の行を読んでみる。

    // Call function `add`
    let add = instance.get_typed_func::<(i64, i64), i64>(&mut store, "add")?;
    let answer = add.call(&mut store, (6, 12))?;
    println!("Call function add: 6 + 12 = {}", &answer);

ここでは 第1回 と全く同じ方法で関数 add を実行している。
このコードは add_and_display との比較のために記述した。

続いて以下の行を読んでみる。

    // Call function `add_and_display`
    let add_and_display =
        instance.get_typed_func::<(i64, i64), ()>(&mut store, "add_and_display")?;
    println!("Call function add_and_display");
    add_and_display.call(&mut store, (5, 3))?;

ここでは関数 add と同様の方法で関数 add_and_display を実行している。
違いは add_and_display が内部で host.display を実行しているので、Rust で何もしなくとも実行時に 5 + 3 = 8 と表示されることだ。

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, Func, FuncType, Linker, Module, Store, ValType

# Build Engine.
engine = Engine()

# Build Module.
wasm = open('/tmp/import.wasm', 'rb').read()
module = Module(engine, wasm)

# Build Store.
store = Store(engine)

# Define function display
i64 = ValType.i64()
display = Func(store, FuncType([i64, i64, i64], []), lambda a, b, answer: print(
    "%d + %d = %d" % (a, b, answer)))

# Build Instance. (Linker is builder class.)
linker = Linker(engine)
linker.define(store, "host", "display", display)
instance = linker.instantiate(store, module)

# Call function add
add = instance.exports(store)['add']
answer = add(store, 6, 12)
print("Call function add: 6 + 12 = %d" % answer)

# Call function add_and_display
add_and_display = instance.exports(store)['add_and_display']
print("Call function add_and_display")
add_and_display(store, 5, 3)

上記の内容を call_wasm.py というファイル名で保存すると、実行結果は以下の様になる。

$ python call_wasm.py
Call function add: 6 + 12 = 18
Call function add_and_display
5 + 3 = 8

上記の Python コードを少しずつ解説していく。
以下では 太字のリンク は wasmtime で定義されたクラスである事を示す。リンク先は Python 版 wasmtime の公式ドキュメント である。

最初に少しまとめて読んでみよう。

# Build Engine.
engine = Engine()

# Build Module.
wasm = open('/tmp/import.wasm', 'rb').read()
module = Module(engine, wasm)

# Build Store.
store = Store(engine)

上記の部分は wasm ファイルのパス以外 第1回 と全く同じである。

続いて以下の行を読んでみる。

# Define function display
i64 = ValType.i64()
display = Func(store, FuncType([i64, i64, i64], []), lambda a, b, answer: print(
    "%d + %d = %d" % (a, b, answer)))

ここでは wasm ファイルで import するために Func のインスタンスを作成している。
JavaScript と異なり wasmtime では外部プログラム(今回は Python)の関数をそのまま wasm ファイルに渡す事は出来ない。

続いて以下の行を読んでみる。

# Build Instance. (Linker is builder class.)
linker = Linker(engine)
linker.define(store, "host", "display", display)
instance = linker.instantiate(store, module)

第1回 は wasm ファイルが何も import しなかったので、インスタンスの作成は
instance = Instance(store, module, ())
という一行で済んだ。(import するオブジェクトとして () を渡している。)

今回も同様の方法で Instance を作成しても良いのだが、import するオブジェクトを作成する事が少々面倒だ。
wasmtime のコア部分は静的型付け言語の Rust で実装されている。
JavaScript の場合は .host.display という名前のプロパティを持つオブジェクトを簡単に作成できるが、多くのプログラム言語に対応する事を考慮の上で静的型付け言語の Rust 経由して連想配列の入れ子の様を渡すのは少々面倒である。)

そこでビルダーである Linker を使って Instance を作成する。
Linke ではメソッド define を繰り返し使う事によって(関数だけでなく)複数のアイテムを wasm ファイルで import できる。

蛇足だが Python 版の Linker では Func の作成から define までを 1 ステップで行う define_func というメソッドも存在する。

続いて以下の行を読んでみる。

# Call function add
add = instance.exports(store)['add']
answer = add(store, 6, 12)
print("Call function add: 6 + 12 = %d" % answer)

ここでは 第1回 と全く同じ方法で関数 add を実行している。
このコードは add_and_display との比較のために記述した。

続いて以下の行を読んでみる。

# Call function add_and_display
add_and_display = instance.exports(store)['add_and_display']
print("Call function add_and_display")
add_and_display(store, 5, 3)

ここでは関数 add と同様の方法で関数 add_and_display を実行している。
違いは add_and_display が内部で host.display を実行しているので、Python で何もしなくとも実行時に 5 + 3 = 8 と表示されることだ。

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/import.wasm', 'rb').read
mod = Wasmtime::Module.new(engine, wasm)

# Build Store
store = Wasmtime::Store.new(engine)

# Define function display
display = Wasmtime::Func.new(store, [:i64, :i64, :i64], []) { |_caller, a, b, answer|
    puts("%d + %d = %d" % [a, b, answer])
}

# Build instance (Linker is builder Class)
linker = Wasmtime::Linker.new(engine)
linker.define(store, "host", "display", display)
instance = linker.instantiate(store, mod)

# Call function add
add = instance.export('add').to_func
answer = add.call(6, 12)
puts("Call function add: 6 + 12 = %d" % answer)

# Call function add_and_display
add_and_display = instance.export("add_and_display").to_func
puts("Call function add_and_display")
add_and_display.call(5, 3)                      

上記の内容を call_wasm.rb というファイル名で保存すると、実行結果は以下の様になる。

Call function add: 6 + 12 = 18
Call function add_and_display
5 + 3 = 8

上記の Ruby コードを少しずつ解説していく。
以下では 太字のリンク は wasmtime で定義されたクラスである事を示す。リンク先は Ruby 版 wasmtime の公式ドキュメント である。

最初に少しまとめて読んでみよう。

# Build Engine
engine = Wasmtime::Engine.new

# Build Module
wasm = open('/tmp/import.wasm', 'rb').read
mod = Wasmtime::Module.new(engine, wasm)

# Build Store
store = Wasmtime::Store.new(engine)

上記の部分は wasm ファイルのパス以外 第1回 と全く同じである。

続いて以下の行を読んでみる。

# Define function display
display = Wasmtime::Func.new(store, [:i64, :i64, :i64], []) { |_caller, a, b, answer|
    puts("%d + %d = %d" % [a, b, answer])
}

ここでは wasm ファイルで import するために Func のインスタンスを作成している。
JavaScript と異なり wasmtime では外部プログラム(今回は Ruby)の関数をそのまま wasm ファイルに渡す事は出来ない。

続いて以下の行を読んでみる。

# Build instance (Linker is builder Class)
linker = Wasmtime::Linker.new(engine)
linker.define(store, "host", "display", display)
instance = linker.instantiate(store, mod)

第1回 は wasm ファイルが何も import しなかったので、インスタンスの作成は
instance = Wasmtime::Instance.new(store, mod)
という一行で済んだ。(import するオブジェクトとしてデフォルト引数の [] を渡している。)

今回も同様の方法で Instance を作成しても良いのだが、import するオブジェクトを作成する事が少々面倒だ。
wasmtime のコア部分は静的型付け言語の Rust で実装されている。JavaScript の場合は .host.display という名前のプロパティを持つオブジェクトを簡単に作成できるが、多くのプログラム言語に対応する事を考慮の上で静的型付け言語の Rust 経由して連想配列の入れ子の様を渡すのは少々面倒である。)

そこでビルダーである Linker を使って Instance を作成する。
Linke ではメソッド define を繰り返し使う事によって(関数だけでなく)複数のアイテムを wasm ファイルで import できる。

蛇足だが Ruby 版の Linker では Func を作成し、渡すところまで 1 ステップで行う func_new というメソッドも存在する。

続いて以下の行を読んでみる。

# Call function add
add = instance.export('add').to_func
answer = add.call(6, 12)
puts("Call function add: 6 + 12 = %d" % answer)

ここでは 第1回 と全く同じ方法で関数 add を実行している。
このコードは add_and_display との比較のために記述した。

続いて以下の行を読んでみる。

# Call function add_and_display
add_and_display = instance.export("add_and_display").to_func
puts("Call function add_and_display")
add_and_display.call(5, 3)

ここでは関数 add と同様の方法で関数 add_and_display を実行している。
違いは add_and_display が内部で host.display を実行しているので、Rust で何もしなくとも実行時に 5 + 3 = 8 と表示されることだ。

ポエム

以前 別の記事 でも書いたが、実は筆者は wasm ファイルで import するのはあまり好きではない。

上記は rust-webpack という npm パッケージを用いて Rust で書いた WebAssembly のコードをブラウザー上の JavaScript から実行する記事である。実は rust-webpack のサンプル等で良く紹介されている方法はメモリリークする。(筆者は興味本位でライブラリの奥まで読んでいる最中に偶然気が付いてしまった。)

rust-webpack 中の人達はこのメモリリークに気が付いており色々と議論していたが、筆者が確認した時点では未解決だった。その議論を見て、筆者は「そもそも論としてプログラムの設計が悪い。wasm ファイルでは import を使用せずに Haskell 型の設計を採用した方が良いのではないか?」と思ったのだ。

最近、このシリーズ記事を書くにあたって wasmtime のドキュメントを読んでいると「Haskell 風の表現だな」と思う事が時々ある。wasmtime の中の人たち(≒ WebAssembly の中の人たち)も同じ事を考えているのでは無いかと想像してしまうのだが、筆者の妄想だろうか?

気になる人は上記の記事も読んでみて欲しい。

1
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
1
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?