Go1.24からWasmで使える型が緩和される話
WebAssembly Advent Calendar 2024の1日目の記事になります.
先月(11月2日)から行われていた技術書典17にて,WebAssembly Cookbook vol.2[1] (新刊)を配布しました.
Cookbook vol.2の第1章では,Go1.24から新しくサポートされるgo:wasmexportディレクティブについて紹介しています.
go:wasmexportディレクティブがサポートされることで,Pure GoでもWASI 0.1のリアクターモジュール[2]を作ることができるようになります.
Wasmで扱える型を緩和するプロポーザル
Go1.24では,go:wasmexportディレクティブのサポート以外にも,GoがWasm上で扱える型の緩和も予定されています.
Go1.23でWasmで扱える型の種類
Go1.21からgo:wasmimportディレクティブがサポートされ,GoでもJSを介さずに直接Wasmの値を利用することができるようになりましたが,現時点では次の型しが扱うことができません.
| Go value | Wasm value | 
|---|---|
| int32,uint32 | i32 | 
| int64,uint64 | i64 | 
| float32 | f32 | 
| float64 | f64 | 
| unsafe.Pointer | i32 | 
そのため,GoからWasmのインターフェースを経由して文字列を渡そうとした場合,次のようなコードを書く必要があります.
// main.go
package main
import "unsafe"
//go:wasmimport console log
func consoleLog(ptr unsafe.Pointer, len int32)
func main() {
    msg := "Hello, 世界!"
    ptr := unsafe.Pointer(unsafe.StringData(msg))
    len := int32(len(msg))
    consoleLog(ptr, len)
}
Goの文字列からポインターと長さを取得し,それらをインポート関数へと渡しています.
このGoのコードからビルドされるWasmを実行するJSのコードは,次のように書くことができます.
// bootstrap.js
import "./wasm_exec.js";
const go = new Go();
const { instance } = await WebAssembly.instantiateStreaming(
  fetch(new URL("main.wasm", import.meta.url)),
  {
    ...go.importObject,
    console: {
      log(ptr, len) {
        console.log(
          new TextDecoder("utf-8").decode(
            new Uint8Array(instance.exports.mem.buffer, ptr, len),
          ),
        );
      },
    },
  },
);
go.run(instance);
JSのコードでは,Wasmから文字列のポインターと長さを受け取り,Wasmのメモリー上から文字列の範囲を取得してJSのstringにデコードしています.
実際にこのコードを実行すると次のような実行結果を得ることができます.
$ deno run --allow-read bootstrap.js
Hello, 世界!
扱える型の緩和
このようにGoを用いてWasmを扱う場合,Wasmの外側とのデータのやり取りを行うためにメモリー上のデータ構造を意識する必要があり,unsafe.Pointerにキャストしたりと冗長なコードを書く必要がありました.
今回紹介するプロポーザルでは,Goで扱える型を緩和することを提案しており,Go1.24以降では次の型をWasmで扱うことができるようなる予定です.
| Go value | Wasm value | 
|---|---|
| bool | i32 | 
| int32,uint32 | i32 | 
| int64,uint64 | i64 | 
| float32 | f32 | 
| float64 | f64 | 
| uintptr,unsafe.Pointer,*T,*struct,*[...]T | i32 | 
| string | (i32, i32)※Goからの引数に限定 | 
これにより,unsafe.Pointerにキャストしなくともポインターを直接扱うことができるようになるなど,Go側のコードを書くコストが軽減されることが期待されます.
また,限定的ではありますが,stringを文字列のポインターと長さをもつタプル(i32, i32)としてサポートするようになります.
そのため,先ほどのGoのコード例を次ように書き換えることができます.
// main.go
package main
//go:wasmimport console log
func consoleLog(msg string)
func main() {
    msg := "Hello, 世界!"
    consoleLog(msg)
}
stringのサポートは本記事執筆時点ですでに実装されているため,最新のGoをビルドすることで試すことができます.
$ go version
go version devel go1.24-733df2bc0a Mon Nov 25 02:23:41 2024 +0000 darwin/arm64
$ deno run --allow-read bootstrap.js
Hello, 世界!
まとめ
Go1.24のWasm関連のアップデートの目玉はなんといってもgo:wasmexportディレクティブのサポートですが,それ以外にもこのような嬉しい改善が行われています.
Goを使ってWasmを作ることが少しだけ簡単になるため,楽しみなアップデートの一つです.
- 
CookbookシリーズはWebAssemblyの活用事例などをまとめたいと思い執筆しているシリーズで,今回初の合同誌として第4章をはくすけ (@hacusk)さんに執筆していただきました. ↩︎ 
- 
プログラムを実行するための main関数を持つコマンドモジュールではなく,コマンドモジュールから利用するためのライブラリーモジュールのこと. ↩︎




Discussion