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

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

#1 Our First WebAssembly Run

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

Uchio Kondo (@udzura)

  • 福岡市エンジニアカフェ ハッカーサポーター
  • フィヨルドブートキャンプ アドバイザー
  • RubyKaigi Speaker (2016 ~)
  • 共同翻訳: 『入門eBPF』(オライリージャパン)
  • 好きなYouTuber: けんた食堂

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

最近やってること: 自作 WASM Runtime

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

今日やること

  • WebAssemblyとは?
  • WASM のバイナリ構造・セクションについて概要
  • 手元でWASMを動かす
    • その1
    • その2 (WASI)
  • ブラウザで動かす
  • importとexportの話をする
    • ブラウザで連携するには?
    • WASIの理解
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

WebAssembly(WASM)の概要

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

WebAssembly ってそもそも何?

  • なんか... ブラウザで動くやつ...
  • 「ブラウザ上でJS以外の言語を動かすことができる技術」
    • 最近使われてるらしい
    • Ruby、Python、Kotlin、他色々対応しつつあるらしい
  • WASMとも呼ばれる。だいたい同じものを指す
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

具体的なユースケースから

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

Unity3d

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

Goolge Meet

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

Linux on Browser

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

WASMが得意なこと

  • 高速な処理
  • 言語を選ばず実装可能
  • ポータブル(ブラウザでもサーバでも組み込んで動く)
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

ブラウザの外で動く例

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

WASMの作り方

  • RustのWASM backend
    • wasm_bindgenのようなツール/SDKも豊富
  • emscripten
    • C/C++ をWASMにする、ブラウザとのグルー部分も生成する
  • AssemblyScript
    • TypeScript のサブセット
  • 各言語での個別の対応
    • Ruby、Python、Go、Kotlin、Swift...
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

WASM のバイナリ構造とセクション

  • 概要の次でいきなりバイナリの話かよ!
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

WASM のサンプルバイナリ

  • とりあえずダウンロードしてみよう
wget \
  https://github.com/udzura/engineer-cafe-lab-wasm-course/raw/refs/heads/master/samples/hello_wasm.wasm
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

WASM の「中身」を確認するコマンド

  • wasm-objdump (WABTに含まれる)を使う
wasm-objdump -x hello_wasm.wasm
# たくさん表示されるので、grep、lessなど組み合わせる
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

WASM バイナリ(モジュール)の構造

  • 先頭8バイトのプリアンブル
    • Magic Number + version
  • その後ろにセクションが複数存在
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

WASM バイナリのセクション

$ wasm-objdump -x /hello_wasm.wasm | grep -E '^[A-Z]'
Section Details:
Type[13]:
Import[1]:
Function[54]:
Table[1]:
Memory[1]:
Global[3]:
Export[4]:
Elem[1]:
Code[54]:
Data[1]:
Custom:
...
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

例えばImport?

  • -j オプションで特定のセクションを表示
$ wasm-objdump -x -j Import hello_wasm.wasm       

hello_wasm.wasm:	file format wasm 0x1
module name: <hello_wasm.wasm>

Section Details:

Import[1]:
 - func[0] sig=3 <hoge> <- env.hoge
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

セクションの例

  • 代表的なもののみ ref
Name 詳細
Type 使う型・シグネチャの定義
Function 定義されている関数定義
Table 関数のロケーションなど、線型メモリ外のオブジェクトの配置情報
Memory 線形メモリの情報を宣言
Global グローバル変数など
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

セクションの例(2)

  • 代表的なもののみ ref
Name 詳細
Import 実行時にimport必要な関数の情報
Export exportされている関数の情報
Elem インスタンス化時に参照するテーブルのエレメントを宣言
Code WASMの実際のバイトコード部分
Data プログラムで使う初期化済みデータ
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

WASM を作って動かそう

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

My first project

$ cargo new hello-wasm --lib
$ cd hello-wasm
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

