1
0

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(第4回)

Last updated at Posted at 2023-07-11

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

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

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

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

第3回 では作成する WebAssembly のコードに整数型の状態(インスタンス変数、プロパティ)を持たせた。
本記事では整数に加えて String 相当のプロパティを持たせる。

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

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

    get_age() {
        return this.age;
    }

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

    get_name() {
        return this.name;
    }

    set_name(name) {
        this.name = name;
    }
}

本記事では上記の様な WebAssembly のコードを実行する下記の外部プログラムを作成する。

  • Vanilla JS (フレームワークやライブラリを使わない、そのままの JavaScript)から WebAssembly を実行
  • Rust, Python, Ruby から低レイヤーのライブラリ wasmtime だけを使って WebAssembly を実行

なお、説明の都合上 Vanilla JS から実行 の章は JavaScript に興味が無くとも読んでほしい。
(そんなに難しい事はやっていないので JavaScript を知らなくとも本質的な部分は理解できるはずだ。)
それ以外の興味の無い言語の章については読み飛ばして問題無い。

WebAssembly の線形メモリ

WebAssembly で String 相当の物を扱うにはハードルが 2 個存在する。

  1. WebAssembly がどうやってメモリを確保するか
  2. WebAssembly と外部プログラムで、どうやって String の受け渡しを行うか

String は一般的にインスタンスの外側にメモリを確保し、そこに保存する byte 列を書き込む。
String のインスタンス自信には byte 列を保存したメモリのアドレスや、その長さ等を保存する。

しかし 第1回 で説明したように「メモリ確保」には System Call を実行する必要が有り、WebAssembly から実行する事は出来ない。

これは外部プログラムが WebAssembly のインスタンスを立ち上げる時にメモリの塊を渡す事で解決する。WebAssembly のコードは OS からメモリをもらうのではなく、あらかじめ渡されたメモリの中でやり繰りするのだ。この「あらかじめ渡されたメモリ」の事を「線形メモリ」と言う。
第2回 で外部プログラムから関数を import したが、同様に線形メモリを import するのだ。

また、線形メモリを export すれば外部プログラムから読み書きできる。
WebAssembly のコードと外部プログラムは String を直接やり取り出来ない。しかし外部プログラムと WebAssembly の両方が線形メモリにアクセス出来れば、あとは String を保管した場所(ポインター)やその長さだけを受け渡しする事で実質的に String の受け渡しの様な事が出来る。

詳細は過去に WebAssembly と外部プログラムのインターフェース という記事で書いたので、気になる人は読んで欲しい。

WebAssembly コードの準備

過去記事同様、WebAssembly Text Format で動作確認をする WebAssembly のコードを作成し、wat2wasm でコンパイルする。

wabt のインストール

github の README.md を読んでコンパイルする。
C++ のコンパイル環境が必要なので注意。

wat2wasm というコマンドがコンパイルされるので、必要に応じて Path を通しておくと良いだろう。

wasm ファイルの準備

wasm ファイルとは、WebAssembly の規格に沿ったバイナリファイルの事である。
最初に各種言語から動かして動作確認するための wasm ファイルを用意しよう。

テキストエディタで以下の様な内容のファイルを作成し /tmp/person2.wat という名前で保存する。
(/tmp/person.wat という名前は 第3回 で使ってしまった。)

