LoginSignup
1
0

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

Last updated at Posted at 2023-07-09

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

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

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

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

過去記事では関数を定義した WebAssembly を外部プログラムから実行した。
しかし WebAssembly のコードクラスの様な物であり、外部プログラムでインスタンス化してから実行される。
今回は各インスタンス事に状態(プロパティ、インスタンス変数)を持たせたい。

本記事では age という i64 (符号付 64 bit 整数)のグローバル変数を持つ WebAssembly のコードを作成する。(WebAssembly の グローバル変数 とは「インスタンス内でグローバルな変数」という意味であり、実質的にはプロパティやインスタンス変数と呼ばれる物に相当する。)

イメージとしては下記の JavaScript のクラスの様な物を作る。

class Person {
    constructor() {
        this.age = -1;
    }

    get_age() {
        return this.age;
    }

    set_age(age) {
        this.age = age;
    }
}

本当は name というプロパティも加えたいのだが、WebAssembly で String 相当の物を扱うのは少し面倒だ。本記事では整数値の age だけを扱い、次回以降で name というプロパティを加えてみる。

その上で以下の様にに外部プログラムから作成した 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/person.wat という名前で保存する。

(module
    (global $inner_age (export "age") (mut i64) (i64.const -1))

    (func (export "get_age") (result i64)
        global.get $inner_age)

    (func (export "set_age") (param $age i64)
        local.get $age
        global.set $inner_age))

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

上記コードは大まかに以下の事を行っている。
(以下で i64 とは「符号付 64 bit 整数」の事である。)

  • 変更可能な i64 型のグローバル変数(インスタンス変数)を定義。この変数を age という名前で外部からアクセス可能にし、この wasm ファイル内部から $inner_age という名前でアクセス可能にしている。(外部と内部で名前を変更した理由は、説明上分かりやすくするためである。普通は同じにするだろう。)
  • 関数を定義。この関数を get_age という名前で外部からアクセス可能にしている。(内部からアクセスするための名前は付けていない。ただし内部からアクセスするための抜け道は存在する。)この関数は引数を取らず、 i64 1 個を返す。
  • 関数を定義。この関数を set_age という名前で外部からアクセス可能にしている。(内部からアクセスするための名前は付けていない。ただし内部からアクセスするための抜け道は存在する。)この関数は i64 引数を 1 個とり、戻り値は無い。

名前から理解できると思うが、 get_age, set_age はそれぞれ「ゲッター」、「セッター」と呼ばれる関数である。

プロパティの値へ直接アクセスする事を許すべきか、セッターやゲッターと呼ばれる関数を介して行うべきかは議論の余地が有る。今回は説明の為に両方可能にした。
値への直接アクセスを許さないならば (export "age") という部分を削除すればよいし、セッターやゲッターを使いたく無ければ該当する関数を削除してかまわない。

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

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

Vanilla JS から実行

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

筆者の JavaScript 環境

  • nodejs v18.16.1

Vanilla JS のコード

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

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

// Build Instances
const importObject = {};
const person1 = new WebAssembly.Instance(mod, importObject);
const person2 = new WebAssembly.Instance(mod, importObject);

// Set and get the age of person1 via the functions.
person1.exports.set_age(BigInt(37));
console.log(`The age of "person1" is ${person1.exports.get_age()}.`);

// Set and get the age of person2 via the global variable.
person2.exports.age.value = BigInt(24);
console.log(`The age of "person2" is ${person2.exports.age.value}.`);

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

node call_wasm.js
The age of "person1" is 37.
The age of "person2" is 24.

JavaScript のコードを少しずつ読んでみる。
(以下の 太字リンク は JavaScript のクラス名(コンストラクター名)である事を示す。リンク先は MDN Web Docs 。)

最初に以下を読んでみる。

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

// Build Instances
const importObject = {};
const person1 = new WebAssembly.Instance(mod, importObject);
const person2 = new WebAssembly.Instance(mod, importObject);

ここまでは 第1回 とほとんど同じだ。
違いは wasm ファイルのパスと、インスタンスを 2 個作っている事だけだ。

次に以下の行を読んでみる。

// Set and get the age of person1 via the functions.
person1.exports.set_age(BigInt(37));
console.log(`The age of "person1" is ${person1.exports.get_age()}.`);

ここでも 第1回 と同様の方法でインスタンスのメソッドを実行している。
WebAssembly の i64 は JavaScript の BigInt に相当するので注意しよう。

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

// Set and get the age of person2 via the global variable.
person2.exports.age.value = BigInt(24);
console.log(`The age of "person2" is ${person2.exports.age.value}.`);

ここは過去の記事とは少し違う。
公開された(public な)変数 age にアクセスしているのだが、外部プログラムである JavaScript 上で agei64 (つまり BigInt )では無く Global だ。
実際の値は age.value でアクセスする必要がある。

