WebAssemblyでRustから値を橋渡しする備忘録

- 限界開発鯖 Advent Calender 2022 2日目 です
- `wasm-bindgen` と `wasm-pack` 使いたくねぇなって思ったので, 使わずにどうこうする方法をこいつらから学びました
    - 昨日の敵は今日の友, ボイラープレートは書き写せ
- RustとwasmとJavaScriptのことについて, 特に断りは入れません

## 前置き

`wasm-bindgen` も `wasm-pack` も使いたくねえので, 自分で1から値を受け渡しします.
ただ, プリミティブ以外の値の受け渡しについて記事が見つからなかったので, 自分で書き残しておこうと思います.

## 環境

```console:zsh
> uname -snrmo
Linux archlinux 5.15.78-1-lts x86_64 GNU/Linux
```

```console:zsh
> rustup -V
rustup 1.25.1 (2022-11-01)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.67.0-nightly (73c9eaf21 2022-11-07)`
```

```toml:~/.cargo/config.toml より抜粋
[target.'cfg(all())']
rustflags = ["-C", "target-cpu=native", "-Z", "share-generics=y"]
```

(nightly常用者なのでnightlyになっていますが, 多分他のchannelでも大丈夫だと思います)

あとWebAssemblyの実行にはdenoを使います.

```console:zsh
> deno -V
deno 1.28.2
```

## 作業

作業空間作成はいつものことなのでざっくりとだけ.

```console:zsh
> cargo new workspace --lib
     Created library `workspace` package
> cd workspace
> # Cargo.tomlを編集
> cargo build --target wasm32-unknown-unknown # 今後, buildする際にはこれを実行しているとする
   Compiling workspace v0.0.0 (/home/user/workspace)
--- snip ---
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
```

```toml:Cargo.toml
[package]
name = "workspace"
version = "0.0.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]
```

あと利便性のためsymlink貼っときます.

```console:zsh
> ln -s target/wasm32-unknown-unknown/debug/workspace.wasm .
```

ディレクトリ構造について, 主要な部分だけ. (`lsd` に吐かせた)

```console
.
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
├── target
│   └── wasm32-unknown-unknown
│       └── debug
│           └── workspace.wasm
└── workspace.wasm → target/wasm32-unknown-unknown/debug/workspace.wasm
```

### 技術的詳細 / 参考

- [今更だけどRustでwasmを試す | ryonkmr.com](https://ryonkmr.com/15488406480)
- [rustc/wasm-bindgenの生成するWebAssemblyを観察する](https://zenn.dev/yubrot/scraps/80855584d77a8c)
- [WebAssembly 開発環境構築の本 | WebAssembly 入門](https://wasm-dev-book.netlify.app/hello-wasm.html)

この辺と, あと [rustwasm/wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) (`wasm-bindgen` ってのはもちろんこれのこと) の吐くボイラープレートと, [rustwasm/wasm-pack](https://github.com/rustwasm/wasm-pack) (`wasm-pack` はこれ) の吐くボイラープレートを観察している.

で, まずwasmのデータ型は何があるんじゃいってのが重要で, これは [Types — WebAssembly 2.0 (Draft 2022-11-09)](https://webassembly.github.io/spec/core/syntax/types.html#value-types) より:

- Number (i32 i64 f32 f64)
    - これが重要
- Vector (v128)
    - SIMDとかに使うらしい, 蚊帳の外
- Reference (…)
    - 関数ポインタのこと？よくわかんない

ってんで, 次にRustのプリミティブとの対応は [WebAssembly 開発環境構築の本 | WebAssembly 入門#暗黙の型変換](https://wasm-dev-book.netlify.app/hello-wasm.html#%E6%9A%97%E9%BB%99%E3%81%AE%E5%9E%8B%E5%A4%89%E6%8F%9B) を見れば大体分かります. ちなみに何故か `usize` との対応が書いてないんですけど, 見た感じ `i32` っぽいです.

なので, まぁまぁプリミティブから当てにならない (Rustのそれとは違う) 上, 型変換で予期しない動作をしたらたまったもんじゃない. のでこの辺を意識しておく必要がある.

また, [rustc/wasm-bindgenの生成するWebAssemblyを観察する : "struct"](https://zenn.dev/link/comments/4127c8b691d587) から:
- 構造体をまんま返すってのはできない
    - もちろんバイト列を返すってのも出来ない
- のでどっかで動的にメモリを確保した上でJavaScript側から吸い出してやる必要がある
- もちろん取得, 開放はプログラマの責任 (Rust的に言えばunsafe)

ってのを知った.

ちなみにここで述べてるのは手元でもちゃもちゃしてた時のことなので, これより後の文言とはあんまり関係がない. 以後の文書部分で全部読み取れる…と思う.

### 実装: `(i32) -> i32`

まず腕ならしと行こう.
ちなみにこれは一瞬で書けるし余裕で動く.

```rs:src/lib.rs
#[no_mangle]
extern fn identity(n: i32) -> i32 { n }
```

buildして, 以下のようにdenoから操作してみる.

```js:stdin
const raw = await Deno.readFile("workspace.wasm");
const mod = new WebAssembly.Module(raw);
const { exports: wasm } = new WebAssembly.Instance(mod);