(module
    (import "host" "mem" (memory 0))
    (export "memory" (memory 0))

    (global $age (mut i64) (i64.const -1))
    (global $name_ptr (mut i32) (i32.const 0))
    (global $name_len (mut i32) (i32.const 0))

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

    (func (export "get_name") (result i32 i32)
        global.get $name_ptr
        global.get $name_len)

    (func (export "set_name") (param $ptr i32) (param $len i32)
        local.get $ptr
        global.set $name_ptr
        
        local.get $len
        global.set $name_len))

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

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

  • 外部プログラムの host.mem という名前で指定されている線形メモリを import。この線形メモリの初期サイズは 0 page (= 0 byte) である。最大サイズは指定していないので、 WebAssembly の仕様上の上限である 32 page (= 2 GB) となる。(WebAssembly の 1 page は 64 KB。)
  • 線形メモリを外部から memory という名前でアクセス可能にする
  • グローバル変数(インスタンス変数、プロパティ)を宣言。この WebAssembly 内部からは $age という名前でアクセス可能にしている。この変数は変更可能な i64 型である。インスタンス作成時にこの変数は -1 で初期化される。
  • グローバル変数(インスタンス変数、プロパティ)を宣言。この WebAssembly 内部からは $name_ptr という名前でアクセス可能にしている。この変数は変更可能な i32 型である。インスタンス作成時にこの変数は 0 で初期化される。
  • グローバル変数(インスタンス変数、プロパティ)を宣言。この WebAssembly 内部からは $name_len という名前でアクセス可能にしている。この変数は変更可能な i32 型である。インスタンス作成時にこの変数は 0 で初期化される。
  • 関数を定義。この関数は外部から get_age という名前でアクセス可能である。この関数は引数を取らず i64 型の値を 1 個返す。
  • 関数を定義。この関数は外部から set_age という名前でアクセス可能である。この関数は i64 型の引数を 1 個取り、値を返さない。
  • 関数を定義。この関数は外部から get_name という名前でアクセス可能である。この関数は引数を取らず i32 型の値を 2 個返す。
  • 関数を定義。この関数は外部から set_name という名前でアクセス可能である。この関数は i32 型の引数を 2 個取り、値を返さない。

線形メモリの初期サイズを 0 byte としたのは「線形メモリのサイズを拡張する」というコードも含めて解説したかったからだ。(通常ならば 1 page (= 64 KB) とするだろう。)

本記事の執筆時点(2023 年 7 月)では、WebAssembly は 32 bit OS を想定している。
そのため線形メモリの最大サイズは 2 GB であり、ポインターやその長さは 32 bit 符号付整数の範囲内に収まる。
(多くのプログラム言語同様、32 bit の最上位 bit はエラーフラグとなっており実質的に使用可能なのは 31 bit である。符号付 32 bit の範囲内に収まる。)

各関数はその名前から分かる様に、それぞれ agename のゲッター、セッターと呼ばれる物だ。
name は 2 個の i32 からなる値で表し、1 個目はポインター、2 個目は長さに相当する。

第3回 はグローバル変数に直接アクセスする方法も解説したが、 name は 2 個の値を同時に扱う必要が有るので今回はこの方法は控える。

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

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

Vanilla JS から実行

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

筆者の JavaScript 環境

  • nodejs v18.16.1

Vanilla JS のコード

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

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

// Build Instance
const importObject = {
    host: {
        mem: new WebAssembly.Memory({ initial: 0 })
    }
};
const person = new WebAssembly.Instance(mod, importObject);

// Set the age
{
    const age = BigInt(41);
    person.exports.set_age(age);
}

// Initialize the memory
const linearMemory = importObject.host.mem;
const page = 1;
linearMemory.grow(page);
const buffer = new Uint8Array(linearMemory.buffer);

// Set the name
{
    const name = new TextEncoder().encode("Shin Yoshida");
    const ptr = 8;
    const subarray = buffer.subarray(ptr, ptr + name.length);
    subarray.set(name);
    person.exports.set_name(ptr, name.length);
}

// Display the name and the age
{
    const age = person.exports.get_age();
    const [ptr, length] = person.exports.get_name();
    const name = new TextDecoder().decode(buffer.subarray(ptr, ptr + length));
    console.log(`The age of ${name} is ${age}.`);
}

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

$ node call_wasm.js
The age of Shin Yoshida is 41.

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

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

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

// Build Instance
const importObject = {
    host: {
        mem: new WebAssembly.Memory({ initial: 0 })
    }
};
const person = new WebAssembly.Instance(mod, importObject);

ここまでは 第2回 とほとんど同じだ。
違いは wasm ファイルのパスと、 importObject.host.memMemory のインスタンスである事だけだ。

複数の Instance を作る場合は、同一の importObject を使うと同じ線形メモリを共有してしまうので注意しよう。

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

// Set the age
{
    const age = BigInt(41);
    person.exports.set_age(age);
}

ここも 第3回 と同様の方法で age を取得している。

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

// Initialize the memory
const linearMemory = importObject.host.mem;
const page = 1;
linearMemory.grow(page);
const buffer = new Uint8Array(linearMemory.buffer);

