WebAssemblyのどこが良いのかわからないからこそWasmに触れてみた話


この記事はRetty Advent Calendar 2019の5日目です。
昨日は @shyne さんの Hello ViewBinding! 歴史から学ぶ明日からViewBindingを使うべき理由 でした。

前置き

Mozilla開発者向けニュースレターでWebAssembly(以下Wasm)の更新情報など目にする事があり、
今まで食わず嫌いしていたWasmについて少し興味が湧きました。
この機会に、Wasmを推し進めるMozillaの考え方を理解しようと思います。

Wasmの特徴

  • 2015年頃から世にでてきた新しいバイナリフォーマットファイル
  • 拡張子は .wasm
  • 最小機能でもファイルサイズは数500KByte~1.5MByteになる(コンパイラによる)
  • モダンなウェブブラウザが対応
  • JavaScriptのようなインタープリタから機械語への変換処理と比較した場合、高速に処理される
  • バイナリフォーマットにするためにコンパイラが必要
  • C/C++, Rust, Go(1.11~)などの低レベルプログラミングができる言語での開発が盛ん

コンパイラ毎にコンパイルに要する時間が異なったり、開発時のプログラミング言語の仕様を持ち込んでコンパイルするためファイルサイズが変わります。

Goがバージョン1.11からWasmに対応したことでカジュアルにWasm開発にチャレンジできるようになりました。

まずはHello worldする

以下を参考に
https://github.com/golang/go/wiki/WebAssembly#getting-started

まずはプロジェクトディレクトリを作成します。

$ mkdir sample

ロジックを組みます

root.go
package main

import "fmt"

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

ロジック本体の親ファイルはroot.goと命名する決まりがあります。
goのパッケージを使えるので必要に応じてimportします。
上記を実行するとブラウザconsoleに ”Hello, WebAssembly” と表示されます。

wasmファイルはWebAssembly JavaScript APIを使って実行されるためwasm_exec.jsをファイルをroot.goと同じプロジェクトディレクトリに配置します。

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

環境変数を持たせてwasmとしてビルドします。

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

サーバーを立ててブラウザで確認します

goexec 'http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))'

http://localhost:8080
にアクセスしてconsoleを開くとメッセージが確認できました。

Goの開発環境があればスタートまでは時間がかからずに開発できました。

Vuguでコンポーネント開発してみる

GoでWebAssemblyに取り組む世間の人が気になったので、ググるとVuguの事例がいくつか出てくるので調べてやってみました。
VuguとはGoで簡単にウェブUIを構築するためのライブラリです。
Vue.jsライクなコーディングでコンポーネントに閉じたり、Propsで値を受け渡しできます。
.vugu という拡張子のファイルを元にgoファイルを生成してくれます。

2019/12/05現在 VuguのGo推奨バージョンは1.13となっています。

まずrootコンポーネントを作成してみます

root.vugu
<div class="delicious-component">
    <input type="text" id="imagetext" @change='data.InputSrcText(event)'>
    <button @click="data.AddImage(event)">Add Image</button>
    <li vg-for='data.Images'>
        <image
            :src='value.Src'
            :alt='value.Alt'
        >
    </li>
</div>

<style>
.delicious-component {
    width: 400px;
}
.delicious-component img {
    width: 100%;
}
</style>

<script type="application/x-go">
type RootData struct {
    Images []ImageData
    SrcText string
}

func (data *RootData) InputSrcText(e *vugu.DOMEvent) {
    data.SrcText = e.JSEvent().Get("target").Get("value").String()
}

func (data *RootData) AddImage(e *vugu.DOMEvent) {
    eEnv := e.EventEnv()

    go func() {
        eEnv.Lock()
        defer eEnv.UnlockRender()
        image := ImageData{
            Src: data.SrcText,
            Alt: data.SrcText,
        }
        data.Images = append(data.Images, image)
    }()
}
</script>

IntelliJでは少なくともシンタックスハイライトをもらえません。(そのうち誰かの努力によりプラグインが提供されるかもしれません)

html、style、scriptで構成されており完全にフロントエンドのコンポーネント開発のJavaScriptフレームワークの様相に、全く異なる言語を持ち込んだことに、少し感動しました。
命名規則としてroot.vuguの場合 RootData というデータプロパティで構造体を定義します。
htmlとのやり取りはこのデータプロパティ経由で行います。
また、子コンポーネントにはいわゆるpropsでデータを流し込めます。

ちなみにビルドすると以下のようなvugu.VGNodeの記述が埋め込まれた静的ファイルができるので、vuguを使わずplaneにGoでWASM開発する上でリファレンス実装として参考にする事もできそうです。