console.log(wasm.identity(10));
console.log(wasm.identity(0.1));
console.log(wasm.identity(-1));
```

出力は:

```:stdout
10
0
-1
```

まぁ, そうよね.

### 観察: `() -> &str`

**本題**. "値を受け渡し" と銘打っているが, 具体的には**バイト列**の受け渡しがしたい.
だけど, まぁなんかいいかなって思って (？) 取り敢えず文字列で試してみることにした.

```rs:src/lib.rs
#[no_mangle]
extern fn return_str() -> &'static str { "hi, wasm." }
```

すると, 以下の警告が表示される.

```
warning: `extern` fn uses type `str`, which is not FFI-safe
 --> src/lib.rs:2:27
  |
2 | extern fn return_str() -> &'static str { "hi, wasm." }
  |                           ^^^^^^^^^^^^ not FFI-safe
  |
  = help: consider using `*const u8` and a length instead
  = note: string slices have no C equivalent
  = note: `#[warn(improper_ctypes_definitions)]` on by default
```

確かに `str` はそのまま吐いてはいけなさそうな感じがする. 取り敢えず素直に生ポインタに直しておく.

```rs:src/lib.rs
#[no_mangle]
extern fn return_str() -> *const char { "hi, wasm.".as_ptr() as _ }
```

まあ, Cで文字列って言ったら `char *` ですし, こちらも `*const char` にしときました.
ではbuildして, 以下のようにdenoから操作してみる.

```js:stdin
const raw = await Deno.readFile("workspace.wasm");
const mod = new WebAssembly.Module(raw);
const { exports: wasm } = new WebAssembly.Instance(mod);

console.log(wasm.return_str());
```

出力は:

```:stdout
1048576
```

まぁ, そうですよね. `*const char` を都合よく文字列と解釈してくれる訳ありませんよね…
まぁ後々分かるんですけど, これは**アドレス**です. 当然といえば当然ですが, なんか文字列をごにょごにょしたものかな〜 (語彙力) ってふんわりしてたら明後日の方向で検証してたので, 一応.
でも, まぁ文字列が格納されたアドレスが貰えた所で, **どこから取り出すの？** となる訳です.

### 観察: `() -> String?`

昨日の敵は今日の友, `wasm-bindgen` と `wasm-pack` に頼ってみます.
あ, しれっと `cargo-edit` 使ってます. ちなみに `v0.11.6` です.

```console:zsh
> cargo add wasm-bindgen
    Updating crates.io index
      Adding wasm-bindgen v0.2.83 to dependencies.
             Features:
             + spans
             + std
             - enable-interning
             - serde
             - serde-serialize
             - serde_json
             - strict-macro
             - xxx_debug_only_print_generated_code
```

`() -> &str` に倣って `wasm-bindgen` にも `str` を返させようと (脳死で) したらですね,

```rs:src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn return_string() -> &'static str { "hi, wasm." }
```

```
error: it is currently not sound to use lifetimes in function signatures
 --> src/lib.rs:4:28
  |
4 | pub fn return_string() -> &'static str { "hi, wasm." }
  |                            ^^^^^^^
