WebAssemblyでScratchプラグインを作ろう!

WebAssemblyで
Scratchプラグインを作ろう!

#2 One More Step into WebAssembly

#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

今回から少し難しくなるよ

#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

今日やること

  • WebAssemblyと文字列
    • 実験その1
  • 線形メモリってなんだ
  • 線形メモリを扱ってブラウザと文字列のやり取りをする
  • 実践的なプログラムを作る
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

WASM・文字列・線形メモリ

#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

文字列を扱ってみよう

  • 「文字列」ってどんなもの?
console.log("Hello, World");
console.log("こんにちは!");

// WASMなら、例えばこう?
let msg = wasm.exports.hello();
console.log(msg);
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

そのままコードに書いてみよう

  • 今日のプロジェクトcrate
$ cargo new --lib hello-string
$ cd hello-string && code Cargo.toml # 前回参照
$ code src/lib.rs
#[no_mangle]
pub fn hello() -> &'static str {
    "Hello, world"
}
$ cargo build --target wasm32-unknown-unknown
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

動かしてみるとどうなる?

<html><head>
    <title>My first wasm</title>
    <script async type="text/javascript">
        const obj = {};
        WebAssembly.instantiateStreaming(fetch("./string1.wasm"), obj).then(
            (wasm) => {
                let msg = wasm.instance.exports.hello();
                console.log(msg);
            },
        );
    </script></head>
    <body><h1>Wasm working on browser</h1></body>
</html>
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

返事がない

  • エラーではない=関数はあるけど、値が返らない?

#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

WASMバイナリはどうなってる?

  • hello() は引数がないはずなのに、 (i32) -> nil ??
$ wasm-objdump -x web/string1.wasm
...
Type[1]:
 - type[0] (i32) -> nil
Function[1]:
 - func[0] sig=0 <hello>
Export[4]:
 - memory[0] -> "memory"
 - func[0] <hello> -> "hello"
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

何が何やら...

(雰囲気でryの画像を貼る)

#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

「線形メモリ」について理解する必要がある

#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

線形メモリとは

  • WASMインスタンスで自由に使えるメモリ領域
  • cf. 普通のOSのプロセス
    • メモリはプロセスごとに割り当てられる
      • そのうちのヒープの範囲にmalloc()できる
    • 他のデータ(命令コードほか)も同じ領域、アドレス範囲で使い分ける
  • WASMの線形メモリ
    • インスタンスごとに割り当てられる
    • 線形メモリはプログラムのデータ専用で取り扱える
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

線形メモリの上の文字列

  • 文字列=連続したバイトという考え方はWASMでも重要
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

つまり、線形メモリの上の文字列は

  • メモリ上の開始位置と、その長さが分かれば文字列を特定できる
  • 文字列特定のためのデータ
    • WASMレベルではどちらも i32, i32 と扱われる
      • Rustレベルでは ptr: *mut u8len: usize
      • ※ C言語的に表現すれば u8 = char
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

線形メモリ = WASM/ブラウザ共有の仕組み

  • WASMの世界とブラウザで、線形メモリを通じてデータのやり取りをすることもできる
  • 逆にいうと:
    • 関数の引数/戻り値か、線形メモリでしか相互のやり取りができない
    • sandboxingの一環でもある
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

改めて

  • 概念的なところをざっくり確認したところで
  • 「文字列」を扱う関数をWASMにしてみよう
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

文字列の理解その1

  • WASMインスタンスからブラウザに戻す時の挙動
    • を理解しよう
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

こういうコードを書く

#[no_mangle]
pub fn hello() -> &'static str {
    "Hello, world"
}
  • wasm32-unknown-unknown でコンパイルする
    • --release でコンパイルして最適化
    • バイナリは target/wasm32-unknown-unknown/release/hello_string.wasm にある
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

こういうシグネチャになる

Type[13]:
 - type[3] (i32) -> nil # hello() -> str がこうなる
Function[55]:
 - func[0] sig=3 <hello>
Export[5]:
 - func[0] <hello> -> "hello"
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

少しWASMバイトコードを追ってみよう

  • 正確に読める必要は今回はなく、雰囲気を掴みます
  • wasm-objdump -d でバイトコードを人間に読める表現にしてくれる
    • wasm-tools にも似た機能がある
$ wasm-objdump -d \
    target/wasm32-unknown-unknown/release/hello_string.wasm
...
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

hello関数のバイトコード

