GoのWASMがライブラリではなくアプリケーションであること

2021/03/29
このエントリーをはてなブックマークに追加

はじめに

がんばって書いた書籍が低評価で少々しょんぼりしているあんどうです。まぁ、つい力が入りすぎて袋小路に思い切り突っ込んだ結果抜けられなくなることってあるよね。あるある。そんなわけで今日はできるだけ力を入れずテンション低めにサクッと行きます。

で、GoのWASM。大道の真ん中をまっすぐに歩まれているみなさんはWASMするときはRustかいっそC/C++をemscriptenでってことになると思いますが、私はしょせん路傍の石の下で低評価が目に入らないように丸まっているダンゴムシ。せっかくだからオレはこのGoでWASMを選ぶぜって感じなんですが、ぶっちゃけあれ、めんどくさいすよね。

あ、ちなみに今回の話は「このめんどくささをまるっと解決!」みたいな気持ちのいい話ではなくて、ただただ「めんどくさいよね」っていうだけの話です。あーめんどくさい。

Rustの場合

まず比較のためにRustの例をあげます。Rustで2数を足し算するWASM関数を定義するとこうなるみたいです。

#[no_mangle]
pub fn add(a: i32, b: i32) -> i32 {
  a + b
}

コンパイル結果がadd.wasmだとすると、JavaScriptから呼び出すときはたぶんこんな感じ。

WebAssembly.instantiateStreaming(fetch("add.wasm")).then(result => {
  const add = result.instance.exports.add
  const val = add(1, 2)
  console.log(val)
})

顧客が本当に欲しかったもの。めんどくさくない。

Goの場合

めんどくさ1

Goだとこうなります。

func add(this js.Value, args []js.Value) interface{} {
  return js.ValueOf(args[0].Int()+args[1].Int())
}

引数や返り値を特殊なオブジェクトでラップしてやる必要があります。ちょっとめんどくさい。とはいえ、まぁ異なる言語間で情報をやり取りするんだから仕方ないとしましょう。

ではこれを呼び出します・・・と言いたいのですが、なんとGoのWASMでは独自に定義した関数を直接呼び出すことはできません。Rustだとexportsプロパティが使えたのに・・・。

めんどくさ2

どうするかというとmain()関数内でなんとかします。こんな感じ。

import "syscall/js"
func main() {
  js.Global().Set("add", js.FuncOf(add))
}

main()関数はシグネチャが決まっているので自由に引数や返り値を設定できません。仕方がないのでJavaScriptのグローバルオブジェクト(globalThis)にプロパティを生やしてそこに関数を設定します。めんどくさくなってきました。

さっそく呼び出してみましょう。コンパイル結果はmain.wasmとします。