ビルド設定を少し編集する

$ code Cargo.toml
[package]
name = "hello-wasm"
version = "0.1.0"
edition = "2021"

[dependencies]

[lib] # ここを変更
crate-type = ["cdylib", "rlib"]
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

関数を実装してみよう

  • src/lib.rs のコードを一通り消して、これだけにする
#[no_mangle]
pub fn fib(n: i32) -> i32 {
    match n {
        ..=-1 => {
            0
        }
        ..=1 => {
            1
        }
        _ => {
            fib(n-1) + fib(n-2)
        }
    }
}
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

Rustすぎる...場合

  • こう書いても同じ結果です
#[no_mangle]
pub fn fib(n: i32) -> i32 {
    if n <= -1 {
        return 0;
    }
    if n <= 1 {
        return 1;
    }
    return fib(n-1) + fib(n-2);
}
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

ビルドしよう

$ cargo build --target wasm32-unknown-unknown
...
hello-wasm/Cargo.toml
   Compiling hello-wasm v0.1.0 (/Users/udzura/hello-wasm)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.90s

$ file ./target/wasm32-unknown-unknown/debug/hello_wasm.wasm 
.../hello_wasm.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

wasmtime で動作確認する

$ wasmtime --invoke fib ./target/wasm32-unknown-unknown/debug/hello_wasm.wasm 20 
# warning: using `--invoke` with a function
#   that takes arguments is experimental and may break in the future...
10946
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

今度は「main」を実装しよう

  • Cargo.toml[lib] を以下に変更
[lib]
# crate-type = ["cdylib", "rlib"]
crate-type = ["lib"]
  • src/main.rs を作成
fn main() {
    println!("Hello Engineer Cafe!")
}
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

ビルドしよう

$ cargo build --target wasm32-wasi
...
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

wasmtimeで動かす

$ wasmtime ./target/wasm32-wasi/debug/hello-wasm.wasm 
Hello Engineer Cafe!

※ なぜかバイナリ名がさっきと違う。cargoの規約?と思われるのでスルー

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

2つのWASMバイナリの違い

fib

Export[4]:
 - memory[0] -> "memory"
 - func[0] <fib> -> "fib"
 - global[1] -> "__data_end"
 - global[2] -> "__heap_base"

hello

Export[3]:
 - memory[0] -> "memory"
 - func[5] <_start> -> "_start"
 - func[7] <__main_void> -> "__main_void"
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

2つのWASMバイナリの違い

fib

Section not found: Import

hello

Import[4]:
 - func[0] sig=7 <_ZN4wasi13lib_generated...> <- wasi_snapshot_preview1.fd_write
 - func[1] sig=4 <__imported_wasi_...> <- wasi_snapshot_preview1.environ_get
 - func[2] sig=4 <__imported_wasi_...> <- wasi_snapshot_preview1.environ_sizes_get
 - func[3] sig=1 <__imported_wasi_...> <- wasi_snapshot_preview1.proc_exit
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

WASI? ワシには難しくて...

  • WASI = WebAssembly System Interface
  • では、WASIとは何か?を踏み込んだところは一旦保留して
    • 先に進みます
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

ブラウザで動かそう

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

first project の方のバイナリを使う

  • target/wasm32-unknown-unknown/debug/hello_wasm.wasm の方を使う
    • fib() を実装した方です
  • web というディレクトリを新たに作ってそこへコピー
$ mkdir web
$ cp target/wasm32-unknown-unknown/debug/hello_wasm.wasm web
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

index.html を作ろう

  • web/index.html を編集
<html><head>
    <title>My first WASM</title>
    <script async type="text/javascript">
        const obj = {
            env: {},
        };
        WebAssembly.instantiateStreaming(fetch("./hello_wasm.wasm"), obj).then(
            (obj) => {
                let answer = obj.instance.exports.fib(20);
                alert("answer: fib(20) = " + answer.toString());
                console.log("debug: it works!");
            },
        );
    </script></head>
    <body><h1>Wasm working on browser</h1></body>
