はじめに
先日WebAssemblyに入門して、実際に以下のチュートリアルを進めながら、WebAssemblyを使用してブラウザ上でGoを動かすことができました。
GoでWebAssemblyに触れよう
https://golangtokyo.github.io/codelab/go-webassembly/
ただ、このチュートリアルをベースにGoの処理を書いていくと気が付くのですが、Goの処理が完了するまでJavaScriptの処理が止まってしまいます。そこで、Goを非同期化させたいと思います。
もし、初めてGoでのWebassemblyに触れる方は、初めに上記のチュートリアルに触れておくことをおすすめします。
Go WebAssemblyで非同期化のコード
js.FuncOf()
で定義した関数が、JS側から呼び出されると、Go側での処理が完了するまで、JSのイベントループはブロックされます。これを回避するためには、Go側の関数で新たなgoroutineを開始する必要があります。今回は画像をWeb上からダウンロードしてブラウザ上に表示するというサンプルコードで説明します。
まずはGo側です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | package main import ( "bytes" _ "image/jpeg" _ "image/png" "net/http" "syscall/js" ) func main() { c := make(chan bool) println("Hello, WebAssembly!") js.Global().Set("download", js.FuncOf(download)) // JS Functionを追加 <-c // WebAssemblyとして読み込まれた後はgoを起動したままにする } func download(this js.Value, inputs []js.Value) interface{} { println("called") // 第一引数にダウンロードする画像のURLを渡してもらう imageUrl := inputs[0].String() // 第二引数にコールバック用の関数を渡してもらう callback := inputs[1] // ダウンロード処理をgoroutineで実行 go func() { resp, err := http.Get(imageUrl) if err != nil { // JSで reject() を呼び出す return } buf := new(bytes.Buffer) buf.ReadFrom(resp.Body) // ダウンロードしたデータをJSの型(Uint8Array)に変換する // JSで new Uint8Array() を呼び出す var outputBytes = js.Global().Get("Uint8Array").New(buf.Len()) // []byte -> Uint8Arrayに変換する js.CopyBytesToJS(outputBytes, buf.Bytes()) // JSで resolve()を呼び出す callback.Invoke(outputBytes) }() return nil } |
このように、goroutineで処理することによって、JavaScriptのイベントループを止めることなく、非同期処理を行うことができます。
ダウンロードした画像をJavaScript側に渡すため、JavaScript側から第二引数にコールバック用の関数を渡してもらうようにしています。この関数を実行するには Invoke()関数を使用します。
Invoke()関数に引数を渡すと、そのままJavaScriptに渡すことができるので、サンプルでは
callback.Invoke(outputBytes)
という形でダウンロードしたデータを渡しています。
次に、HTML側です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | <!doctype html> <html> <head> <meta charset="utf-8"> <title>Hello, WebAssembly</title> </head> <body> <!-- wasm_exec.jsを読み込む --> <script src="wasm_exec.js"></script> <script> // WebAssembly.instantiateStreamingがない場合のポリフィル if (!WebAssembly.instantiateStreaming) { WebAssembly.instantiateStreaming = async (resp, importObject) => { const source = await (await resp).arrayBuffer(); return await WebAssembly.instantiate(source, importObject); }; } // main.wasmにビルドされたGoのプログラムを読み込む const go = new Go(); let mod, inst; WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => { mod = result.module; inst = result.instance; // 実行 go.run(inst); }); function run(inputElement) { console.clear(); const imageUrls = [ "http://localhost:8080/HNCK2227.jpg", "http://localhost:8080/HNCK2634.jpg", "http://localhost:8080/HNCK2737.jpg", "http://localhost:8080/HNCK2763.jpg", "http://localhost:8080/HNCK3281.jpg", "http://localhost:8080/HNCK3303.jpg", "http://localhost:8080/HNCK3323.jpg", ]; const downloadTasks = imageUrls.forEach(url => { // goで定義したdownload関数を呼び出す。 download(url, (result) => { var element = document.createElement('img'); element.src = URL.createObjectURL(new Blob([result.buffer])); element.width = "300"; document.getElementById('output').appendChild(element); }); }) } </script> <button type="button" onclick="run()">画像ダウンロード</button> <span id="status"></span> <div id="output"></div> </body> </html> |
先ほどgo側で定義したように、download関数の第一引数に画像のURL、第二引数にはコールバック用の関数を渡します。コールバック用の関数では、第一引数に画像データが渡ってくるので、これを受け取ってimgタグとして表示しています。
このように、goroutineを使うことでJavaScriptと非同期で連携することができます。JavaScript側からのインターフェイスとしては、通常の非同期処理と同じであることも分かりました。それでは、この非同期化処理をいよいよPromise化したいと思います。
Promise化する
先ほどのコードをPromise化するため、download関数を次のように変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | func download(this js.Value, inputs []js.Value) interface{} { println("called") imageUrl := inputs[0].String() // Promise化するJS関数を生成 handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} { resolve := args[0] reject := args[1] // ダウンロード処理をgoroutineで実行 go func() { resp, err := http.Get(imageUrl) if err != nil { // JSで reject() を呼び出す reject.Invoke() return } // resp.Body buf := new(bytes.Buffer) buf.ReadFrom(resp.Body) // ダウンロードしたデータをJSの型(Uint8Array)に変換する // JSで new Uint8Array() を呼び出す var outputBytes = js.Global().Get("Uint8Array").New(buf.Len()) // []byte -> Uint8Arrayに変換する js.CopyBytesToJS(outputBytes, buf.Bytes()) // JSで resolve()を呼び出す resolve.Invoke(outputBytes) }() return nil }) // JSで new Promise(handler) するのと同じです。 return js.Global().Get("Promise").New(handler) } |
ポイントを紹介します。
-
js.Global().Get("Promise").New(handler)
でPromiseを生成することができます。これは、JavaScriptでnew Promise(handler)
を実行しているのと全く同じです。 - handlerは
jsFuncOf()
で生成したJavaScriptの関数を渡します。 - handlerが呼び出されると、通常のPromiseと同じように、第一引数にresolve、第二引数にrejectが渡されるので、非同期処理後にコールバックすることができます。
最後に、JavaScript側をPromiseに対応したコードに変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <body> // ・・・中略・・・ async function run(inputElement) { console.clear(); const imageUrls = [ "http://localhost:8080/HNCK2227.jpg", "http://localhost:8080/HNCK2634.jpg", "http://localhost:8080/HNCK2737.jpg", "http://localhost:8080/HNCK2763.jpg", "http://localhost:8080/HNCK3281.jpg", "http://localhost:8080/HNCK3303.jpg", "http://localhost:8080/HNCK3323.jpg", ]; const downloadTasks = imageUrls.map(url => { // download(url)はPromiseを返すので、thenで処理をチェインすることができる。 return download(url).then(result => { var element = document.createElement('img'); element.src = URL.createObjectURL(new Blob([result.buffer])); element.width = "300"; document.getElementById('output').appendChild(element); }) }) await Promise.all(downloadTasks); // 全てのダウンロード処理を待機 document.getElementById('status').innerText = "ダウンロード完了"; } </script> <button type="button" onclick="run()">画像ダウンロード</button> <span id="status"></span> <div id="output"></div> </body> |
画像を一気にダウンロードして、全て完了したら「ダウンロード完了」と表示するようにしています。
download(url)
が、Go側で生成されたPromiseを返すので、thenやawaitを使うことができます。
おまけ
ダウンロードしたデータはGoでは
[]byte
型になりますが、これをそのままJavaScriptが側に渡すと InvalidValueエラーとなります。そこで、
js.CopyBytesToJS()
関数を使って、
[]byte
から
Uint8Array
に変換する必要がありました。
また、
js.CopyBytesToJS()
の引数には、コピー先の
Uint8Array
を渡す必要があるため、あらかじめ空の
Uint8Array
を渡しています。
ちなみに、これとは逆の変換をするには、
js.CopyBytesToGo()
を使い、JavaScriptの
Uint8Array
を
[]byte
に変換することができます。
1 2 3 4 5 6 7 8 9 | // ダウンロードしたデータをJSの型(Uint8Array)に変換する // JSで new Uint8Array() を呼び出す var outputBytes = js.Global().Get("Uint8Array").New(buf.Len()) // []byte -> Uint8Arrayに変換する js.CopyBytesToJS(outputBytes, buf.Bytes()) // JSで resolve()を呼び出す callback.Invoke(outputBytes) |
さいごに
GoでWebassemblyを使うユースケースがあまり思い浮かばないのですが、今後のアップデートでもっと用途が広がるかもしれません。今はまだ局所的な組み込みが現実的なのかなと思いました。