```

と怒られるので, `String` に変更.

```rs:src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn return_string() -> String { "hi, wasm.".to_owned() }
```

これはbuildできます. で, ここで `wasm-pack` の出番. ちなみにこれは `v0.10.3` .

<details><summary><code>wasm-pack</code> について</summary>

なんかよく分かんないけど, `cargo install` が通らないんですよね.

[🐛 v0.10.3 "Segmentation fault (core dumped)" when installing from sources · Issue #1186 · rustwasm/wasm-pack](https://github.com/rustwasm/wasm-pack/issues/1186)

```console:zsh
> cargo install wasm-pack --features curl/static-curl
```

私はこうやって導入してます. ちなみに公式のインストーラ使ってもいいですが, あれはリリースに置いてある (Linuxなら `x86_64-unknown-linux-musl` 向けの) バイナリを落としてきてるだけなので, 実質的にさっきのIssueで言及されていた解決法と被るんですよね. ちなみに手元環境で `-musl` 向けのbuildは試してません.

---

</details>

```console:zsh
> wasm-pack build -t web
```

何やら色々出ますが, 無視. `pkg/` に色々吐かれます.

```
pkg
├── package.json
├── workspace.d.ts
├── workspace.js
├── workspace_bg.wasm
└── workspace_bg.wasm.d.ts
```

でまあ, denoから使ってみよう.

```js:stdin
import init, { return_string } from "./pkg/workspace.js";

await init();

console.log(return_string());
```

なにやらよくわからんお作法が出てきていますがこの辺は割愛. 出力は:

```:stdout
hi, wasm.
```

おお, ちゃんと出ている…
ここででっけぇボイラープレート (`pkg/workspace.js`) についてかいつまんで見ていく. ちなみに他のファイルはほぼ見るとこない. `.wasm` はただのbuild済みのバイナリだし, `*.d.ts` はただの型定義ファイルだし.

```js:pkg/workspace.js
// --- snip ---
async function init(input) {
    if (typeof input === 'undefined') {
        input = new URL('workspace_bg.wasm', import.meta.url);
    }
    const imports = getImports();

    if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
        input = fetch(input);
    }

    initMemory(imports);

    const { instance, module } = await load(await input, imports);

    return finalizeInit(instance, module);
}
// --- snip ---
```

まず `init` についてですが, 実は `initSync` の方が分かりやすく:

```js:pkg/workspace.js
// --- snip ---
function initSync(module) {
    const imports = getImports();

    initMemory(imports);

    if (!(module instanceof WebAssembly.Module)) {
        module = new WebAssembly.Module(module);
    }

    const instance = new WebAssembly.Instance(module, imports);

    return finalizeInit(instance, module);
}
// --- snip ---
```

私が自分で書いてたWebAssemblyのインスタンス生成 (denoを使った操作の冒頭部分) とほぼ同じ.
と, その他諸々のあんましようわからん (`wasm-bindgen` のお作法なので詳しいことは知らない) ことをしてます.

で次, `return_string` について.

```js:pkg/workspace.js
// --- snip ---
export function return_string() {
    try {
        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
        wasm.return_string(retptr);
        var r0 = getInt32Memory0()[retptr / 4 + 0];
        var r1 = getInt32Memory0()[retptr / 4 + 1];
        return getStringFromWasm0(r0, r1);
    } finally {
        wasm.__wbindgen_add_to_stack_pointer(16);
        wasm.__wbindgen_free(r0, r1);
    }
}
// --- snip ---
```

まずなんとなく分かるのは, "`wasm.return_string`, なんか自分の書いた関数と違くね？" ってこと.
というわけで `cargo-expand` (`1.0.35 + prettyplease 0.1.21`) を使ってRust側のマクロを展開してみる.

```console:zsh
> cargo expand
--- snip ---
    Finished dev [unoptimized + debuginfo] target(s) in 7.53s