000117 func[0] <hello>:
 000118: 20 00                      | local.get 0
 00011a: 41 0c                      | i32.const 12
 00011c: 36 02 04                   | i32.store 2 4
 00011f: 20 00                      | local.get 0
 000121: 41 80 80 c0 80 00          | i32.const 1048576
 000127: 36 02 00                   | i32.store 2 0
 00012a: 0b                         | end
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!
;; 最初の引数 = バッファ(メモリ上のoffset)
local.get 0
;; 132の 12 をスタックに
i32.const 12
;; メモリの offset + 4 に値 12 を保存
i32.store 2 4

;; 以下、同様に 1048576 を offset + 0 に保存
;; 1048576 こそ "Hello, world" のある位置
local.get 0
i32.const 1048576
i32.store 2 0

;; 関数を抜ける
end
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

ブラウザで呼び出す

const obj = {};
WebAssembly.instantiateStreaming(fetch("./string1.wasm"), obj).then(
    (wasm) => {
        // bufferを確保するためヒープの先頭位置を使う
        let offset = wasm.instance.exports.__heap_base;
        let length = 8; // 32bit+32bit
        // bufferは、ブラウザ側からmemoryを操作し、memory中に確保する!
        let memory = wasm.instance.exports.memory;
        let buffer = new Uint8Array(memory.buffer, offset, length);
        wasm.instance.exports.hello(offset);

        // consoleから参照できるようにグローバル変数に入れてしまう
        window.buffer = buffer;
        window.wasm = wasm;
    },
);
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

buffer はこうなっている

> buffer
//=> Uint8Array(8) [0, 0, 16, 0, 12, 0, 0, 0,]
  • 上位32bit = リトルエンディアンで 0, 0, 16, 0 == 1048576
  • 下位32bit = リトルエンディアンで 12
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

これを解釈する

let buffer2 = new Uint8Array(wasm.instance.exports.memory.buffer, 1048576, 12);
let msg = String.fromCharCode.apply(null, buffer2);
concole.log(msg);
//=> Hello, world
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

つまり...

  • Rust(rustc) では、 &str を返そうとすると、指定のバッファに offset, size を埋めて返す関数になる
    • rustc 1.80.1 での挙動で、将来変わる可能性もある
  • String などを使ってみても同じようになる
  • これは正直、低レイヤといわれても扱いづらい...
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

以下のどちらかを推奨したい

#[no_mangle]
pub unsafe fn hello2() -> *const u8 {
    // これであれば直接offsetを返してくれる。ただしsizeがわからない
    "Hello, world".as_ptr()
}

use core::slice::from_raw_parts_mut;
#[no_mangle]
pub unsafe fn hello3(buf: *mut u8, buflen: i32) {
    let src: &[u8] = "Hello, world".as_bytes();
    // prt + lenから slice を再生する
    let buf: &mut [u8] = from_raw_parts_mut(buf, buflen as usize);
    // 値を入れていく
    (*buf).copy_from_slice(src);
}
// offset, sizeをコントロールできる。あくまで学習なのでunsafe...
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

再び動作検証

  • この講義では基本後者を使う方針
use core::slice::from_raw_parts_mut;
#[no_mangle]
pub unsafe fn hello_by_buflen(buf: *mut u8, buflen: i32) {
    let src: &[u8] = "Hello, world".as_bytes();
    let buf: &mut [u8] = from_raw_parts_mut(buf, buflen as usize);
    buf.copy_from_slice(src);
}
  • 同じように wasm32-unknown-unknown でコンパイルする
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

ブラウザから呼び出す

const obj = {};
WebAssembly.instantiateStreaming(fetch("./string1.wasm"), obj).then(
    (wasm) => {
        let offset = wasm.instance.exports.__heap_base;
        let length = 12; // 12 bytes 必要なため
        let memory = wasm.instance.exports.memory;
        let buffer = new Uint8Array(memory.buffer, offset, length);
        wasm.instance.exports.hello_by_buflen(offset, length);
        // 上記のbufferに値が入るのでそのまま使える
        let msg = String.fromCharCode.apply(null, buffer);
        console.log(msg);
    },
);
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

動作確認


#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

文字列の理解その2

  • ブラウザからWASMインスタンスに渡す時の挙動
    • こちらも理解しよう
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

この場合のコード

// 文字列の1バイト目を数値として返す
#[no_mangle]
pub fn welcome(src: &str) -> i32 {
    src.as_bytes()[0] as i32
}
  • シグネチャはこうなる。strが2つの引数になる
Type[14]:
 - type[2] (i32, i32) -> i32
