Getting Started Golang/WebAssembly


4 min read
Getting Started Golang/WebAssembly

WebAssembly(wasm)はブラウザ上で動作するアセンブリ「っぽい」言語で、C/C++やRust、Kotlin/Native、そして我らがGo言語などがwasm形式へのコンパイルをサポートするべく開発が進められています。昨年の12月にW3C勧告となりました。

Go言語では1.11でwasmのサポートが実験的に入り、syscall/jsパッケージをインポートすることで使用することができます。1.14NaClサポートも終了しましたし、少しでも触っておいた方が良いかな、と感じたので、軽く動かしてみます。

まずは下準備として、適当に作ったプロジェクトディレクトリにwasm_exec.jsをコピーしてきます。wasm_exec.js$GOROOT/misc/wasm以下にあるので、その辺を探しに行きます。macOSを使っていて、HomebrewでGoをインストールした場合は/usr/local/Cellar/go/${GO_VERSION}/libexec/misc/wasm/wasm_exec.jsに置いてありました。特に深く考えずに、$(go env GOROOT)を使いましょう。

$ cp $(go env GOROOT)/misc/wasm/wasm_exec.js .

下準備はこれだけで、後はアプリケーションコードを書いていきます。まずはGo側。Hello Worldをやってみましょう。go mod initした後、適当なエディタ(もちろんemacsが良いと思います)でmain_js.goを開き、次の様に書きます。main.goじゃなくてmain_js.goなのは、後でもう一個main.goを作るからです。

package main

import "fmt"

func main() {
    fmt.Println("Hello, WebAssembly!")
}

はい、簡単ですね。特に説明はいらないと思います。これをwasm用の形式にビルドしていきます。ビルドも特に特殊なツールを使うということもなく、OSをjs、アーキテクチャをwasmとしてgo buildすればOKです。ここでは、main.wasmというファイルに書き出します。

$ GOOS=js GOARCH=wasm go build -o main.wasm main_js.go

次にHTML側を書いていきます。現時点ではmain関数で文字列を出力しているだけですから、読み込みだけを行っていきます。wasm_exec.jsを読み込むのと、main.wasmを読み込んで実行する、という感じでしょうか。この辺は(というかこれまでの部分もそうなんですが)Go wikiからのコピペです。

<!doctype 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>
  </body>
</html>

Go wikiではgoexecをつかってHTTPサーバーを立ててブラウザから表示しましょう、みたいな感じで書いてあるんですが、私はファイルをウォッチして再ビルド、とかもやらせたかったので、次の様に適当にmain.goを作成しました。

//+build !js

package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/exec"
	"strings"
	"time"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	watchDir(ctx, ".")

	http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))
}

func watchDir(ctx context.Context, dir string) {
	fi, err := os.Stat(dir)
	if err != nil {
		log.Print(err)
		return
	}

	if !fi.IsDir() {
		go watchFile(ctx, dir)
		return
	}

	f, err := os.Open(dir)
	if err != nil {
		log.Print(err)
		return
	}
	defer f.Close()

	filenames, err := f.Readdirnames(-1)
	if err != nil {
		log.Print(err)
		return
	}

	for _, filename := range filenames {
		if strings.HasSuffix(filename, ".wasm") {
			continue
		}
		log.Printf("watch %s", filename)
		go watchFile(ctx, filename)
	}
}

func watchFile(ctx context.Context, file string) {
	lastStat := time.Now()
	for {
		select {
		case <-ctx.Done():
			return
		default:
		}

		stat, err := os.Stat(file)
		if err != nil {
			panic(err)
		}

		if stat.ModTime().After(lastStat) {
			log.Printf("file change detected: run go build")

			ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
			cmd := exec.CommandContext(ctx, "go", "build", "-o", "main.wasm", ".")
			cmd.Env = append(cmd.Env, "GOOS=js", "GOARCH=wasm")
			cmd.Env = append(cmd.Env, os.Environ()...)
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr

			if err := cmd.Run(); err != nil {
				log.Printf("error on go build: %v", err)
			}

			cancel()
			log.Printf("go build successfully done")

		}
		lastStat = stat.ModTime()
		time.Sleep(1 * time.Second)

	}
}

