😄

Go x WebAssembly(wasm) フロントエンドのコードをGoで書く方法

2023/12/24に公開

概要

以前から気になっていたWebAssemblyで遊んでみました。Go言語で。

本記事で話すこと。

  • WebAssemblyとは
  • GoでWebAssemblyを作る方法
    • Goのソースコードからmain.wasmをビルドし、ウェブサーバーから配信し、ブラウザ上でWebAssemblyを動かしてみるまでの方法。
  • 所感

WebAssemblyとは

  • WebAssemblyはバイナリ形式のコード。
    • WebAssembly自体は中間コードのようなもの。WebAssemblyを直接には実行できない。
    • VirtualMachineがWebAssemblyを実行可能な機械語へ変換し、実行する。
  • WebAssemblyは、ブラウザ上で機械語へ変換され、実行される。
  • WebAssemblyファイルの拡張子は.wasm
  • WebAssemblyはアセンブリ言語みたい。
    • バイナリ形式ををテキスト形式に変換することで、このアセンブリ言語らしきコードを読むことができる。詳しくはこちら
    • 勿論、このアセンブリ言語らしきコードを直接書くこともできる。
  • WebAssemblyのサイズを小さくすることが重要。
    • バイナリ形式がある一つの理由は、ファイルサイズをできるだけ小さくするため。ブラウザ上における、WebAssemblyファイルをロードする時間を小さくしたいため。
  • WebAssemblyはポータビリティがある。
    • VirtualMachineが存在していれさえすれば、WebAssemblyを実行可能。
    • ブラウザ以外の環境で実行できるのか?というと、現在のところWebAssemblyだけでは微妙っぽい。WebAssembly System Interface(WASI)などの仕組みが整ってからになるのかな。

WebAssemblyが目指していること(その1)

ブラウザ上でネイティヴアプリケーション同等の処理速度を出すことを目指してる。より機械語に近い、アセンブリ言語のようなバイナリフォーマットなので、スクリプト言語であるJavaScriptと比べると速い。

WebAssemblyが目指してること(その2)

ウェブ技術との融合。基本的には既存のウェブ技術でアプリケーションを作り、処理速度が求められる箇所だけをWebAssemblyで作る、みたいなことを目指してる。故に、JavaScriptはWebAssemblyを呼び出せるし、WebAssemblyもJavaScriptを呼び出せるようになっている。WebAssemblyは、既存のウェブ技術を置き換える(JavaScript無くして全部をWebAssemblyにするぞ!)みたいなことを目指してない。

WebAssemblyが目指してること(その他)

他にも色々ある。全部知りたい方は、下記を読むといいです。多分。

https://developer.mozilla.org/en-US/docs/WebAssembly

Go言語でWebAssembly

Go言語でWebAssemblyなSPAを作ってみる。3種類のファイルが必要。

  • Goのソースコード
  • JavaScript
  • wasm_exec.js
    • GoのWebAssemblyをブラウザ上で実行するためのサポートファイル。
    • wasm_exec.jsは、Go言語でのWebAssembly特有のサポートファイル。サポートファイルはconst go = new Go()におけるGoクラス、go.importObjectgo.runなどのヘルパー関数を提供する。ブラウザ上でGoのWebAssemblyを実行するためには、これらヘルパー関数を使用する(なぜヘルパー関数が必要となるのか?については調べられなかった)。
    • wasm_exec.jsは、GOROOTのmiscディレクトリ配下のものをコピーする。Go1.N系でmain.wasmをビルドした場合、Go1.N系のGOROOT配下にあるwasm_exec.jsをコピーすること。メジャーバージョンを合わせなければならない点に注意。

Goのソースコード

main.go
package main

import (
	"fmt"
	"runtime"
	"syscall/js"
)