ここでは線形メモリのサイズを 1 page (= 64 KB) に拡張している。
(この wasm ファイルが線形メモリを使うのは name の保存だけなので 1 page で十分だと判断した。)

MemoryArrayBuffer (もしくは SharedArrayBuffer )のラッパーである。
後で使いやすい様に、この ArrayBufferbuffer という名前でアクセス可能にしている。
(この ArrayBuffer については JavaScript を良く知らない人は読み飛ばして良い。)

私達は wat ファイルを作成する時に memory という名前で外部から線形メモリにアクセス可能にした。
const linearMemory = importObject.host.mem; というステップは
const linearMemory = person.exports.memory; に置き換える事も可能だ。

「wat ファイルで線形メモリを export (公開)する必要は無かったのではないか?」と疑問に思う人も居るだろう。

その疑問は、ある意味では正しい。実際に線形メモリを export しなくとも、このプログラムは正常動作する。
しかし、この方法は「抜け道」の様な物だと筆者は考えている。

例えば外部プログラムを Rust で実装する場合はこの抜け道は使用できないないので export が必要だ。
JavaScript も今後の仕様変更で、この抜け道が禁止されるかもしれない。

筆者の個人的意見としては、外部プログラムはどうでも良い。(動けば良い。)
しかし WebAssembly は(少なくとも建前上は)「どの環境でも動く」事を目指しているので export する方が良いと思う。

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

// Set the name
{
    const name = new TextEncoder().encode("Shin Yoshida");
    const ptr = 8;
    const subarray = buffer.subarray(ptr, ptr + name.length);
    subarray.set(name);
    person.exports.set_name(ptr, name.length);
}

ここでは "Shin Yoshida" という stringperson に保存を以下の手順で実行している。

  1. "Shin Yoshida" という string をバイト列に変更し、 name という変数に保存
  2. 線形メモリの ptr バイト目から ptr + name.length バイト目の部分に subarray という変数名をつける
  3. subarrayname のバイト列をコピー
  4. person のグローバル変数(インスタンス変数、プロパティ)に ptrname.length の情報を保存

ptr の値を 8 にした事に大きな意味はない。(線形メモリは 64 KB 確保してあるので、コピー先はその中のどの部分でも良い。)
ただ、先頭(0 byte 目)から使用すると wasm ファイルの中ではアドレス 0 番地にデータが書き込まれている様に見えてしまう。0 番地は多くのプログラム言語で Null ポインターと呼ばれる物だ。あまり気持ちが良くないので 8 byte 目から使用を開始した。

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

// Display the name and the age
{
    const age = person.exports.get_age();
    const [ptr, length] = person.exports.get_name();
    const name = new TextDecoder().decode(buffer.subarray(ptr, ptr + length));
    console.log(`The age of ${name} is ${age}.`);
}

ここでは personagename を取得、表示している。

age第3回 と同じ方法で関数 get_age を実行して取得している。

name も同様に関数 get_name を実行して「線形メモリの、どの部分に name が保存されているか」を取得した後、 TextDecoder を介して string に変換している。

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

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

    // Build `Memory`
    let ty = MemoryType::new(0, None);
    let memory = Memory::new(&mut store, ty)?;

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

    // Set the age
    let set_age = person.get_typed_func::<i64, ()>(&mut store, "set_age")?;
    set_age.call(&mut store, 41)?;

    // Initialize the memory
    const PAGE: u64 = 1;
    let linear_memory = person.get_memory(&mut store, "memory").unwrap();
    linear_memory.grow(&mut store, PAGE)?;

    // Set the name
    {
        const NAME: &'static [u8] = "Shin Yoshida".as_bytes();
        const PTR: usize = 8;
        linear_memory.write(&mut store, PTR, NAME)?;
        let set_name = person.get_typed_func::<(i32, i32), ()>(&mut store, "set_name")?;
        set_name.call(&mut store, (PTR as i32, NAME.len() as i32))?;
    }

    // Display the name and the age.
    {
        let get_age = person.get_typed_func::<(), i64>(&mut store, "get_age")?;
        let age = get_age.call(&mut store, ())?;

        let get_name = person.get_typed_func::<(), (i32, i32)>(&mut store, "get_name")?;
        let (ptr, length) = get_name.call(&mut store, ())?;

        let mut buffer = Vec::with_capacity(length as usize);
        unsafe { buffer.set_len(length as usize) };
        linear_memory.read(&store, ptr as usize, buffer.as_mut_slice())?;
        let name = String::from_utf8(buffer).unwrap();

        println!("The age of {} is {}.", &name, &age);
    }

    Ok(())
}

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