```

```rs:stdout
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use wasm_bindgen::prelude::*;
pub fn return_string() -> String {
    "hi, wasm.".to_owned()
}
#[automatically_derived]
const __wasm_bindgen_generated_return_string__const: () = {
    pub unsafe extern "C" fn __wasm_bindgen_generated_return_string() -> <String as wasm_bindgen::convert::ReturnWasmAbi>::Abi {
        let _ret = {
            let _ret = return_string();
            _ret
        };
        <String as wasm_bindgen::convert::ReturnWasmAbi>::return_abi(_ret)
    }
};
```

これを見てもなんか関数シグネチャが違う気がするけど, あんま気にしないでおきます.
ここで注目すべきなのは, `ReturnWasmAbi::return_abi` ってやつ. これ, めちゃくちゃコールスタックが深いので, ざっくり見ると:

- `String::return_abi` (`ReturnWasmAbi`)
- `String::into_abi` (`IntoWasmAbi`)
- `String::into_bytes`
- `Vec::<u8>::return_abi` (`IntoWasmAbi`)
- `Vec::<u8>::into_boxed_slice`
- `Box::<[u8]>::into_abi` (`IntoWasmAbi`)

そして行き着いたのが [これ](https://github.com/rustwasm/wasm-bindgen/blob/main/src/convert/slices.rs#L128-L130):

```rs:wasm-bindgen/src/convert/slices.rs#L128~130
vectors! {
    u8 i8 u16 i16 u32 i32 u64 i64 usize isize f32 f64
}
```

もちろん冗談です. このマクロの [行き着く先](https://github.com/rustwasm/wasm-bindgen/blob/main/src/convert/slices.rs#L33-L46) は (インデント調節してます):

```rs:wasm-bindgen/src/convert/slices.rs#L33-L46 fixed
impl IntoWasmAbi for Box<[$t]> {
    type Abi = WasmSlice;

    #[inline]
    fn into_abi(self) -> WasmSlice {
        let ptr = self.as_ptr();
        let len = self.len();
        mem::forget(self);
        WasmSlice {
            ptr: ptr.into_abi(),
            len: len as u32,
        }
    }
}
```

**ただのfat pointerやんけ**. ちなみに [`WasmSlice`](https://docs.rs/wasm-bindgen/latest/wasm_bindgen/convert/struct.WasmSlice.html) ってのは:

```rs
#[repr(C)]
pub struct WasmSlice {
    pub ptr: u32,
    pub len: u32,
}
```

こんな感じ.
これをどうやって帰り値にしてるのかまでは分からないけど, 取り敢えずfat pointerもどきのデータ構造を返してるのは分かった. **で,** これを踏まえた上でさっきの `return_string` についてのボイラープレートを見てみよう.

```js
        wasm.return_string(retptr);
        var r0 = getInt32Memory0()[retptr / 4 + 0];
        var r1 = getInt32Memory0()[retptr / 4 + 1];
        return getStringFromWasm0(r0, r1);