</html>
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

instantiateとはなんぞや?

WebAssembly.instantiateStreaming() 関数は、ソースのストリームから直接 WebAssembly モジュールをコンパイルしてインスタンス化します。これは、 WASMコードをロードするための最も効率的で最適な方法です。

  • 2つの引数: source, importObject
  • プロミスを返すので、WASMをロードしてからの処理をつなげる
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

手元にサーバを立てて確認しよう

$ cd web
$ python3 -m http.server 8080

# 手元にRubyが入ってる人はこっちでもOKです
# お好きな方で
$ ruby -run -e httpd -- .
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

ブラウザで
fibを計算できました!

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

シン・importとexport

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

少し高度な話に入ります

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

まずは: 先ほどの「hello world」

  • 文字列を出力する方のWASMバイナリをブラウザで動かしてみる
  • fibが動いたけん楽勝やろ?
  • 同じように web ディレクトリにコピーし、 index.html を加工してロードする
$ cp ./target/wasm32-wasi/debug/hello-wasm.wasm web/hello2.wasm
WebAssembly.instantiateStreaming(fetch("./hello2.wasm"), obj).then(
    (obj) => {
        // ロードOKだけログに出してみよう
        console.log("debug: load OK!");
    },
);
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

エラーになります

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

エラーメッセージを覚えておいてください

Uncaught (in promise) TypeError: WebAssembly.instantiate():
Import #0 "wasi_snapshot_preview1": module is not an object or function
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

importとexportの話

  • エラーメッセージを翻訳すると: Import すべきモジュールが指定されていない
  • さっきは不要だったけど、なぜ?
  • 逆に対になる Export もあるけど、つまりこれは何?
// What is exports ??
let answer = obj.instance.exports.fib(20);
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

importとexportを使うコードを書いてみよう

  • あえて両方を使ってみる
  • 混乱しないように新しいcrateを作っておこう
$ cargo new --lib hello-more
$ cd hello-more
$ code Cargo.toml # 前のスライド参照
$ code src/lib.rs
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!
extern "C" {
    fn my_callback(n: i32);
}

#[no_mangle]
pub unsafe fn fib(n: i32) -> i32 {
    let r = match n {
        ..=-1 => {
            0
        }
        ..=1 => {
            1
        }
        _ => {
            fib(n-1) + fib(n-2)
        }
    };
    my_callback(r);
    r
}
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

まずはビルドしてみよう

$ cargo build --target wasm32-unknown-unknown
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

セクションを確認してみよう

$ wasm-objdump -x -j Export ./target/wasm32-unknown-unknown/debug/hello_more.wasm 
# or Import

Export[4]:
 - memory[0] -> "memory"
 - func[1] <fib> -> "fib"
 - global[1] -> "__data_end"
 - global[2] -> "__heap_base"

Import[1]:
 - func[0] sig=3 <my_callback> <- env.my_callback
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

これをブラウザで使うには?

  • 前のコード(fib() を実行した時のhtml)をそのまま流用して動かしてみる
  • (また)こういうエラーが出る
index.html:1 Uncaught (in promise) TypeError: WebAssembly.instantiate(): 
Import #0 "env": module is not an object or function
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

importObject 引数の話

  • instantiateStreaming() の2番目の引数 importObject を経由して関数を渡すことができる
const importObject = {
    env: { .... },
};
WebAssembly.instantiateStreaming(
    fetch("./hello.wasm"),
    importObject, // <- Here
).then(...)
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

「コールバック」をWASMに渡してみよう

const obj = {
    // モジュール名
    env: {
        // これがコールバック
        my_callback: function (value) {
            console.log("callback value = " + value.toString());
        }
    },
};
WebAssembly.instantiateStreaming(fetch("./hello3.wasm"), obj).then(
    (obj) => {
        let answer = obj.instance.exports.fib(20);
        console.log("answer: fib(20) = " + answer.toString());
    },
);
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