$ cargo run
   Compiling call_person2 v0.1.0 (/home/wbcchsyn/tmp/person2/call_person2)
    Finished dev [unoptimized + debuginfo] target(s) in 3.55s
     Running `target/debug/call_person2`
The age of Shin Yoshida is 41.

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

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

    // Build `Memory`
    let ty = MemoryType::new(0, None);
    let memory = Memory::new(&mut store, ty)?;

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

ここは 第2回 とほとんど同じだ。
違いは wasm ファイルのパスと linker.define の引数に Memory を渡している事だけだ。

変数 person の宣言に mut を付ける必要が無いのは 第3回 と同じ理由だ。
mut を付けなくとも agename を変更出来る。

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

    // Set the age
    let set_age = person.get_typed_func::<i64, ()>(&mut store, "set_age")?;
    set_age.call(&mut store, 41)?;

ここでは 第3回 と同じような方法で personage を保存している。

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

    // Initialize the memory
    const PAGE: u64 = 1;
    let linear_memory = person.get_memory(&mut store, "memory").unwrap();
    linear_memory.grow(&mut store, PAGE)?;

ここでは線形メモリのサイズを 1 page (= 64 KB) に拡張している。
(この wasm ファイルが線形メモリを使うのは name の保存だけなので 1 page で十分だと判断した。)
私達は wat ファイルを作成する時に memory という名前で外部から線形メモリにアクセス可能にしたので関数 person.get_memory で取得できる。

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

    // Set the name
    {
        const NAME: &'static [u8] = "Shin Yoshida".as_bytes();
        const PTR: usize = 8;
        linear_memory.write(&mut store, PTR, NAME)?;
        let set_name = person.get_typed_func::<(i32, i32), ()>(&mut store, "set_name")?;
        set_name.call(&mut store, (PTR as i32, NAME.len() as i32))?;
    }

ここでは "Shin Yoshida" という Stringperson に保存を以下の手順で実行している。

  1. "Shin Yoshida" という &str&[u8] に変更し、 NAME という変数に保存
  2. 線形メモリの PTR バイト目以降に NAME のバイト列をコピー(コピーする長さは NAME.len()
  3. person のグローバル変数(インスタンス変数、プロパティ)に PTRNAME.len() の情報を保存

PTR の値を 8 にした事に大きな意味は無い。(線形メモリは 64 KB 確保してあるので、コピー先はその中のどの部分でも良い。)
ただ、先頭(0 byte 目)から使用すると wasm ファイルの中ではアドレス 0 番地にデータが書き込まれている様に見えてしまう。0 番地は多くのプログラム言語で Null ポインターと呼ばれる物だ。あまり気持ちが良くないので 8 byte 目から使用を開始した。

Rust コードで 1 点注意が必要なのは、 PTRNAME.len() といった値の型だ。
外部プログラムでは、これらの値は usize である。この外部プログラムを 64 bit OS 上で動かすならば usize符号無し 64 bit 整数 になるだろう。
一方、本記事の執筆時点(2023 年 7 月)では WebAssembly は基本的に 32 bit OS を想定しており、私達の作った wat ファイルでは関数 set_name の引数は 符号付き 32 bit 整数 である。

外部プログラムと WebAssembly 関数の引数の間で明確な型変換が必要である。

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

    // Display the name and the age.
    {
        let get_age = person.get_typed_func::<(), i64>(&mut store, "get_age")?;
        let age = get_age.call(&mut store, ())?;

        let get_name = person.get_typed_func::<(), (i32, i32)>(&mut store, "get_name")?;
        let (ptr, length) = get_name.call(&mut store, ())?;

        let mut buffer = Vec::with_capacity(length as usize);
        unsafe { buffer.set_len(length as usize) };
        linear_memory.read(&store, ptr as usize, buffer.as_mut_slice())?;
        let name = String::from_utf8(buffer).unwrap();

        println!("The age of {} is {}.", &name, &age);
    }

ここでは person から agename を取得して標準出力に表示している。

age の取得方法は 第3回 と同じだ。

name の取得は下記の方法で行っている。(ざっくり言うと、先ほど name を保存した時の逆の手順だ。)

  1. 関数 get_name を実行し、 name が線形メモリ内のどの場所に保存されているか確認する。
  2. 線形メモリ上の name の内容をコピーする buffer という変数を作成
  3. 線形メモリ上の namebuffer にコピー
  4. Vec<u8> 型の bufferString 型に変換

先ほどと同様に、外部プログラムの usize のサイズと WebAssembly のアドレスの bit 長が一致するとは限らないので注意しよう。

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, Instance, Limits, Linker, Memory, MemoryType,
                      Module, Store)