Function[57]:
 - func[3] sig=2 <welcome>
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

また少しWASMバイトコードを追ってみよう

00012c func[1] <welcome>:
 00012d: 02 40                      | block
 00012f: 20 01                      |   local.get 1
 000131: 45                         |   i32.eqz
 000132: 0d 00                      |   br_if 0
 000134: 20 00                      |   local.get 0
 000136: 2d 00 00                   |   i32.load8_u 0 0
 000139: 0f                         |   return
 00013a: 0b                         | end
 00013b: 41 00                      | i32.const 0
 00013d: 41 00                      | i32.const 0
 00013f: 41 ac 80 c0 80 00          | i32.const 1048620
 000145: 10 ab 80 80 80 00          | call 43 <_ZN4core9panicking...>
 00014b: 00                         | unreachable
 00014c: 0b                         | end
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

blockの中を確認する

// 2番目の引数=sizeを取得
local.get 1
// 0 だとout of indexなのでエラー、ブロック抜ける
i32.eqz
br_if 0
// 1番目の引数=offsetを取得
local.get 0
// そのoffsetにあるバイトをi32として戻り値にload
i32.load8_u 0 0
// ブロックを抜ける/そのまま関数も抜ける
return
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

ブラウザで確認

const obj = {};
WebAssembly.instantiateStreaming(fetch("./string1.wasm"), obj).then(
    (wasm) => {
        let msg = "Hello from JavaScript";
        let offset = wasm.instance.exports.__heap_base;
        let memory = wasm.instance.exports.memory;
        let buffer = new Uint8Array(memory.buffer, offset, msg.length);
        // ブラウザ側で、メモリを確保し、値を埋める
        for( var i = 0; i < msg.length; i++ ) {
            buffer[i] = msg.charCodeAt(i);
        }
        let result = wasm.instance.exports.welcome(offset, msg.length);
        let msg2 = "result = " + result.toString();
        console.log(msg2);
    },
);
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

ブラウザで確認結果

  • "H".charCodeAt(0) == 72

#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

つまりこう書いても大体同じ

use core::slice::from_raw_parts;
#[no_mangle]
pub unsafe fn welcome2(src: *const u8, srclen: i32) -> i32 {
    let src: &[u8] = from_raw_parts(src, srclen as usize);
    src[0] as i32
}
  • 本講義では、文字列を戻すときに合わせてこちらを使う
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

ここまでのまとめ

#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

WASMインスタンス→ブラウザ、の場合

  • ブラウザ側で、線形メモリ上に文字列を置くバッファを確保する
  • そのバッファの位置を返却先としてWASMの関数に渡す
  • WASMの関数はそのバッファに文字列を更新する
  • 戻ってきたら同じ位置の線形メモリを通して文字列を取り出す
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

ブラウザ→WASMインスタンス、の場合

  • ブラウザ側で、線形メモリ上に文字列のバイト列をコピーする
  • そのバッファの位置をWASMの関数に渡す
    • 必要なら長さも渡す
  • WASMの関数はその情報から文字列を取り出して扱う
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

文字列の扱い・応用編

#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

以下を実装しよう

  • WASMによるワードカウントモジュールを作る
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

設計しよう

  • フォーム→テキストを取り出す
  • テキストデータ→WASMに渡す
  • WASM→文字列を復元する
  • 数えた結果は数値なので、関数の戻り値でやり取りできる
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

Rust側をまずは書いておく

  • プロジェクトをもう一つ作る
$ cargo new --lib wordcount-wasm
# ... Cargo.tomlなどを編集

ダミーの値を返してみよう

#[no_mangle]
pub unsafe fn wordcount(src: *mut u8, srclen: i32) -> i32 {
    return 100;
}
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

ブラウザ側実装

<html>
  <head>
    <title>My first wasm</title>
    <script async type="text/javascript">
      // ...次のスライドへ
    </script>
  </head>
  <body>
    <h1>WordCounter!</h1>
    <textarea rows="10" cols="80"
      id="target">Default text...</textarea>
    <input type="button" onclick="fire();" value="Count!"/>
  </body>
</html>
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

JavaScript:

const obj = {};
window.wasmInstance = null;
window.wordcount = null;
window.fire = function() {
    if (wasmInstance === null) { return; }
    let input = document.getElementById("target").value;
    let offset = wasmInstance.exports.__heap_base;
    let memory = wasmInstance.exports.memory;
    let buffer = new Uint8Array(memory.buffer, offset, input.length);
    for( var i = 0; i < input.length; i++ ) {
        buffer[i] = input.charCodeAt(i);
    }
    let result = wordcount(offset, input.length);
    let msg = "result = " + result.toString();
    alert(msg);
}
WebAssembly.instantiateStreaming(fetch("./wc.wasm"), obj).then(
    (wasm) => {
        window.wasmInstance = wasm.instance;
        window.wordcount = wasm.instance.exports.wordcount;
        console.log("loaded!")
    },
);
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

一度通しで動かす

  • wasm-wordcount crateをwasm32-unknown-unknownでビルド
  • できたwasmファイルをhtmlと同じディレクトリに wc.wasm として保存

#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

一度通しで動かす

  • ダミーの値がポップアップすればOK

#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

Rust側のワードカウンター

  • あくまで一例 / 真面目なエラー処理は...宿題です
#[no_mangle]
pub unsafe fn wordcount(src: *mut u8, srclen: i32) -> i32 {
    let src: &[u8] = from_raw_parts(src, srclen as usize);
    if srclen == 0 {
        return 0; // empty
    }
    let mut count = 1;
    for c in src.iter() {
        let c = *c;
        if c == 0x20 { // space
            count += 1;
        }
    }
    count
}
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

動作確認しよう

  • もう一度wasm32-unknown-unknownでビルド、コピー

#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

こんなにめんどくさいの?

  • と思ったあなたへの答え
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

実際のWASMの現場では

  • wasm-bindgenがラップしてくれます
  • 詳細なドキュメントは公式に譲りますが、今回は簡単に試す
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

wasm-bindgen を導入する

  • wasm-pack が必須になるのでインストール
  • wasm-bindgen を依存に追加
$ cargo install wasm-pack
$ cargo add wasm-bindgen
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

wasm-bindgen 対応関数

  • #[wasm_bindgen] attributeが使える
// &str は戻り値には使えない
#[wasm_bindgen]
pub fn hello_bg() -> String {
    "Hello, world".to_string()
}

#[wasm_bindgen]
pub fn welcome_bg(src: &str) -> i32 {
    src.as_bytes()[0] as i32
}
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

wasm-bindgen のコンパイル

$ wasm-pack build --target web
# ./pkg に一式が生成されるのでコピー
$ cp -r pkg/ web/pkg/
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

ブラウザからの利用

<script async type="module">
    import init, {hello_bg, welcome_bg} from './pkg/hello_string.js';
    await init();
    let result = hello_bg();
    console.log(result);

    let result2 = welcome_bg("Welcome world");
    console.log("code of W is: " + result2.toString());
</script>
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

今回、あえて低レイヤなやり方をしています

  • 抽象度が低い実装に触れることのメリット:
    • 実際の仕事では抽象度の高いツールを使っても理解しやすくなる
    • トラブルシュートやデバッグの解像度が上がる
  • このメリットを重視して、やや遠回しだけどなるべく抽象化しないやり方をしています
  • 内部を理解しつつ現場で使う際は最適な方法を取りましょう
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

まとめ

#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

今日のまとめ

以下のような内容を学んだはず

  • WASMにおける「文字列」
  • 線形メモリの基本
  • テキストデータをやり取りする際の実装例
  • 応用
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

演習課題

#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

演習課題

  • 1) upcase() という、ブラウザから来たASCIIの文字列を全て大文字にして返却する関数をWASMで実装しましょう。以下のようなシグネチャになるはずです。
    • Rustレベルでは:
      upcase(srcp: *const u8, slen: i32, destp: *mut u8, dlen: i32)
    • WASMレベルでは:
      upcase(srcp: i32, slen: i32, destp: i32, dlen: i32) -> nil
  • 2) 1) をブラウザで呼び出して動作確認しましょう。
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

演習課題(上級編)

  • 3) wasm-bindgen が生成してくれるマクロコードを確認し、文字列についてどのようなラッパーコードが生成されるか確認してみよう...。
  • めちゃ大変なので、いつかという気持ちでやってみましょう
#2 One More Step into WebAssembly
WebAssemblyでScratchプラグインを作ろう!

次回

  • #3 WebAssembly モジュールでより複雑なことをしよう
    • 予定: 12/16(月) 19:00 start
    • キーワード:
      • 画像処理
      • グレースケール化
    • 次回は平日夜間開催なのでご注意ください!
  • TBA: 2025年の予定
#2 One More Step into WebAssembly

バイナリが一致しないこともあるが、opt-level = "s" かは確認

memoryが 2 になっている理由は不明、普通に後述のコードで動くため保留する