root.go
func (comp *Root) BuildVDOM(dataI interface{}) (vdom *vugu.VGNode, css *vugu.VGNode, reterr error) {
    data := dataI.(*RootData)
    _ = data
    _ = fmt.Sprint
    _ = reflect.Value{}
    event := vugu.DOMEventStub
    _ = event
    css = &vugu.VGNode{Type: vugu.VGNodeType(3), Data: "style", DataAtom: vugu.VGAtom(458501), Namespace: "", Attr: []vugu.VGAttribute(nil)}
    css.AppendChild(&vugu.VGNode{Type: vugu.VGNodeType(1), Data: "\n.delicious-component {\n    width: 400px;\n}\n.delicious-component img {\n    width: 100%;\n}\n", DataAtom: vugu.VGAtom(0), Namespace: "", Attr: []vugu.VGAttribute(nil)})
    var n *vugu.VGNode
    n = &vugu.VGNode{Type: vugu.VGNodeType(3), Data: "div", DataAtom: vugu.VGAtom(92931), Namespace: "", Attr: []vugu.VGAttribute{vugu.VGAttribute{Namespace: "", Key: "class", Val: "delicious-component"}}}
    vdom = n
....

子コンポーネントのimage.vuguを作成します。

image.vugu
<div class="image">
    <img
        src="data.Src"
        alt="data.Alt"
    >
</div>

<style>
  .image {
    display: inline;
  }
</style>

<script type="application/x-go">

type ImageData struct {
    Src string
    Alt string
}

func (data *Image) NewData(props vugu.Props) (interface{}, error) {
  ret := &ImageData{}
  ret.Src, _ = props["src"].(string)
  ret.Alt, _ = props["src"].(string)
  return ret, nil
}
</script>

親から受け取ったpropsを使ってimgタグを生成します。
ビルドしてブラウザで見られるようにサーバーを起動するファイルを作成します。

devserver.go
// +build ignore

package main

import (
    "log"
    "net/http"
    "os"

    "github.com/vugu/vugu/simplehttp"
)

func main() {
    wd, _ := os.Getwd()
    l := "127.0.0.1:8844"
    log.Printf("Starting HTTP Server at %q", l)
    h := simplehttp.New(wd, true)
    // include a CSS file
    // simplehttp.DefaultStaticData["CSSFiles"] = []string{ "/my/file.css" }
    log.Fatal(http.ListenAndServe(l, h))
}

実行します。

$ go run devserver.go

フォームに画像URLを入れると表示されました。

devserverで確認したところroot.wasmのような静的ファイルは有りませんでした。
動的にコンパイルして出力しているようです。

試していませんがbuildしてdistに吐き出すことでCIツールなどと組み合わせてdeployもできそうです。
dist.goの実行でmain.wasmが出力される点も丁寧に記載されています。
https://www.vugu.org/doc/build-and-dist

Vuguハマリポイント

jsonをhttpで取ってきてparseする参考実装を実行しようとしたところ
moduleが読み込めないとのことで先に進めなくなりました。
このあたりで詰んだためこれ以上はVuguプロジェクトチームの更新を待つことにしました。

<div class="demo-comp">
    <div vg-if='data.isLoading'>Loading...</div>
    <div vg-if='len(data.bpi.BPI) > 0'>
        <div>Updated: <span vg-html='data.bpi.Time.Updated'></span></div>
        <ul>
            <li vg-for='data.bpi.BPI'>
                <span vg-html='key'></span> <span vg-html='fmt.Sprint(value.Symbol, value.RateFloat)'></span>
            </li>
        </ul>
    </div>
    <button @click="data.HandleClick(event)">Fetch Bitcoin Price Index</button>
</div>

<script type="application/x-go">
import "encoding/json"
import "net/http"
import "log"

type RootData struct {
    bpi bpi
    isLoading bool
}

type bpi struct {
    Time struct { Updated string `json:"updated"` } `json:"time"`
    BPI map[string]struct { Code string `json:"code"`; Symbol string  `json:"symbol"`; RateFloat float64 `json:"rate_float"` } `json:"bpi"`
}

func (data *RootData) HandleClick(event *vugu.DOMEvent) {

    data.bpi = bpi{}

    go func(ee vugu.EventEnv) {

        ee.Lock()
        data.isLoading = true
        ee.UnlockRender()

        res, err := http.Get("https://api.coindesk.com/v1/bpi/currentprice.json")
        if err != nil {
            log.Printf("Error fetch()ing: %v", err)
            return
        }
        defer res.Body.Close()

        var newb bpi
        err = json.NewDecoder(res.Body).Decode(&newb)
        if err != nil {
            log.Printf("Error JSON decoding: %v", err)
            return
        }

        ee.Lock()
        defer ee.UnlockRender()
        data.bpi = newb
        data.isLoading = false

    }(event.EventEnv())
}

</script>
Error from compile: exit status 1 (out path="/var/folders/pp/8jq5bcfd549dbg2mdbbl9qd40000gp/T/main_wasm_104540040"); Output:
build main: cannot load github.com/vugu/vugu/domrender: module github.com/vugu/vugu@latest found (v0.1.0), but does not contain package github.com/vugu/vugu/domrender

Wasmの良いと感じたところ

別の言語パラダイムで作った生成物とJavaScriptとのコンビネーション技が使えるところが一番よいと感じました。
Mozillaが手掛けるRustとの相性がよく、Rustの具体的な使いみちでもあります。
Vuguのようなライブラリは今後も色々でてくると期待できます。

また、以下TechCrunchの記事通り、オフラインでの活用についても将来性があると思います。
https://jp.techcrunch.com/2019/11/13/2019-11-12-mozilla-partners-with-intel-red-hat-and-fastly-to-take-webassembly-beyond-the-browser/

反面、まだまだ多くのエンジニアが参入可能な環境が整っていないことや具体的な活用方法アイディアが湧きにくい現状を考えると日本で盛んに開発されるのは少し先かなという印象です。
そのまま淘汰される技術とならなければよいのですが。

参考:

https://developer.mozilla.org/ja/docs/WebAssembly
https://www.vugu.org/
https://hacks.mozilla.org/category/webassembly/

ご覧いただきありがとうございました。

明日は @wtnVegnaあなたのエリアは何処から? 〜地理空間クラスタリングとの差分検証〜 です。