const go = new Go()
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then(result => {
  go.run(result.instance)
  console.log(add(1, 2))
}

new Go()とかgo.importObjectとかgo.runとかいろいろとありますが説明しません。雰囲気でご理解ください。説明しすぎても読みにくいだけですからね。これはあくまで一般論で、特に自身の経験に根ざしたとかそういう話ではありませんが。

結果どうなるかと言うと

こうなりました。「Goプログラムはすでに終了しています」だそうです。は? いや、まぁ、main()関数の実行が終わったらアプリケーションが終わるのはわかりますけども。

めんどく3

しょうがないのでみんな大好きなチャンネルを使ってmain()関数を終わらなくします。

func main() {
  ch := make(chan struct{})
  js.Global().Set("add", js.FuncOf(add))
  <-ch
}

これでmain()関数は終了しません。実行してみましょう。

計算できました。勝ったッ!第3部完!

めんどくさ4

・・・と思いましたか?残念。まだです。呼び出し側のコードを次のように修正して実行してみます。

const go = new Go()
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then(result => {
  go.run(result.instance)
  console.log(add(1))
}

add関数を呼び出そうとしていますが、引数が足りていません。どうなるでしょう。

panicします。とはいえこれは別にいいのです。さらにこのままコンソールでadd(1, 2)を実行してください。

なんと次の呼び出しも「Goプログラムはすでに終了しています」と言われて失敗します。これは先ほどのpanicでmain()関数が終了してしまったことが原因です。

これをどうするかですが、プログラムのpanicを完全に防止することは人類には不可能です。難しく考えるよりも、panicで終了した場合はアプリケーションを再度立ち上げましょう。難しく考えすぎても読みにくいだけですから。あくまで一般論として。

めんど草MAX

ということでこれが今回の最終形です。

let mod, inst
const go = new Go()
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then(async (result) => {
  mod = result.module
  inst = result.instance
  go.run(inst)
  console.log(await add2(1))
})

async function add2(i, j) {
  try {
    return add(i, j)
  } catch(e) {
    inst = await WebAssembly.instantiate(mod, go.importObject)
    go.run(inst)
    return add2(i, j)
  }
}
  • panicしたときにWebAssembly.instantiate()で復帰できるように、最初のWebAssembly.instantiateStreaming()実行時にmodinstという変数に情報を保存します
  • 復帰の際に使用するWebAssembly.instantiate()は非同期なので、add()関数を直接使用するのではなく、ラップした非同期のadd2()関数を使用しました
  • add2()では例外発生時に、modを使用してWebAssembly.instantiate()を呼び出し、再度instを設定、go.run(inst)でアプリケーションを再起動します
  • JSの例外はGo側でpanicしたときに吐かれるのではなく、そのときはログを吐いて通常終了し、次回Go側のコードが呼び出されるときに件の「Goプログラムはすでに終了しています」が出ます。そのため、add2()関数の例外処理でGoアプリケーションを再起動したあとで関数を再実行しました

最初のRust版と比較するともうめんど草MAX。

ついでにGo側の全体像も載せておきます。

package main

import "syscall/js"

func main() {
  ch := make(chan struct{})
  js.Global().Set("add", js.FuncOf(add))
  <-ch
}

func add(this js.Value, args []js.Value) interface{} {
  return js.ValueOf(args[0].Int()+args[1].Int())
}

一応、実行結果も確認しておきましょう。

panic後に関数を呼び出しても無事に動作しました。

まとめ

最後にGoのWASMのめんどくさポイントをまとめます。

  • Go言語側でいちいちValueOf()FuncOf()でラップする必要がある
  • main()関数しか呼び出せない
  • チャンネルを使用してmain()関数を終了させなくする必要がある
  • 返り値などにグローバルオブジェクトを使用する必要がある
  • Go側で例外が発生した場合はmain()関数を再起動する必要がある

結局、最初の一つを除きGo WASMのツラミは「ライブラリではなくアプリケーションしか作成できないこと」に尽きます。なんでこんな事になってるんですかね・・・。

ただまぁ、一つ言えるのはGo WASMの作者陣もめんどくさくしようと思ってこうなってるんではなくて、GoでWASMを吐けるようにしよう、みんなに喜んでもらおう、と思ってこうなってるはずですよね。そういう作者の気持ちを慮ってあげることも大切なのではないか。創作物に対するそういう気持ちこそが次はもっと良いものを作ろうという原動力になるのではないか。そしてそれこそが巡り巡ってすべての人が幸せになれる道なのではないか。あくまで一般論としてそんな事を考えました。

ということで、私はこれからも頑張ってGoでWASMを使い続けようと思います。

 


 

などと書いていたらレビューで大橋パイセンに「TinyGoのWASMビルドならmain以外の関数を公開できる」と教えてもらいました。

https://github.com/tinygo-org/tinygo/blob/master/src/examples/wasm/export/wasm.go

package main

func main() {
}

//export add
func add(a, b int) int {
    return a + b
}

関数定義の前に//export 関数名というコメントをつけるとJavaScript側からinst.exports.add()みたいにして呼び出せるみたいです。ValueOf()でラップする必要もチャンネル使ってmain()で待ち受ける必要もなし。これ!これが欲しかった。

TinyGoでWASMビルドするとファイルサイズがだいぶ小さくなるのはどこかで見て知ってましたが、exportsできるのは知りませんでした。次回はその辺を触ってみたいと思います。

私はこれからもGoでWASMを使い続けるつもりでしたがTinyGoに乗り換えるかも知れません。

その他の記事

Other Articles

2021/09/16
[ECMAScript] Pipe operator 論争まとめ – F# か Hack か両方か

2021/07/05
TypeScript v4.3 の機能を使って immutable ライブラリの型付けを頑張る

2021/06/25
Denoでwasmを動かすだけの話

2021/05/18
DOMMatrix: 2D / 3D 変形(アフィン変換)の行列を扱う DOM API

2021/03/26
Pythonプロジェクトの共通のひな形を作る

2021/03/25
インラインスタイルと Tailwind CSS と Tailwind CSS 入力補助ライブラリと Tailwind CSS in JS

2021/03/23
Serverless NEGを使ってApp Engineにカスタムドメインをワイルドカードマッピング

2021/01/07
esbuild の機能が足りないならプラグインを自作すればいいじゃない

2020/08/26
TypeScriptで関数の部分型を理解しよう

2020/06/16
[Web フロントエンド] esbuild が爆速すぎて webpack / Rollup にはもう戻れない

2020/03/19
[Web フロントエンド] Elm に心折れ Mint に癒しを求める

2020/02/28
さようなら、TypeScript enum

2020/02/14
受付のLooking Glassに加えたひと工夫

2020/01/28
カブクエンジニア開発合宿に行ってきました 2020冬

2020/01/30
Renovateで依存ライブラリをリノベーションしよう 〜 Bitbucket編 〜

2019/12/27
Cloud Tasks でも deferred ライブラリが使いたい

2019/12/25
*, ::before, ::after { flex: none; }

2019/12/21
Top-level awaitとDual Package Hazard

2019/12/20
Three.jsからWebGLまで行きて帰りし物語

2019/12/18
Three.jsに入門+手を検出してAR.jsと組み合わせてみた

2019/12/04
WebXR AR Paint その2

2019/11/06
GraphQLの入門書を翻訳しました

2019/09/20
Kabuku Connect 即時見積機能のバックエンド開発

2019/08/14
Maker Faire Tokyo 2019でARゲームを出展しました

2019/07/25
夏休みだョ!WebAssembly Proposal全員集合!!

2019/07/08
鵜呑みにしないで! —— 書籍『クリーンアーキテクチャ』所感 ≪null 篇≫

2019/07/03
W3C Workshop on Web Games参加レポート

2019/06/28
TypeScriptでObject.assign()に正しい型をつける

2019/06/25
カブクエンジニア開発合宿に行ってきました 2019夏

2019/06/21
Hola! KubeCon Europe 2019の参加レポート

2019/06/19
Clean Resume きれいな環境できれいな履歴書を作成する

2019/05/20
[Web フロントエンド] 状態更新ロジックをフレームワークから独立させる

2019/04/16
C++のenable_shared_from_thisを使う

2019/04/12
OpenAPI 3 ファーストな Web アプリケーション開発(Python で API 編)

2019/04/08
WebGLでレイマーチングを使ったCSGを実現する

2019/04/02
『エンジニア採用最前線』に感化されて2週間でエンジニア主導の求人票更新フローを構築した話

2019/03/29
その1 Jetson TX2でk3s(枯山水)を動かしてみた

2019/03/27
任意のブラウザ上でJestで書いたテストを実行する

2019/02/08
TypeScript で “radian” と “degree” を間違えないようにする

2019/02/05
Python3でGoogle Cloud ML Engineをローカルで動作する方法

2019/01/18
SIGGRAPH Asia 2018 参加レポート

2019/01/08
お正月だョ!ECMAScript Proposal全員集合!!

2019/01/08
カブクエンジニア開発合宿に行ってきました 2018秋

2018/12/25
OpenAPI 3 ファーストな Web アプリケーション開発(環境編)

2018/12/23
いまMLKitカスタムモデル(TF Lite)は使えるのか

2018/12/21
[IoT] Docker on JetsonでMQTTを使ってCloud IoT Coreと通信する

2018/12/11
TypeScriptで実現する型安全な多言語対応(Angularを例に)

2018/12/05
GASでCompute Engineの時間に応じた自動停止/起動ツールを作成する 〜GASで簡単に好きなGoogle APIを叩く方法〜

2018/12/02
single quotes な Black を vendoring して packaging

2018/11/14
3次元データに2次元データの深層学習の技術(Inception V3, ResNet)を適用

2018/11/04
Node Knockout 2018 に参戦しました

2018/10/24
SIGGRAPH 2018参加レポート-後編(VR/AR)

2018/10/11
Angular 4アプリケーションをAngular 6に移行する

2018/10/05
SIGGRAPH 2018参加レポート-特別編(VR@50)

2018/10/03
Three.jsでVRしたい

2018/10/02
SIGGRAPH 2018参加レポート-前編

2018/09/27
ズーム可能なSVGを実装する方法の解説

2018/09/25
Kerasを用いた複数入力モデル精度向上のためのTips

2018/09/21
競技プログラミングの勉強会を開催している話

2018/09/19
Ladder Netwoksによる半教師あり学習

2018/08/10
「Maker Faire Tokyo 2018」に出展しました

2018/08/02
Kerasを用いた複数時系列データを1つの深層学習モデルで学習させる方法

2018/07/26
Apollo GraphQLでWebサービスを開発してわかったこと

2018/07/19
【深層学習】時系列データに対する1次元畳み込み層の出力を可視化

2018/07/11
きたない requirements.txt から Pipenv への移行

2018/06/26
CSS Houdiniを味見する

2018/06/25
不確実性を考慮した時系列データ予測

2018/06/20
Google Colaboratory を自分のマシンで走らせる

2018/06/18
Go言語でWebAssembly

2018/06/15
カブクエンジニア開発合宿に行ってきました 2018春

2018/06/08
2018 年の tree shaking

2018/06/07
隠れマルコフモデル 入門

2018/05/30
DASKによる探索的データ分析(EDA)

2018/05/10
TensorFlowをソースからビルドする方法とその効果

2018/04/23
EGLとOpenGLを使用するコードのビルド方法〜libGLからlibOpenGLへ

2018/04/23
技術書典4にサークル参加してきました

2018/04/13
Python で Cura をバッチ実行するためには

2018/04/04
ARCoreで3Dプリント風エフェクトを実現する〜呪文による積層造形映像制作の舞台裏〜

2018/04/02
深層学習を用いた時系列データにおける異常検知

2018/04/01
音声ユーザーインターフェースを用いた新方式積層造形装置の提案

2018/03/31
Container builderでコンテナイメージをBuildしてSlackで結果を受け取る開発スタイルが捗る

2018/03/23
ngUpgrade を使って AngularJS から Angular に移行

2018/03/14
Three.jsのパフォーマンスTips

2018/02/14
C++17の新機能を試す〜その1「3次元版hypot」

2018/01/17
時系列データにおける異常検知

2018/01/11
異常検知の基礎

2018/01/09
three.ar.jsを使ったスマホAR入門

2017/12/17
Python OpenAPIライブラリ bravado-core の発展的な使い方

2017/12/15
WebAssembly(wat)を手書きする

2017/12/14
AngularJS を Angular に移行: ng-annotate 相当の機能を TypeScrpt ファイルに適用

2017/12/08
Android Thingsで4足ロボットを作る ~ Android ThingsとPCA9685でサーボ制御)

2017/12/06
Raspberry PIとDialogflow & Google Cloud Platformを利用した、3Dプリンターボット(仮)の開発 (概要編)

2017/11/20
カブクエンジニア開発合宿に行ってきました 2017秋

2017/10/19
Android Thingsを使って3Dプリント戦車を作ろう ① ハードウェア準備編

2017/10/13
第2回 魁!! GPUクラスタ on GKE ~PodからGPUを使う編~

2017/10/05
第1回 魁!! GPUクラスタ on GKE ~GPUクラスタ構築編~

2017/09/13
「Maker Faire Tokyo 2017」に出展しました。

2017/09/11
PyConJP2017に参加しました

2017/09/08
bravado-coreによるOpenAPIを利用したPythonアプリケーション開発

2017/08/23
OpenAPIのご紹介

2017/08/18
EuroPython2017で2名登壇しました。

2017/07/26
3DプリンターでLチカ

2017/07/03
Three.js r86で何が変わったのか

2017/06/21
3次元データへの深層学習の適用

2017/06/01
カブクエンジニア開発合宿に行ってきました 2017春

2017/05/08
Three.js r85で何が変わったのか

2017/04/10
GCPのGPUインスタンスでレンダリングを高速化

2017/02/07
Three.js r84で何が変わったのか

2017/01/27
Google App EngineのFlexible EnvironmentにTmpfsを導入する

2016/12/21
Three.js r83で何が変わったのか

2016/12/02
Three.jsでのクリッピング平面の利用

2016/11/08
Three.js r82で何が変わったのか

2016/12/17
SIGGRAPH 2016 レポート

2016/11/02
カブクエンジニア開発合宿に行ってきました 2016秋

2016/10/28
PyConJP2016 行きました

2016/10/17
EuroPython2016で登壇しました

2016/10/13
Angular 2.0.0ファイナルへのアップグレード

2016/10/04
Three.js r81で何が変わったのか

2016/09/14
カブクのエンジニアインターンシッププログラムについての詩

2016/09/05
カブクのエンジニアインターンとして3ヶ月でやった事 〜高橋知成の場合〜

2016/08/30
Three.js r80で何が変わったのか

2016/07/15
Three.js r79で何が変わったのか

2016/06/02
Vulkanを試してみた

2016/05/20
MakerGoの作り方

2016/05/08
TensorFlow on DockerでGPUを使えるようにする方法

2016/04/27
Blenderの3DデータをMinecraftに送りこむ

2016/04/20
Tensorflowを使ったDeep LearningにおけるGPU性能調査

→
←

関連職種

Recruit

フロントエンドエンジニア(TypeScript)

業務内容

業務内容 当ポジションは弊社Webサービスのフロントエンド機能設計及び実装を担当します。 サービス毎の開発チー […]

→
←

お客様のご要望に「Kabuku」はお応えいたします。
ぜひお気軽にご相談ください。

お電話でも受け付けております
03-6380-2750
営業時間:09:30~18:00
※土日祝は除く