動作確認

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

ところで

  • 「WASIについては置いておきましょう」と言いました
    • この段階で、WASIについて理解するための道具が整理できました
  • 少しだけ寄り道してみましょう
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

改めて: WASI

  • 教科書的説明

Webブラウザ以外の実行環境において、WebAssemblyに備わっていることが望ましいさまざまなシステムインターフェイスを策定し実現するもの -- Publickey

  • WASMからシステムへのインタフェースの規約
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

WASIとは結局何?

  • 「OSのような環境」で動かすためにあらかじめ規定されたimport関数のセット(=モジュール)、と見なすことができる
    • まさにPOSIXのような感じ
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

「WASI対応」とは結局

  • システムに合わせて、import用の関数のセットを実装することに他ならない
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

ブラウザを「WASI対応」させる

  • WASIのimport関数を実装したものをブラウザで用意すればいい
    • 不要なものは何ならダミーでもいい
  • OSSでもいくつか存在する
  • 例えば:
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

さっきの「hello world」に再挑戦

<script type="module">
    import {WASI, File, OpenFile, ConsoleStdout} 
      from 'https://cdn.jsdelivr.net/npm/@bjorn3/[email protected]/+esm'
    const fds = [
        new OpenFile(new File([])), // 0
        ConsoleStdout.lineBuffered(msg => console.log(`[WASI stdout] ${msg}`)), // 1
        ConsoleStdout.lineBuffered(msg => console.warn(`[WASI stderr] ${msg}`)), // 2
    ];
    const wasi = new WASI([], [], fds);
    const obj = {
        "wasi_snapshot_preview1": wasi.wasiImport,
    };
    const wasm = await WebAssembly.compileStreaming(fetch("hello2.wasm"));
    const instance = await WebAssembly.instantiate(wasm, obj);
    wasi.start(instance);
</script>
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

まとめ

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

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

  • WASMの概要、セクション
  • WASMのビルドの仕方
  • WASMの動かし方(コマンド、ブラウザ)
  • 関数のimport/exportの基本
    • WASIの触りも確認した

※ 資料が足りてないところは、コードサンプルや質問で!

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

演習課題

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

演習課題

  • 1) is_prime(n: i32) -> i32 という、数値を受け取ってそれが素数なら 1 、そうでなければ 0 を返す関数をWASMで実装してください。
    • その関数を実装したWASMにコールバックを渡せるようにし、 console.log() を使ってどの数で試しに割ろうとしたかをデバッグできるようにしてください。
  • 2) WASMに値を渡したり、受け取った値を表示する際に、DOMと連携して使いやすくしてください。
    • 例えば、入力をinput要素から取る、div要素の中身を更新する、など。
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

次回

  • #2 WebAssembly モジュールとブラウザを連携させよう
    • 予定: 11/23(土) 14:00 start
    • キーワード:
      • 文字列
      • 線形メモリ
  • ご連絡: #3 の日程が平日夜になりました。 12/16(月) 19:00 ~
    • ご了承の上、ご調整をお願いします 🙇
#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

参考図書について

#1 Our First WebAssembly Run
WebAssemblyでScratchプラグインを作ろう!

参考図書について

  • 実践Rust入門
    • 今後、少し難しいRustのコードが出てくるので...
    • @udzura が読んだ本
    • 基本概念を理解するのに良い本
#1 Our First WebAssembly Run

モジュールは後からも出てきて、モジュールって何だよ!という質問が出てきそうだけど WASMの文脈においては「互いに呼び合える、関数を束ねたもの」ぐらいの概念と思っておけば良さそう なので、WASMで実装した関数の塊もモジュールだし、 JavaScript側でオブジェクトに束ねたものもモジュール。 ES Moduleと比較すると良さそうだけど、識者の意見を聞きたい感じ...