🔖

Go1.24からWasmで使える型が緩和される話

2024/12/01に公開

WebAssembly Advent Calendar 2024の1日目の記事になります.

先月(11月2日)から行われていた技術書典17にて,WebAssembly Cookbook vol.2[1] (新刊)を配布しました.

https://techbookfest.org/product/7CHqqtaeaRYrwDwQNCX0T7

Cookbook vol.2の第1章では,Go1.24から新しくサポートされるgo:wasmexportディレクティブについて紹介しています.
go:wasmexportディレクティブがサポートされることで,Pure GoでもWASI 0.1のリアクターモジュール[2]を作ることができるようになります.

Wasmで扱える型を緩和するプロポーザル

Go1.24では,go:wasmexportディレクティブのサポート以外にも,GoがWasm上で扱える型の緩和も予定されています.

https://github.com/golang/go/issues/66984

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を作ることが少しだけ簡単になるため,楽しみなアップデートの一つです.

脚注
  1. CookbookシリーズはWebAssemblyの活用事例などをまとめたいと思い執筆しているシリーズで,今回初の合同誌として第4章をはくすけ (@hacusk)さんに執筆していただきました. ↩︎

  2. プログラムを実行するためのmain関数を持つコマンドモジュールではなく,コマンドモジュールから利用するためのライブラリーモジュールのこと. ↩︎

株式会社モニクル

Discussion