# Build Engine.
engine = Engine()

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

# Build Store.
store = Store(engine)

# Build Memory.
limits = Limits(0, None)
ty = MemoryType(limits)
linear_memory = Memory(store, ty)

# Build Instance.
linker = Linker(engine)
linker.define(store, "host", "mem", linear_memory)
person = linker.instantiate(store, module)

# Set the age.
person.exports(store)['set_age'](store, 41)

# Initialize the memory.
PAGE = 1
linear_memory.grow(store, PAGE)

# Set the name.
NAME = b"Shin Yoshida"
PTR = 8
linear_memory.write(store, NAME, PTR)
person.exports(store)["set_name"](store, PTR, len(NAME))

# Display the name and the age.
age = person.exports(store)["get_age"](store)

(name_ptr, name_length) = person.exports(store)["get_name"](store)
name = linear_memory.read(store, name_ptr, name_ptr + name_length).decode()

print("The age of %s = %d" % (name, age))

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

$ python call_wasm.py
The age of Shin = 41

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

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

from wasmtime import (Engine, Instance, Limits, Linker, Memory, MemoryType,
                      Module, Store)

# Build Engine.
engine = Engine()

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

# Build Store.
store = Store(engine)

# Build Memory.
limits = Limits(0, None)
ty = MemoryType(limits)
linear_memory = Memory(store, ty)

# Build Instance.
linker = Linker(engine)
linker.define(store, "host", "mem", linear_memory)
person = linker.instantiate(store, module)

ここでは 第2回 とほとんど同じだ。
違いは wasm ファイルのパスと、linker.define() の引数に Memory を渡している事だけだ。

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

# Set the age.
person.exports(store)['set_age'](store, 41)

ここでは 第3回 と同じ方法で age を設定している。

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

# Initialize the memory.
PAGE = 1
linear_memory.grow(store, PAGE)

ここでは線形メモリのサイズを 1 page (= 64 KB) に拡張している。
(この wasm ファイルが線形メモリを使うのは name の保存だけなので 1 page で十分だと判断した。)

私達は wat ファイルを作成する時に memory という名前で外部から線形メモリにアクセス可能にした。
linear_memory.grow(store, PAGE); というステップは
person.exports(store)["memory"].grow(store, PAGE) に置き換えても良い。

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

# Set the name.
NAME = b"Shin Yoshida"
PTR = 8
linear_memory.write(store, NAME, PTR)
person.exports(store)["set_name"](store, PTR, len(NAME))