```

なんとなく, `r0` ってのが `ptr` で, `r1` が `len` なんだな〜って気がしてきますね？しますよね？

```js:pkg/workspace.js
// --- snip ---
function getStringFromWasm0(ptr, len) {
    return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
// --- snip ---
```

**(笑)**

全部の実装までは出しませんが, この `get*Memory0()` ってやつは [`TypedArray`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) でメモリのビューを取ってます. 代表として [`Uint8Array`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/Uint8Array) について見ると:

```js:pkg/workspace.js
// --- snip ---
let cachedUint8Memory0 = new Uint8Array();

function getUint8Memory0() {
    if (cachedUint8Memory0.byteLength === 0) {
        cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
    }
    return cachedUint8Memory0;
}
// --- snip ---
```

てなわけで, `TypedArray` のコンストラクタで言うところの `
new Uint8Array(buffer [, byteOffset [, length]]);` を使って [`ArrayBuffer`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) のビューを取ってます. 対象となる `ArrayBuffer` は生成済みの [`WebAssembly.Instance`](https://developer.mozilla.org/ja/docs/WebAssembly/JavaScript_interface/Instance) から `.exports.memory` にある [`WebAssembly.Memory`](https://developer.mozilla.org/ja/docs/WebAssembly/JavaScript_interface/Memory) オブジェクトの `.buffer` プロパティから参照できます.

### 実装: `() -> *const FatPointer` (to `str`)

そろそろ読むだけ読んだので, 文字列を自分で吐き出してみる.
取り敢えず `wasm-bindgen` はお役御免.

```console:zsh
> cargo rm wasm-bindgen
    Removing wasm-bindgen from dependencies
```

で, 実装.

```rs:src/lib.rs
#[repr(C)]
struct FatPointer {
    ptr: usize,
    ext: usize,
}

#[no_mangle]
extern fn return_str() -> *const FatPointer {
    let str = "hi, wasm.";

    let fatptr = FatPointer {
        ptr: str.as_ptr() as _,
        ext: str.len() as _,
    };

    let boxed = Box::new(fatptr);
    let ptr = Box::into_raw(boxed) as *const _;

    ptr
}
```

残念なことにfat pointerはそのままではJavaScript側に返せないので一度ヒープに置いて読み出させる. **実行ごとに `core::mem::sizeof::<FatPointer>()` 分のメモリリークが起きていることに留意**.
実際に返す値は `*const FatPointer` なので, ヒープ上の格納位置へのポインタとなる.

buildして, いざ読み出してみよう. denoで以下のように操作する:

```js:stdin
const raw = await Deno.readFile("workspace.wasm");
const mod = new WebAssembly.Module(raw);
const { exports: wasm } = new WebAssembly.Instance(mod);

const ptr = wasm.return_str();
const i32view = new Int32Array(wasm.memory.buffer);
const fatptr = {
  ptr: i32view[ptr / 4 + 0],
  ext: i32view[ptr / 4 + 1],
};

const u8view = new Uint8Array(wasm.memory.buffer);
const rawstr = u8view.subarray(fatptr.ptr, fatptr.ptr + fatptr.ext);

const decoder = new TextDecoder("utf-8", { ignoreBOM: true, fatal: true });
const str = decoder.decode(rawstr);

console.log(str);
```

出力は:

```:stdout
hi, wasm.
```

きたこれ.

### 実装: `() -> *const FatPointer` (to `[u8]`)

目標であった**バイト列**について. さっきの文字列とほぼやってることは同じ.

```rs:src/lib.rs
use core::mem::ManuallyDrop;

#[repr(C)]
struct FatPointer {
    ptr: usize,
    ext: usize,
}

#[no_mangle]
extern fn return_str() -> *const FatPointer {
    let bytes = ManuallyDrop::new(vec![0x00, 0x01, 0x02, 0x03 as u8]);

    let fatptr = FatPointer {
        ptr: bytes.as_ptr() as _,
        ext: bytes.len() as _,
    };

    let boxed = Box::new(fatptr);
    let ptr = Box::into_raw(boxed) as *const _;

    ptr
}
```

気をつける点としては `core::mem::ManuallyDrop` を使っている点. `Vec` は動的メモリ確保なので, そのまま野放しにしたら**メモリ領域が開放されてしまう**. なので, 要は `Vec::drop` を呼び出させないよう, **メモリを開放しないよう** にしている訳だ. **ちなみにもれなくこれもメモリリークである**.
そしたら, 読み出して配列に格納する. denoで以下のように操作する:

```js:stdin
const raw = await Deno.readFile("workspace.wasm");
const mod = new WebAssembly.Module(raw);
const { exports: wasm } = new WebAssembly.Instance(mod);

const ptr = wasm.return_str();
const i32view = new Int32Array(wasm.memory.buffer);
const fatptr = {
  ptr: i32view[ptr / 4 + 0],
  ext: i32view[ptr / 4 + 1],
};

const u8view = new Uint8Array(wasm.memory.buffer);
const rawbytes = u8view.subarray(fatptr.ptr, fatptr.ptr + fatptr.ext);
const bytes = [];
rawbytes.forEach((e) => bytes.push(e));

console.log(bytes);
```

そして, 出力:

```:stdout
[ 0, 1, 2, 3 ]
```

**GG**. 対戦ありがとうございました.

## 最後に

**めちゃくちゃ取り急ぎで書いた記事なので,** 掘っ建て感満載ですが直近でやったこととその備忘録として執筆しました. 実はこれ以外にも値を受け取ったり, 変更したり, あとメモリリーク部分の後始末も…って色々したかったんですけど, 時間の都合上盛り込めませんでした. **っていうかまだやったことないし**. 書けない…

宣伝~~と背水の陣~~ですが, 私のやる気があれば [Nanai10a/quanote](https://github.com/Nanai10a/quanote) に今回のwasmの知識が盛り込まれたアプリケーションが**そのうち**上がるはずです. **まだなにもありません**.

もしこの記事が何かの参考になれば幸いです. では.