第6回は、Go言語における開発事例を紹介します。GoからのJavaScript関数呼び出し、JavaScriptからのGo関数の呼び出しについても紹介します。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
今回は、プログラミング言語GoによるWebAssemblyプログラムの開発を紹介します。Goは、Googleの開発したプログラミング言語であり、WebAssemblyへの対応も積極的に行われています。C/C++やRustでは、EmscriptenやRust and WebAssemblyのような外部プロジェクトとの連携でWebAssembly対応を果たしていました。Goでは、直接WebAssemblyバイナリをターゲットにできるなど、言語レベルでWebAssemblyに対応しているのが特徴です。そのため、Goの言語環境を整える以外の特別な環境整備が不要で、すぐに標準ライブラリを含めたGoの言語仕様を十分に生かしたWebAssemblyプログラムを作成することができます。
Goは、C/C++やRustと同様にネイティブコードをターゲットにしたプログラミング言語であるので、その高い効率と高速性を生かした使い方が、同様に真価を発揮しやすいといえます。連載第1回でも紹介したように、まずは速度が重視される部分をGoが受け持つというのが自然な使われ方でしょう。
本稿では、それを前提にJavaScriptコードとの連携まで含めた、GoによるWebAssemblyプログラムの開発を紹介していきます。
Goによる開発のために、以下に挙げるソフトウェアを準備しておきます。コードエディタの他にはGoの言語環境を用意するだけなので、非常にシンプルです。以降、操作例はmacOSによるものを示しますが、適宜Windows環境についても補足します。
Goがインストールされていない場合、macOS環境、Windows環境ともに以下からインストーラをダウンロードしてインストールすることができます。macOSではHomebrewも利用できます。
All releases - The Go Programming Language
WebAssemblyをターゲットにする場合、Goのバージョンは1.11以降である必要があります。バージョンは、以下のように確認できます(この場合はGo 1.20.2)。
% go version go version go1.20.2 darwin/amd64
VSCodeでGoをプログラミングするなら、その名の通りのGo拡張機能をインストールしておきましょう。この拡張機能では、IntelliSenseやデバッグ機能が追加されます。拡張機能をインストールしてGoのソースコードを開くと、goplsやgo-outlineなどのGoのツールのインストールを促すポップアップが表示されますので、[Install]をクリックしてインストールしましょう。
goplsは「Go please」の略で、Goの公式Language Serverです。Language Serverとは、例えばVSCodeの場合は言語ごとの補完機能やフォーマッタ、定義参照などの機能を提供するための仕組みです。go-outlineは文字通り、Goのソースファイルから定義などをJSON形式で抽出するのためのツールです。
既述の通り、GoではWebAssemblyへのコンパイルが標準でサポートされます。簡単なサンプルを通じて、GoによるWebAssemblyプログラムの開発を概観しましょう。
まずは、プロジェクトを作成します。本連載のためのatmarkit_wasmフォルダの中に、go_wasmフォルダを作成し、ここをプロジェクトのフォルダとします。以降、このフォルダを基点に作業します。カレントフォルダを移動したら、go mod initコマンドでモジュールとして初期化します。パラメーターには、モジュールが公開される場合のインポートパスを指定します。今回はモジュールとして公開するものではないので、単にsampleとしていますが、本来はGitHubのリポジトリURLなど一意となる名前を指定します。
% mkdir go_wasm % cd go_wasm % go mod init sample go: creating new go.mod: module sample
これで、go_wasmフォルダがGoプロジェクトのために初期化されます。具体的には、go.modファイルが作成されます。go.modはモジュール管理のためのファイルで、既定では以下のような内容になります。
module sample (1) go 1.20 (2)
(1)は、モジュール作成時に指定したモジュールのパス、そして(2)はターゲットのGoのバージョンです。go getコマンドなどでモジュールへの参照を追加していくと、以下のようにrequireブロックが追加されます。さらにgo.sumファイルが作成され、そこに参照モジュールのバージョンとハッシュが登録されていきます。
require ( github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 // indirect …略… )
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 h1:aSISe… github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrr… …略…
プロジェクトを作成したら、サーバの起動などに必要なツールをインストールしておきます。コマンドは、go.modファイルのあるフォルダで実行する必要があります。
% go get github.com/shurcooL/goexec go: downloading github.com/shurcooL/goexec v0.0.0-20200425235707-36ff6d2d1adc …略… go: added golang.org/x/tools v0.7.0 % go install github.com/shurcooL/goexec@latest go: finding module for package golang.org/x/tools/imports …略…
Goをインストールすると、ユーザーのホームフォルダにgoフォルダが作成されます。ツールは、ここのbinフォルダすなわち~/go/binフォルダ(Windowsの場合は%HOMEPATH%\go\binフォルダ)にインストールされます。ツールの実行時は、フォルダパスを明示するか、あるいは環境変数PATHにフォルダを追加しておくと良いでしょう。
WebAssemblyにコンパイルするGoのソースファイルmain.goを作成します。"Hello, WebAssembly!"と出力するだけの、Go入門でよく使われるコードです。
package main import "fmt" func main() { fmt.Println("Hello, WebAssembly!") }
ソースファイルを作成したら、すぐにコンパイルできます。Windows環境では、環境変数GOOSとGOARCHを設定してからgo buildコマンドを実行します。
% GOOS=js GOARCH=wasm go build -o test.wasm main.go
これで、main.goファイルをコンパイルして、WebAssemblyバイナリであるtest.wasmファイルが生成されます。GOOSとGOARCHは、それぞれターゲットのOSとアーキテクチャを指定するための環境変数です。この場合、JS(JavaScript)環境のWASM(WebAssembly)という意味になります(go tool dist listコマンドで一覧を確認できます)。-oオプションで出力先をtest.wasmと、main.goとは無関係に見えるものにしているのには理由があるのですが、これについては後述します。
生成されたtest.wasmは、Goによって用意されたJavaScriptグルー(WebAssemblyオブジェクトを生成するJavaScriptコード)とHTMLファイルを使って、すぐに表示できます。以下のようにして、2つのファイルをプロジェクトにコピーします。
% cp $(go env GOROOT)/misc/wasm/wasm_exec.js . % cp $(go env GOROOT)/misc/wasm/wasm_exec.html .
wasm_exec.jsファイルがJavaScriptグルー、wasm_exec.htmlファイルがブラウザから開くHTMLファイルです。GOROOTは、Goのインストールされたフォルダを保持する環境変数です。Windows環境では環境変数GOROOTは設定されないので、C:\Program Files\Go\misc\wasmなどのフォルダ名を直接指定してください。
goexecコマンドによってHTTPサーバを起動し、wasm_exec.htmlファイルを開いてみます。
% ~/go/bin/goexec 'http.ListenAndServe(`:8080`, http.FileServer(http.Dir(`.`)))'
http://localhost:8080/wasm_exec.htmlをブラウザで開くと、以下のように「Run」というボタンだけのシンプルなページが表示されます。この[Run]ボタンをクリックすると、JavaScriptコンソールにmain.goのfmt.Println文で記述されたメッセージが出力されます。
このように、Goでは内部でWebAssembly対応が完結しています。
動作確認できたところで、ここまでの手順で使用されたファイルを見ていきましょう。ただしmain.goについては改めての説明は不要と思うので省略します。HTMLファイルwasm_exec.htmlのみを見てみます(wasm_exec.jsファイルは、Goオブジェクトに関する内部的な処理を受け持つのが役割なので省略)。
<!doctype html> <html> <head> <meta charset="utf-8"> <title>Go wasm</title> </head> <body> <!-- JavaScriptグルーの読み込み --> <script src="wasm_exec.js"></script> (1) <script> // polyfillのためのコードは省略 // Goオブジェクトの生成 const go = new Go(); (2) let mod, inst; // WebAssemblyバイナリの読み込み WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject).then((result) => { (3) // モジュールとインスタンスをセット mod = result.module; (4) inst = result.instance; document.getElementById("runButton").disabled = false; }).catch((err) => { console.error(err); }); // ボタンクリックで呼び出されるrun関数の定義 async function run() { (5) console.clear(); // Goのmain関数を呼び出す await go.run(inst); (6) inst = await WebAssembly.instantiate(mod, go.importObject); } </script> <button onClick="run();" id="runButton" disabled>Run</button> (7) </body> </html>
(1)は、JavaScriptグルーのwasm_exec.jsを読み込んでいます。これによって、Goオブジェクトが利用可能になります。
(2)は、Goオブジェクトをインスタンス化しています。以降、このオブジェクトを通じてWebAssemblyプログラムにアクセスします。
(3)は、WebAssembly.instantiateStreamingメソッドによるWebAssemblyプログラムtest.wasmの読み込みです。ここでファイル名が決め打ちされているため、go buildコマンドの出力先をtest.wasmと明示したのです。
(4)からは、WebAssemblyモジュールとインスタンスを取得し、エラーの発生はなかったとして(7)で記述されているボタンの無効化を解除します。
(5)は、ボタンのクリックで呼び出される関数runの定義です。コンソールをクリア後、(6)にてGoオブジェクトのrun関数を呼び出しています。このrun関数がmain.goのmain関数を呼び出すことで、コンソールにfmt.Println文の内容が出力されます。run関数の実行後は、WebAssembly.instantiateメソッドによってインスタンスをリセットしています。
Goには、Web操作のためのパッケージsyscall/jsが用意されています。これを使うことで、例えばGoのコードからDOMの操作が可能になります。ここでは、前回と同様にGoで円周率を計算し、それをDOMに反映させる事例を紹介します。
src/lib.rsファイルに、円周率を計算してDOMに反映させる関数を追加します。円周率は、ライプニッツの公式で計算しています(アルゴリズムの詳細は省略します)。
package main // 必要なパッケージのインポート import ( (1) "strconv" "syscall/js" ) // ボタンクリックでJavaScriptから呼び出される関数 func calc(this js.Value, args []js.Value) any { (2) // DOMから計算回数(countText)と計算結果(resultText)の要素を取得しておく document := js.Global().Get("document") (3) countText := document.Call("getElementById", "countText") resultText := document.Call("getElementById", "resultText") // 計算回数を取得して整数値として評価する countValue, err := strconv.ParseInt(countText.Get("value").String(), 10, 64) (4) if err != nil { return map[string]any{"error": err.Error()} } // 円周率の計算 value := 1.0 (5) for i := int64(1); i <= countValue; i++ { index := 2 * i - 1 value -= 1.0 / float64(2 * index + 1) value += 1.0 / float64(2 * (index + 1) + 1) } value *= 4.0 // 計算結果をセットし、関数の戻り値ともする resultText.Set("textContent", value) (6) return map[string]any{"result": value} } func main() { // calc関数をgoCalc関数にマップする js.Global().Set("goCalc", js.FuncOf(calc)) (7) // ここで実行をブロックし終了しないようにする select {} (8) }
(1)は、strconvとsyscall/jsパッケージを使うためのimport文です。
(2)は、ボタンクリックでJavaScriptから呼び出される関数の定義です。この関数は後述するFuncOf関数の引数になるので、シグネチャはリストの通りになっている必要があります。
(3)からは、DOMからGlobalメソッドでdocumentオブジェクトを取得、そこからさらにGetメソッドで計算回数のinput要素と計算結果のspan要素をそれぞれ取得しています。ここで使用されているメソッドについて概略を表にまとめておきます。jsは、syscall/jsパッケージによるJavaScriptとのインタフェースとなるオブジェクトです。
構文 | 概要 |
---|---|
Global() Value | JavaScriptのグローバルオブジェクト(windowまたはglobal)を返す |
Get(p string) Value | pで指定されるJavaScriptのプロパティの値を返す |
Set(p string, x any) | pで指定されるJavaScriptのプロパティに値xをセットする |
Call(m string, args ...any) Value | mで指定されるJavaScriptのメソッドをargsを引数として呼び出して結果を返す。argsは不定長なので2個以上の引数にも対応する |
表 使用したsyscall/jsパッケージのメソッド |
(4)では、計算回数のフィールドを整数値(int64)として評価しています。エラー発生時には、エラーの内容を保持したマップを返して関数を終了します。
(5)からは、ライプニッツの公式による円周率の計算です。計算回数だけ計算を実行して結果をvalueにセットします。
(6)からは、上記のSetメソッドを用いた計算結果の要素への設定です。続けて、計算結果を保持したマップを返して関数を終了します。
(7)では、(2)で定義した関数calcを、SetメソッドでグローバルにgoCalcメソッドとして登録しています。これにより、JavaScriptからgoCalcという名前でcalc関数を呼び出せます。このサンプルでは示していませんが、引数の受け取りも可能です。
(8)はselect文ですが、中身が空なので実行がブロックされます。これにより、main関数を終了することでランタイムが消去されないようにしています。
この時点で、form.goに対してgo buildコマンドの実行を済ませておきます。
% GOOS=js GOARCH=wasm go build -o form.wasm form.go
Goの関数を呼び出して計算結果を書き戻すHTMLファイルを作成します。
…略… <script> …略… const go = new Go(); (1) let mod, inst; WebAssembly.instantiateStreaming(fetch("form.wasm"), go.importObject).then((result) => { mod = result.module; inst = result.instance; go.run(inst); }).catch((err) => { console.error(err); }); function calc() { (2) console.log(goCalc()); } </script> <p><label>計算回数は? <input type="number" id="countText" /></label> (3) <button id="calcButton" onclick="calc();">計算</button> </p> <p>円周率は <span id="resultText">未計算</span> です。</p> …略…
(1)からの流れはwasm_exec.htmlと同様ですが、インスタンス生成後に直ちにGoのmain関数を呼び出しています。これにより、goCalc関数がJavaScriptコードから呼び出し可能になります。
(2)は、ボタンクリックで呼び出される関数です。処理内容は、Go側から登録された関数goCalcを呼び出して、戻り値をコンソールに出力しているのみです。正常に終了すれば、計算結果がコンソールにも出力されます。
(3)以降は計算回数のテキスト入力要素、計算を開始するためのボタン、そして計算結果を格納するspan要素です。ボタンを除き、id属性の値がGoのコードにおけるものと一致している必要があることに注意してください。
HTTPサーバを起動し、Webブラウザでhttp://localhost:8080/wasm_form.htmlにアクセスして、以下のように計算結果が表示されれば成功です。
今回は、GoによるWebAssemblyプログラムの開発を紹介しました。
次回は、PythonとRubyのWebAssemblyへのアプローチについて紹介します。
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・Twitter: @yyamada(https://twitter.com/yyamada)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.