func main() {
	// 標準出力は、デベロッパーツールのConsoleへ出力される
	fmt.Println("Hello, WebAssembly!!!")

	// 標準ライブラリsyscall/jsを使って、ブラウザ上のJavaScriptオブジェクトを操作する
	output := fmt.Sprintf("Runtime version=%s", runtime.Version())
	js.Global().Get("document").Call("getElementById", "go-info").Set("innerHTML", output)

	// main関数が終了すると、Instanceがなくなってしまう。
	// 以下はmain関数の終了を防止するためのブロック。
	blocker := make(chan struct{})
	<-blocker
}

JavaScript

index.html
<html>
<head>
	<meta charset="utf-8" />
	<script src="../wasm_exec.js"></script>
	<script>
		const go = new Go();
		WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
			go.run(result.instance);
		});
	</script>
</head>
<body>
	<h3>Go</h3>
	<div>実行環境の情報</div>
	<div id="go-info"></div>
</body>
</html>
.wasmファイルをビルドする方法
GOOS=js GOARCH=wasm go build -o main.wasm main.go
ウェブサーバーから配信しなければならないファイル群
cp index.html ./dist/
cp main.wasm ./dist/
cp `go env GOROOT`/misc/wasm/wasm_exec.js ./dist/
python3 -m http.server -d ./dist/

結果。

「Go言語のWebAssembly」=「syscall/jsを用いて、ブラウザ上のJavaScriptオブジェクトを操作する」

標準ライブラリsyscall/jsを用いることで、ブラウザ上のJavaScriptオブジェクトに対する操作(プロパティを読み込んだり、プロパティを追加したり、メソッドを呼び出したり)が可能となる。

ブラウザ上のJavaScriptオブジェクトが操作できるため、DOMの操作もできる。
したがって、SPAのウェブアプリケーションを作るために必要なことは大体できるようになる。

ちなみに、Goソースコード中におけるhttp.Clientを使ってHTTPを介したリソース取得処理は、ブラウザ上ではfetch関数を使ったリソース取得処理となる。参考

main関数は必須

必ずmain関数を介さなければならない。
main関数を介さずに、ある特定の関数だけをブラウザ上で実行する、みたいなことはできないらしい(この辺、RustやC++だとどうなんだろう?知ってる人いたら教えて下さい)。

所感

良い

Goが提供する標準ライブラリsyscall/jsがとてもシンプルなため、WebAssemblyを触ったことがない人でも、急こう配の学習曲線で習得が可能(表現でちょっと遊んでみた。習得しやすいってことですw)だと思う。

頑張ればGoだけでフロントエンドのSPAが書けると思う。
Goで書けることそれ自体が嬉しい。実際に、プロダクション環境で動くアプリケーションでそれをやるかどうかは別としてw。

気になる

標準ライブラリsyscall/jsは、This package is EXPERIMENTALらしいので、プロダクションコードをGoのWebAssemblyにするのは、リスクがあるかも。

どの部分をGoで書いて、どの部分をJavaScriptで書くか、の規律を明確にしないと、スパゲッティコード化しそう。標準ライブラリsyscall/jsは、必要最低限の機能しか提供していないが故に、自由度が非常に高い。各々が自由に複数人でGoのWebAssemblyなアプリケーションを書く場合、オレオレでも良いのでフレームワークっぽくしておく、あるいは、既存サードパーティ製のフレームワークっぽいものを利用する、などした方が良いかもしれない。

.wasmファイルのサイズが気になる。フロントエンドの場合、バイナリファイルのロード速度重要なのでね。ネット探せば情報あるのかな?

僕自身への宿題(結局よくわからなかった)

main関数が終了すると、Instanceがなくなってしまう。
以下はmain関数の終了を防止するためのブロック。

GoでWebAssemblyをどう書くか以前に、WebAssemblyについて理解を深めておいた方が良さそう。今回、index.htmlのscriptタグ中で行われていることについて理解するための時間が取れなかったしきれなかった。メカニズムを理解されたい方は、多分、こちらあたりを読み、かつ、wasm_exec.jsを読むと良いと思う。

参考

Discussion