書き殴りなのでちょっと恥ずかしいですが、これで準備ができました。後々遊んでいく上でファイルが増える可能性も考慮し、ディレクトリ内のファイルを(.wasm拡張子を持ったファイル以外)ウォッチして変更があったらgo buildをする、という感じです。main.go自体はwasmのビルドの時は無視してほしいので//+build !jsと書いておきました。いよいよ表示してみましょう。現時点でディレクトリ内はこのようになっています:

$ ls
go.mod  index.html  main.go  main.wasm*  main_js.go  wasm_exec.js

おもむろにgo run .として、ブラウザでhttp://localhost:8080を開き、ウェブコンソールを立ち上げ、ページを更新します。

きちんと表示されました。ちょっと感動です。「おぉ、動いた!」という気持ちになったHello worldはいつぶりのことか・・・

これだけでも楽しいのですが、せっかくなのでもう少し進んでみます。みんなボタンを押したら文字列を表示したいと思うと思いますから、そんな感じでやってみましょう。

先ほどのmain_js.goを少々改変して、次の様にします。

package main

import (
	"fmt"
	"syscall/js"
)

func Print(this js.Value, args []js.Value) interface{} {
	fmt.Println("Button is pushed")
	return nil
}

func main() {
	js.Global().Set("print", js.FuncOf(Print))
	select {}
}

いよいよsyscall/jsパッケージが登場しました。適当にGoogleで検索をしたときに上の方に出てきたページは少々情報が古く、js.NewCallbackで、func([]js.Value)として定義したGoの関数をJavaScript側で使用するコールバック関数にして、それをjs.Global().Setで登録する、みたいな感じで書いてあったんですが、Go1.14ではすでに仕様が変わっており、func(js.Value, []js.Value) interface{})として定義したGoの関数をjs.FuncOfで変換してそれを登録します。

今回はボタンが押されたら文字列を表示する、というだけなので、返値はnilです。printとして登録することにしました。

下から2行目にあるselect {}main関数をブロックするためのもので、chanとかでブロックしても良いようです。無駄ですけど、多分for {}とかしても動きはするんじゃないでしょうか。

HTML側は次の一行を<body>の中に入れます。

<button onClick="print()">hello</button>

ブラウザに戻り、ページを更新し、表示されたボタンを押します。

これは結構楽しいですね。wasm完全に理解しました。

以上です。


リプトンのコールドブリューシリーズが便利で美味しい件
Previous article

リプトンのコールドブリューシリーズが便利で美味しい件

最近、近所の西友で「水出し3分」と書かれたリプトンのティーバッグを発見したので、ちょっとお高い・・・と思いつつも購入したところ、見事にハマってしまいました。 近所の西友で二種類、ウェルシアでさらに一種類を発見元々水よりはお茶が好きで、たまに水出しのルイボスティーや緑茶、ジャスミン茶、あるいはお湯で出した後冷やした紅茶なんかを冷蔵庫において飲んではいたのですが、どうにも大きいピッチャーというのは(まぁ我が家で使っているのは1.2L程度の物なので、そこまで大きいという訳でもないのですが・・・)どうにも洗うのが面倒で、度々放置してしまっていました。 一方ティーバッグで一杯ずつ入れるのは、洗い物という観点から言うとさほど面倒ではないのですが、(夏と言うこともあり)冷たい物を飲みたいという気持ち強かったのです。お湯で出した後氷で冷やすと薄くなってしまったりもしますし、できれば水出しでいれたい、と感じていました。

dbrandのCamera Skin (Pixel4用)を購入した
Next article

dbrandのCamera Skin (Pixel4用)を購入した

dbrandはスマホやPC、タブレットといったデバイス用に「スキンシール」と呼ばれるアイテムを製造・販売しているカナダの会社です。スキンシールはデバイスを傷や汚れから守る薄いステッカーのような物です。 特にスマホにケースをつけずに使いたい、所謂「裸族」に人気の商品のようなのですが、私は普段Pixel4にケースを付けて使っているので、カメラ部分用のSkinを購入してみました。 今回購入したCamera Skin (Pixel4用)はお値段$1.95、日本への送料が$5.00で、合計$6.95でした。Camera


GO TOP

🎉 You've successfully subscribed to something tech.!
OK