ここでは "Shin Yoshida" という文字列を person に保存を以下の手順で実行している。

  1. "Shin Yoshida" という文字列の bytes 型に変更した値を NAME という変数に保存
  2. 線形メモリの PTR バイト目以降に NAME のバイト列をコピー(コピーする長さは len(NAME)
  3. person のグローバル変数(インスタンス変数、プロパティ)に PTRlen(NAME) の情報を保存

PTR の値を 8 にした事に大きな意味は無い。(線形メモリは 64 KB 確保してあるので、コピー先はその中のどの部分でも良い。)
ただ、先頭(0 byte 目)から使用すると wasm ファイルの中ではアドレス 0 番地にデータが書き込まれている様に見えてしまう。0 番地は多くのプログラム言語で Null ポインターと呼ばれる物だ。あまり気持ちが良くないので 8 byte 目から使用を開始した。

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

# Display the name and the age.
age = person.exports(store)["get_age"](store)

(name_ptr, name_length) = person.exports(store)["get_name"](store)
name = linear_memory.read(store, name_ptr, name_ptr + name_length).decode()

print("The age of %s = %d" % (name, age))

ここでは person から agename を取得して標準出力に表示している。

age の取得方法は 第3回 と同じだ。

name の取得は下記の方法で行っている。(ざっくり言うと、先ほど name を保存した時の逆の手順だ。)

  1. 関数 get_name を実行し、 name が線形メモリ内のどの場所に保存されているか確認
  2. 線形メモリ上の name の内容をコピーした bytearray 型を取得し、 str 型に変換

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

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

# Build Memory
linear_memory = Wasmtime::Memory.new(store, min_size: 0)

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

# Set the age
person.export('set_age').to_func.call(41)

# Initialize the memory
PAGE = 1
linear_memory.grow(PAGE)

# Set the name
NAME = "Shin Yoshida"
PTR = 8
linear_memory.write(PTR, NAME)
person.export('set_name').to_func.call(PTR, NAME.length)

# Display the name and age
age = person.export('get_age').to_func.call

name_ptr, name_length = person.export('get_name').to_func.call
name = linear_memory.read(name_ptr, name_length)

puts("The age of %s is %s." % [name, age])

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

$ ruby call_wasm.rb
The age of Shin Yoshida is 41.

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

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

require 'wasmtime'

# Build Engine
engine = Wasmtime::Engine.new

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

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

# Build Memory
linear_memory = Wasmtime::Memory.new(store, min_size: 0)

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

ここまでは 第2回 とほとんど同じだ。
違いは wasm ファイルのパスと、linker.define の引数に Memory を渡している事だけだ。

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

# Set the age
person.export('set_age').to_func.call(41)

ここでは 第3回 と同じ方法で age を設定している。

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

# Initialize the memory
PAGE = 1
linear_memory.grow(PAGE)

ここでは線形メモリのサイズを 1 page (= 64 KB) に拡張している。
(この wasm ファイルが線形メモリを使うのは name の保存だけなので 1 page で十分だと判断した。)

私達は wat ファイルを作成する時に memory という名前で外部から線形メモリにアクセス可能にした。
linear_memory.grow(PAGE); というステップは
person.export("memory").to_memory.grow(PAGE) に置き換えても良い。

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

# Set the name
NAME = "Shin Yoshida"
PTR = 8
linear_memory.write(PTR, NAME)
person.export('set_name').to_func.call(PTR, NAME.length)

ここでは "Shin Yoshida" という文字列を person に保存を以下の手順で実行している。

  1. "Shin Yoshida" という String を NAME という変数に保存
  2. 線形メモリの PTR バイト目以降に NAME のバイト列をコピー(コピーする長さは NAME.length
  3. person のグローバル変数(インスタンス変数、プロパティ)に PTRNAME.length の情報を保存

PTR の値を 8 にした事に大きな意味は無い。(線形メモリは 64 KB 確保してあるので、コピー先はその中のどの部分でも良い。)
ただ、先頭(0 byte 目)から使用すると wasm ファイルの中ではアドレス 0 番地にデータが書き込まれている様に見えてしまう。0 番地は多くのプログラム言語で Null ポインターと呼ばれる物だ。あまり気持ちが良くないので 8 byte 目から使用を開始した。

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

# Display the name and age
age = person.export('get_age').to_func.call

name_ptr, name_length = person.export('get_name').to_func.call
name = linear_memory.read(name_ptr, name_length)

puts("The age of %s is %s." % [name, age])

ここでは person から agename を取得して標準出力に表示している。

age の取得方法は 第3回 と同じだ。

name の取得は下記の方法で行っている。(ざっくり言うと、先ほど name を保存した時の逆の手順だ。)

  1. 関数 get_name を実行し、 name が線形メモリ内のどの場所に保存されているか確認
  2. 線形メモリ上の name の内容をコピーした String を取得

まとめ

今回は外部プログラムと WebAssembly 間で String の受け渡し方法をご紹介した。

ただ、今回紹介した方法は「WebAssembly 側で線形メモリを使うのがプロパティ name の保存だけ」という制限付きでのみ上手くいく。例えば WebAssembly に「文字列を保持する可変長配列(Array や Vector と呼ばれる物)」を持たせたいとしたら、話は変わってくる。

もう少し複雑な例については次回以降にご紹介したいと思う。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?