これまでにカンファレンスなどで発表した資料をここにまとめます。 随時更新です。
最終更新: 2025-01-18
↓↓↓ 発表日降順で並んでいます
続きを読むJavaScript から wasm の関数を呼び出して結果の文字列を受け取る方法を書き残します。
前編はこちら。
前後編のコードを全部まとめたリポジトリがここにあります。
まずは 前の記事 にも挙げた前提知識をおさらいしましょう。
詳しくはこちらを参照してください。
まずは wasm のコンパイル前のコードを見てみましょう。
今回は TinyGo で wasm をコンパイルします。なぜ TinyGo かと云うと //go:wasmimport
ディレクティブが使えるからです。
wasm には文字列型が無いために Go から string をそのまま return することはできません。代わりにポインタとバイト列の長さの組みを返して、JavaScript 側で指定した領域の線形メモリの読むことで文字列を受け渡します。
ところが2025年1月現在は wasm にコンパイル可能な関数にかなりの制限があります。なんと複数の値を返すことができません。苦肉の策としてポインタを返す関数とバイト列の長さを返す関数を別々に定義します。
// 複数の関数 `returnString()`, `getBufSize()` で状態を共有するためのグローバル変数 var bufSize uint32 //go:wasmexport returnString func returnString() unsafe.Pointer { str := "やっほー From WebAssembly!🤟" // 文字列をバイト列に変換 buf := []byte(str) // バイト列のサイズを取得 // getBufSize() から参照できるようにグローバル変数に保存 bufSize = uint32(len(buf)) // バイト列の先頭のポインタを取得して返す // 本当は多値 `uint32(ptr), bufSize` を返したいが // 2025年1月現在 TinyGo は単一の uint32 しか返せない return unsafe.Pointer(&buf[0]) } //go:wasmexport getBufSize func getBufSize() uint32 { return bufSize }
returnString がポンタを返します。getBufSize はバイト列の長さを返します。
二つの関数の間で状態を共有するためにグローバル変数 bufSize を定義しておいて、都度書き込んだり読み出したりします。くそダサい実装ですね。仕方ない。
続いて JavaScript から文字列を読み出しましょう。
// wasm から文字列をメモリに書き込み、その開始位置のポインタを受け取る const ptr = wasm.exports.returnString(); // wasm から文字列の長さを受け取る const len = wasm.exports.getBufSize();
前述したとおり TinyGo では一度に複数の値を返せないので、二つの関数を立て続けに呼び出して1つずつ値を受け取ります。
ちなみにこれは wasm の制約ではなく TinyGo コンパイラの制約です。wasm では (result u32 u32)
のようにして多値を返すことは可能です。
// wasm モジュールは線形メモリを exports.memory として露出している // このメモリをやりとりに使う const memory = new Uint8Array(wasm.exports.memory.buffer);
wasm モジュールは外界とデータをやりとりするために線形メモリを exports.memory
としてモジュールの外に露出しています。このメモリから文字列を読み取りたいのでまずは Uint8Array に変換します。
// ptr, len で指定された範囲のバイト列を読み取る const utf8str: Uint8Array = memory.slice(ptr, ptr + len); // メモリには UTF-8 形式で書き込まれているので // デコードして JavaScript の文字列 (UTF-16 形式) に変換する return new TextDecoder().decode(utf8str);
memory のうち ptr
~ ptr + len
の範囲を切り取り、TextDecoder でデコードします。
Go では文字列が UTF-8 形式でエンコードされています。一方で JavaScript では UTF-16 形式で文字列を扱っているためにエンコード変換が必要です。
デモコードは deno で実行することを想定して書きました。deno で実行してみます。
$ deno run --allow-read receiveStringFromWasm.ts
やっほー From WebAssembly!🤟
いい感じです。
今回のデモコードにはメモリの取り扱いの不備があります。
Go からローカル変数のポインタを返していますが、このメモリは Go の GC によって (JavaScript 側で参照する前に) 回収されてしまう恐れがあります。このあたりは Go の GC と wasm の仕様の噛み合わせが悪い部分ですね。
wasm から JavaScript へ文字列を受け取る方法を見てきました。
Go 側で多値を返せないせいでとてもダサいコードになっていましたね。今後に期待。
私からは以上です。
JavaScript から wasm の関数に文字列を渡して呼び出す方法を書き残します。
後編はこちら。
前後編のコードを全部まとめたリポジトリがここにあります。
wasm で扱えるデータ型は i32, i64, f32, f64 のみです。ざっくりと整数型と浮動小数点数型です。つまりそもそも wasm 世界で文字列型を直接扱うことは出来ないわけです。
そのため整数型や浮動小数点数型よりも複雑なデータを扱おうとすると、ポインタ と データの長さ の長さの組みを使って メモリ上の連続した領域 としてデータを扱うことになります。
さて wasm がメモリ上の連続した領域しか扱えないとのことなので「じゃあ文字列をメモリ上の連続した領域として扱うか」となる訳ですが。次に困るのは文字列のバイト列表現がプログラミング言語によってまちまちということです。
バイト列表現は文字コードとエンコード形式によって決まります。例を見てみましょう。
Go の string
や Rust の &str
は Unicode を UTF-8 エンコードしたものを内部表現としています。
例えば 'Hello, 世界🌍'
という文字列を UTF-8 エンコードして16進数で表示すると、
48 65 6C 6C 6F 2C 20 E4 B8 96 E7 95 8C F0 9F 8C 8D
こんな感じになります。
JavaScript の string
や Java の String
は Unicode を UTF-16 エンコードしたものを内部表現としています。
例えば 'Hello, 世界🌍'
という文字列を UTF-16 エンコードして16進数で表示すると、
00 48 00 65 00 6C 00 6C 00 6F 00 2C 00 20 4E 16 75 4C D8 3C DF 0D
こんな感じになります。
そんなわけで「とあるメモリ上の連続した領域」に「文字列をとある形式でバイト列表現したもの」を書いたり読んだりして文字列を受け渡すことになります。
そのとき wasm を呼び出す側と wasm のコンパイル元とで文字列のバイト列表現の形式を示し合わせておかなければいけません。それをせず、例えば UTF-16 エンコードしたバイト列を UTF-8 デコードして読み出そうとしてしまうと正しく文字列を受け渡せません。
「WebAssembly の規格として定めれた表現で受け渡せばそのような事故はおこらないのでは?」と思いますよね。はい、wasm を介して異なる言語間でデータをやりとるするための規格化は The Wasm Interface Type (WIT) として進められています。が、2025年1月現在はまだ策定中で "定められた表現" というものは存在していません。
前置きが長くなりましたが JavaScript から wasm に文字列を渡しましょう。
今回は TinyGo を使ってコンパイルした wasm バイナリを前提とします。なぜ TinyGo かと云うと //go:wasmimport
ディレクティブが使えるからです。
他の言語でも理屈は同じですが前述したように、その言語がどんなバイト列を文字列として認識できるか気にする必要があります。
JavaScript コードの全文を見せます。
wasm モジュールの読み込み (loadModule
関数) 、文字列を線形メモリに書き込む (writeStringToMemory
関数)、本題の関数の呼び出し (wasm.exports.printString
の呼び出し) に処理を分けています。
それぞれ見ていきましょう。
loadModule
関数)wasm の読み込みについては TinyGo の公式ドキュメントに解説を譲ります。
import './wasm_exec.js';
でインポートしている JS ファイルは TinyGo をインストールすると同梱されているものです。TinyGo で wasm をコンパイルするチュートリアルをやれば目にするはず。
writeStringToMemory
関数)// JavaScript はデフォルトで UTF-16 形式で文字列を扱う // 今回は UTF-8 形式でやりとりしたいのでエンコードしておく let utf8str: Uint8Array = new TextEncoder().encode(str);
JavaScript では内部的に UTF-16 形式で文字列を取り扱います。今回は文字列を受け取る側 (Golang) の都合に合わせて UTF-8 形式でメモリに書き込んであげたので、まずは文字列を UTF-8 形式のバイト列にエンコードします。
// wasm モジュールは線形メモリを exports.memory として露出している // このメモリをやりとりに使う const memory = new Uint8Array(wasm.exports.memory.buffer);
書き込み先は exports.memory
です。wasm モジュールは外界とデータをやりとりするために線形メモリを exports.memory
としてモジュールの外に露出しています。メモリを露出させて直接書き換えさせるなんてワイルドですね。
さて問題は exports.memory
の どこに 書き込むかということです。何も考えず memory[0]
から書き込みを始めると wasm モジュールが使用しているメモリを破壊してしまうかもしれません。
// TinyGo が export してくれる関数 // 引数で指定したバイト数のメモリ領域を確保し、その先頭アドレスを返す const ptr = wasm.exports.malloc(utf8str.length);
メモリ破壊を避けるため、書き込み可能なメモリ領域を wasm モジュール側で 確保してもらい、確保済みのメモリの先頭のアドレスを教えてもらいます。TinyGo でコンパイルした wasm においては exports.malloc
がその仕事をしてくれます。
exports.malloc
は全ての環境で提供されるものではありません。例えば Go の標準コンパイラで吐いた wasm は malloc を export していません。場合によっては malloc 相当の関数をユーザーが定義する必要があります。
// 確保したメモリ領域に UTF-8 形式のバイト列を書き込む memory.set(utf8str, /* offset: */ ptr);
JavaScript の TypedArray は Array に無い便利なメソッドを実装してくれています。Uint8Array.prototype.set
は ptr の位置から先に utf8str の値を書き込んでくれます。
さて exports.memory
は JavaScript 世界と Wasm 世界で共有されているので、ここまでで memory
の参照は手放してしまって問題ありません。
大事なのは ptr
の値と utf8str.length
で、この数値の組みを手がかりにして wasm 側で線形メモリのどこからどこまで文字列が書き込まれているのかを知ることができます。
// ポインタとバイト列の長さの組みを返す // この組みを wasm の関数に渡すことで文字列が書き込まれた領域を知らせる return [ptr, utf8str.length];
というわけで [ptr, utf8str.length]
を return すれば writeStringToMemory 関数でするべき仕事は完了です。
wasm.exports.printString
の呼び出し)無事に文字列を線形メモリに書き込めたので、文字列を渡して wasm 関数を呼び出しましょう。
今回は渡された文字列を標準出力に印字するだけの関数 printString
を呼び出します。ちなみにこの関数はユーザー定義関数で、今回のデモのためにちゃちゃっと実装したものです。
// ユーザー定義関数 `printString(ptr: i32, len: i32)` を呼び出す // ポインタとバイト列の長さの組みを渡すことで文字列を書き込んだ領域を知らせる wasm.exports.printString(ptr, len); // => Hello, 世界🌍
はい、「文字列を渡して」と言いつつ (ptr, len)
を渡しています。前述したとおり wasm には文字列型が無いのでこのようにする以外に無いですね。
デモコードは deno で実行することを想定して書きました。deno で実行してみます。
$ deno run --allow-read passStringToWasm.ts
Hello, 世界🌍
はい、うまく動きました。
おそらくですが型情報を剥がして JavaScript にすれば Node.js やブラウザでも動きます。
今回のデモでは Go で実装した printString
を呼び出しました。Go 側の実装も見てみましょう。
package main import "fmt" func main() { c := make(chan struct{}) <-c } //go:wasmexport printString func printString(str string) { fmt.Println(str) }
printString
のシグネチャは func(string)
です。これを wasm にコンパイルして WebAssembly テキスト形式 (wat) を見てみると、
(func $main.printString#wasmexport.command_export (type 0) (param i32 i32) ...
となっています。
コンパイル時に (string)
→ (i32, i32)
と引数が変換されているようですね。wasm 世界に文字列形式が無いためにこのような変換をしているのでしょう。
JavaScript から wasm へ文字列を渡す方法を見てきました。 大したコード量ではないですが、自前でエンコードしたりメモリに書き込んだりするの面倒くせぇですね。
私からは以上です。
React と SolidJS のリアクティブシステムがそれぞれ対照的で面白い。React は冪等性を前提にしていて SolidJS は副作用を前提にしている。真逆だ。詳しく見てみよう。
この記事においてリアクティブシステムとは 「とある変数が更新されたとき、その変数が使われている式や UI が自動的に再計算・再反映される機構」 とします。変数が更新されたらその変数を表示している部分も更新してほしいよね、ということです。
React のコンポーネントはこのような姿をしています。
// React function Counter() { const [count, setCount] = useState(0); return ( <div> <button onClick={() => setCount(count => count + 1)}>+1</button> <p>count: {count}</p> </div> ); }
React はコンポーネントが 冪等 であることを期待しています。同じ引数で呼び出す限り、何度呼んでも出力は同じものであると期待するのです。
はい、これには嘘があります。React のコンポーネントは useState を初めとした hooks によって状態を持つことができます。なのでより正確には、同じ引数・同じステートであるかぎり出力は同じであれということです。
ステートと出力については逆方向の期待もあります。コンポーネントの実行に伴ってステートが更新されるべきでは無いというのが逆方向の期待です。
ステートが更新されると React は新しい値を反映させようとします。反映のためにまずコンポーネントを呼び出します。しかしその呼び出しによって再びステートが更新され...。そうやって無限ループが引き起こされてしまいます。
このような無限ループを避けるための要求が 純粋性 です。React コンポーネントは純粋関数で、実行に伴ってステートを更新するようなことがないようにという要求です。ステートが更新されるのはもっぱらユーザー操作によって (より詳細にはイベントハンドラの実行によって) です。
冪等で純粋な関数は扱いやすくて便利です。でも肝心の DOM への反映は誰がどうやってやるのでしょうか?
それをやるのがご存じバーチャル DOM です。更新前後でバーチャル DOM の差分が取られます。その差分こそが現在の DOM に反映させるべきものです。React は最小限の差分だけを反映させます。
このように DOM の更新という名の 副作用 は React エンジンによって裏側に隠されています。
SolidJS のコンポーネントはこのような姿をしています。
// SolidJS function Counter() { const [count, setCount] = createSignal(0); return ( <div> <button onClick={() => setCount(count => count + 1)}>+1</button> <p>count: {count()}</p> </div> ); }
ほとんど React と同じですね。useState が createSignal に置き換わった程度です。ではリアクティブシステムも同じようなものなのか。そうではありません。
一番の違いは コンポーネントが1度しか呼び出されない ことです。1度しか呼ばれないので冪等かどうか考えるまでもありません。さらにバーチャル DOM は登場せず、直接的に実際の DOM に反映されます。
コンポーネントが1度しか呼び出されないのならどうやって変数の更新を反映させるのでしょうか。それを知るために SolidJS が上記の JSX からどのような JavaScript コードを生成するか見てみましょう。
// <div><button/><p/></div> は下記のようなコードに変換される const div = createElement('div'); const button = createElement('button'); div.appendChild(button); button.textContent = '+1'; button.addEventListener('click', () => setCount(count => count + 1)); const p = createElement('p'); div.appendChild(p); createEffect(() => { p.textContent = `count: ${count()}`; });
div, button, p を作り appendChild している何気ないコードです。
上記のコードの中でリアクティブのために重要なのは createEffect(() => { p.textContent = `count: ${count()}`; });
の行、ここだけです。
createEffect は SolidJS における組み込み関数です。名前から React の useEffect みたいなものかな?と思わされますが全然違います。公式ドキュメント には下記のように説明されています。
createEffect は依存する値が更新されるたびに副作用を発生させる一般的な方法です。
値が更新されるたびに createEffect に与えた関数が自動で呼び出されるようにトラッキングします。
大胆に要約すると、createEffect は1度しか呼ばれないコンポーネントの中で 「この部分だけは値が更新されるたびに再実行して!」とマークする ことを可能にします。そして createEffect に渡した関数の中で 直接的に 副作用を発生せるのが SolidJS 流です。さきほどの例では textContent の書き換え p.textContent = `count: ${count()}`;
を発生させていましたね。
<p>{count()}</p>
のような JSX を書くと内部的に createEffect を含むコードが生成されます。それだけでなく createEffect はユーザーが直接的に使うことも可能です。
// SolidJS function Counter() { const [count, setCount] = createSignal(0); createEffect(() => { // count が更新されるたびに localStorage に保存する localStorage.setItem('count', count()); }); return ( <div> <button onClick={() => setCount(count => count + 1)}>+1</button> <p>count: {count()}</p> </div> ); }
createEffect(() => { ... });
を追記して、その中で count の値を localStorage へ保存するようにしました。これにて count が更新されるたび localStorage に保存されるようになりました。
(実はブラウザが更新されたときの localStorage からの読み出しを書いていないのでこのコードは不完全です)
ここまで見てきて「SolidJS はコンポーネントを1度しか実行しない」というのも部分的に嘘であったことがわかりました。 コンポーネント全体は 1度しか呼び出しません。 createEffect でマークしたコード片は 値が更新されるたびに再実行されます。
React のコンポーネントは冪等かつ純粋であることが期待されています。コンポーネントの中でユーザーが直接的に副作用を発生させることは React 流では無く、副作用 (DOM の更新) はバーチャル DOM の裏側に隠されています。
SolidJS のリアクティブシステムにおいては副作用を発生させる関数こそが主役です。<p>{count()}</p>
のような JSX を書いたときもユーザーが直接的に createEffect を使うときも、内部的には副作用を発生させることが主目的になります。
最後に個人的な感想です。
createEffect の中でユーザー自らが副作用をハンドリングし何度も再実行させるなんて、規模が多くなったときに巨大なスパゲッティになってしまわないか?と、思いながら趣味のプロジェクトで SolidJS を使ってみています。今のところうまく動いているようです。
それにしても React の冪等・純粋な世界観からすると SolidJS の組み込み関数はどれもマジカルな挙動ですね。自明な挙動を好む人は SolidJS は肌に合わないかも。
(え?React hooks もたいがいマジカルだろって?それはそうだね)
私からは以上です。