過去記事でも書いたが、WebAssembly は静的型付け言語である。
person1, person2 いずれの場合においても ageBigInt 以外の値を設定しようとすると TypeError が投げられるだろう。

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_person
$ cd call_person

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/person.wasm").unwrap();
    let module = Module::new(&engine, wasm)?;

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

    // Build `Instance`
    let person1 = Instance::new(&mut store, &module, &[])?;
    let person2 = Instance::new(&mut store, &module, &[])?;

    // Set and get the age of person1 via the method.
    {
        let get_age = person1.get_typed_func::<(), i64>(&mut store, "get_age")?;
        let set_age = person1.get_typed_func::<i64, ()>(&mut store, "set_age")?;
        set_age.call(&mut store, 37)?;
        println!(
            r#"The age of "person1" is {}."#,
            &(get_age.call(&mut store, ())?)
        )
    }

    // Set and get the age of person2 via the global variable.
    {
        let age = person2.get_global(&mut store, "age").unwrap();
        age.set(&mut store, Val::I64(24))?;
        println!(
            r#"The age of "person2" is {}."#,
            &(age.get(&mut store).unwrap_i64())
        );
    }

    Ok(())
}

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

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/call_person`
The age of "person1" is 37.
The age of "person2" is 24.

上記の 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/person.wasm").unwrap();
    let module = Module::new(&engine, wasm)?;

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

上記は 第1回 と全く同じコードなので説明を省略する。

続いて以下の行だ。

    // Build `Instance`
    let person1 = Instance::new(&mut store, &module, &[])?;
    let person2 = Instance::new(&mut store, &module, &[])?;

ここでも 第1回 と同じような方法で Instance を 2 個作成している。

普段から Rust のコードを書いている人は、変数の宣言時に let mut person1 の様に mut が付かない事に違和感を覚えるかもしれない。
筆者も最初は「これでは age の値を変更できないのではないか?」と思った。

しかし、実際にはこのコードで問題ない。

Rust 上で age の型は Global となっている。
そして person1person2 は、それぞれの age の値を保持する Global への参照しか持っていない。

person1age を変更すると、その値を保持する Global は変更される。しかし person1 内部の参照先のアドレスは変わらない。
なので person1person2mut を付ける必要は無い。

では person1person2 から参照される Global の実態はどこにあるのか?
それは Store が持っている。
この後解説するが、 age の値を変更する際に &mut store という参照を関数に渡している。

Store の実装を見ると、初期化を遅延している事が多い。
直感的には「この処理は更新しないだろう」と思うような場面でも実際には更新される可能性が有る。
Instance の使用時には、だいたい &mut store を引数に取る関数を実行する。

ところで let person2 = person1.clone(); の様な書き方をすると、 person1person2 は同じ Global を参照してしまう。( person2person1 の浅いコピーになってしまう。)

person1person2age が一度に変更されると困るので、ちゃんと person2Instance::new を使って初期化しよう。

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

    // Set and get the age of person1 via the method.
    {
        let get_age = person1.get_typed_func::<(), i64>(&mut store, "get_age")?;
        let set_age = person1.get_typed_func::<i64, ()>(&mut store, "set_age")?;
        set_age.call(&mut store, 37)?;
        println!(
            r#"The age of "person1" is {}."#,
            &(get_age.call(&mut store, ())?)
        )
    }

ここでは 第1回 と同じような方法で person1age を関数 get_age, set_age を使って読み書きしている。
特筆すべき点は無いだろう。

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

    // Set and get the age of person2 via the global variable.
    {
        let age = person2.get_global(&mut store, "age").unwrap();
        age.set(&mut store, Val::I64(24))?;
        println!(
            r#"The age of "person2" is {}."#,
            &(age.get(&mut store).unwrap_i64())
        );
    }

ここでは person2age をグローバル変数を介して読み書きしている。
person2.get_global(&mut store, "age")GlobalOption を返す。
age という Global を wasm ファイルが export していない場合、 None を返す。)

GlobalVal という enum のラッパーの様な構造をしている。
実際に値を読み書きする際は setget といったメソッドを介する必要が有るので注意しよう。

個人的には関数の場合は get_typed_func で型を指定して取得できたのだからグローバル変数も get_typed_global で型を指定して取得できれば良いと思うのだが、違うようだ。

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
wasm = open('/tmp/person.wasm', 'rb').read()
module = Module(engine, wasm)

# Build Store
store = Store(engine)

# Build Instance
person1 = Instance(store, module, ())
person2 = Instance(store, module, ())

# Set and get the age of person1 via the method.
person1.exports(store)['set_age'](store, 37)
print("""The age of "person1" is %d.""" % person1.exports(store)['get_age'](store))

# Set and get the age of person2 via the global variable.
person2.exports(store)['age'].set_value(store, 24)
print("""The age of "person2" is %d.""" % person2.exports(store)['age'].value(store))

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

$ python call_wasm.py
The age of "person1" is 37.
The age of "person2" is 24.

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

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

from wasmtime import Engine, Module, Store, Instance

# Build Engine
engine = Engine()

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

# Build Store
store = Store(engine)

# Build Instance
person1 = Instance(store, module, ())
person2 = Instance(store, module, ())

ここまでは 第1回 とほとんど同じだ。
違いは wasm ファイルのパスと、インスタンスを 2 個作っている事だけだ。

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

# Set and get the age of person1 via the method.
person1.exports(store)['set_age'](store, 37)
print("""The age of "person1" is %d.""" % person1.exports(store)['get_age'](store))

ここでは 第1回 と同じような方法で person1age を関数 get_age, set_age を使って読み書きしている。
特筆すべき点は無いだろう。

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

# Set and get the age of person2 via the global variable.
person2.exports(store)['age'].set_value(store, 24)
print("""The age of "person2" is %d.""" % person2.exports(store)['age'].value(store))

ここは過去の記事とは少し違う。
公開された(public な)変数 age にアクセスしているのだが、外部プログラムである Python 上で agei64 (つまり int )では無く Global だ。
実際の値には set_value()value() と言ったメソッドを介してアクセスする。

過去記事でも書いたが、WebAssembly は静的型付け言語である。
person1, person2 いずれの場合においても ageint 以外の値を設定しようとすると TypeError が投げられるだろう。

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

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

# Build instance
person1 = Wasmtime::Instance.new(store, mod)
person2 = Wasmtime::Instance.new(store, mod)

# Set and get the age of person1 via the method.
person1.export('set_age').to_func.call(37)
puts(%q!The age of "person1" is %d.! % person1.export('get_age').to_func.call)

# Set and get the age of person2 via the global variable.
person2.export('age').to_global.set(24)
puts(%q!The age of "person2" is %d.! % person2.export('age').to_global.get)

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

$ ruby call_wasm.rb
The age of "person1" is 37.
The age of "person2" is 24.

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

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

require 'wasmtime'

# Build Engine
engine = Wasmtime::Engine.new

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

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

# Build instance
person1 = Wasmtime::Instance.new(store, mod)
person2 = Wasmtime::Instance.new(store, mod)

ここまでは 第1回 とほとんど同じだ。
違いは wasm ファイルのパスと、インスタンスを 2 個作っている事だけだ。

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

# Set and get the age of person1 via the method.
person1.export('set_age').to_func.call(37)
puts(%q!The age of "person1" is %d.! % person1.export('get_age').to_func.call)

ここでは 第1回 と同じような方法で person1age を関数 get_age, set_age を使って読み書きしている。
特筆すべき点は無いだろう。

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

# Set and get the age of person2 via the global variable.
person2.export('age').to_global.set(24)
puts(%q!The age of "person2" is %d.! % person2.export('age').to_global.get)

ここは過去の記事とは少し違う。
公開された(public な)変数 age にアクセスしているのだが、外部プログラムである Python 上で agei64 (つまり Integer )では無く Global だ。
実際の値は getset と言ったメソッドを介してアクセスする。

過去記事でも書いたが、WebAssembly は静的型付け言語である。
person1, person2 いずれの場合においても ageInteger 以外の値を設定しようとすると TypeError が投げられるだろう。

ポエム

本記事で紹介したコードは説明の為に余計な物を極力排除したため、どれもあまり美しくないと思っている。筆者が業務でコードを書く事になったら、もっと工夫するだろう。

中でも Rust のコードは最悪だ。 Rust から実行 を読まれた方は気が付くだろうが、このコードはプログラマーの直感に反する。なぜ Rust のコードだけが他の JavaScript, Python, Ruby に比べて、こんなに汚いのだろうか?
そもそも論として、筆者は wasm ファイルを Rust から扱う事は筋が良くないのではないかと思っている。

少し話が脱線するが、他で実装されたコード(ライブラリやプログラム)を組み合わせる事で自身は単純なロジックしか持たない様な使い方をするプログラム言語を「Glue 言語」と言う事も有る。(Glue とは英語で「糊(のり)」という意味。)

例えば JavaScript の柔軟性は複雑なロジックを実装する事には向かないが、Glue 言語としては有利に働く事が多い。Glue 言語と下回りのコード(今回は WebAssembly)の接合部分のややこしい部分を、JavaScript は「何となく」で動くようにしてくれる。

もちろん「何となく」で動かすためにはコスト(CPU 実行時間やメモリ効率)がかかる。しかし所詮呼び出すだけだ。全体のパフォーマンスに与える影響は限定的になるだろう。

また、静的型付け言語であり内部まで細かい調整が可能な WebAssembly は、JavaScript と対極にある。WebAssembly と JavaScript を両方使う事で、互いの欠点を補う事が出来るだろう。
Ruby や Python も同様だ。

Rust はどうだろう?
Rust は細かい事を気にするので、プログラマーは WebAssembly との接合部分の面倒な事を全て指示する必要が有る。一方で WebAssembly と Rust の得意分野、苦手分野は重複する部分が大きい。それならば初めから全て Rust で書いた方がシンプルでパフォーマンスも良くなるのではないか?

「本当に外部プログラムを Rust で実装する必要有る?」と自分で聞いてみると良いかもしれない。「難しい部分だけ Rust で書き WebAssembly 形式にコンパイルして、Glue 言語には他の言語を選択する」という方法もある。もちろん「外部プログラムを Rust で書くべき」特殊な事例もあるはずだが、多くは無いと思う。「流行だから」という理由だけで WebAssembly を使うのは良くないかもしれない。

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