カテゴリー: FrontEnd

Go WebAssemblyでPromiseを使って非同期化してみた

はじめに

先日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側です。

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側です。

<!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" >

先ほどgo側で定義したように、download関数の第一引数に画像のURL、第二引数にはコールバック用の関数を渡します。コールバック用の関数では、第一引数に画像データが渡ってくるので、これを受け取ってimgタグとして表示しています。

このように、goroutineを使うことでJavaScriptと非同期で連携することができます。JavaScript側からのインターフェイスとしては、通常の非同期処理と同じであることも分かりました。それでは、この非同期化処理をいよいよPromise化したいと思います。

Promise化する

先ほどのコードをPromise化するため、download関数を次のように変更します。

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に対応したコードに変更します。

<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" >

画像を一気にダウンロードして、全て完了したら「ダウンロード完了」と表示するようにしています。
download(url)が、Go側で生成されたPromiseを返すので、thenやawaitを使うことができます。

おまけ

ダウンロードしたデータはGoでは []byte型になりますが、これをそのままJavaScriptが側に渡すと InvalidValueエラーとなります。そこで、js.CopyBytesToJS()関数を使って、 []byteから Uint8Arrayに変換する必要がありました。
また、 js.CopyBytesToJS()の引数には、コピー先の Uint8Arrayを渡す必要があるため、あらかじめ空の Uint8Arrayを渡しています。
ちなみに、これとは逆の変換をするには、 js.CopyBytesToGo()を使い、JavaScriptの Uint8Array[]byteに変換することができます。

  // ダウンロードしたデータを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を使うユースケースがあまり思い浮かばないのですが、今後のアップデートでもっと用途が広がるかもしれません。今はまだ局所的な組み込みが現実的なのかなと思いました。

おすすめ書籍

カイザー

シェア
執筆者:
カイザー

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

2週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

4週間 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前