你好WebAssembly

0x5010發表於2018-06-28

概述

上篇介紹瞭如何成功執行了Go編譯的第一個WebAssembly(以下簡稱wasm)二進位制檔案,接著進一步測試下Gowasm的能實現的功能。

從Go呼叫JS

Go的標準庫有一個新的包syscall/js,先看下js.go檔案。裡面定義了個新的型別js.Value,它表示一個JavaScript值。它提供了一個簡單的API來操縱任何型別的JavaScript值並與之互動:

  • js.Value.Get()js.Value.Set()檢索並設定Object值的屬性
  • js.Value.Index()js.Value.SetIndex()檢索並設定Array值中的值
  • js.Value.Call()在一個Object值上呼叫一個方法
  • js.Value.Invoke()呼叫一個函式值
  • js.Value.New()在代表JS型別的引用上呼叫new運算子
  • 在相應的Go型別中檢索JavaScript值的其他方法(如js.Value.Int()js.Value.Bool()

一個js.ValueOf()函式,它接受任何Go基本型別並返回相應的js.Value

最後是一些有趣的變數:

  • js.Undefinedjsundefined對應的js.Value
  • js.Nulljsnull對應的js.Value
  • js.Global允許訪問js全域性範圍的js.Value

嘗試呼叫下jswindow.alert()將訊息其顯示在對話方塊中,而不是傳送到console

由於在瀏覽器中,global就是window,從global中檢索alert(),於是有了一個alert型別的js.Value變數,它是對jswindow.alert的引用,在其上使用js.Value.Invoke()。可以發現在將引數傳遞給Invoke之前不需要呼叫js.ValueOf(),它接受interface{}引數,並通過呼叫ValueOf去執行。

package main

import (
    "syscall/js"
)

func main() {
    alert := js.Global().Get("alert")
    alert.Invoke("Hello wasm!")
}

現在,當點選按鈕時,會彈出一條包含Hello wasm!訊息的對話方塊。

完整程式碼

從JS呼叫Go

如上從Go呼叫js非常簡單,接著看callback.go檔案。裡面定義了一個新的js.Callback型別,它代表一個Gofunc包裝以便用作js回撥。一個js.NewCallback()函式,它接受一個js.Value切片(並且不返回任何內容)並返回一個js.Callback。並提供一些機制來管理活動回撥,以及一個js.Callback.Close()函式,當不再使用回撥時必須呼叫它來釋放相應資源。另外還有一個js.NewEventCallback()函式來接受js事件。

先試著做一些簡單的事情,從js端觸發Gofmt.Println

當前執行wasm二進位制檔案的run()函式如下所示,需要在wasm_exec.html中進行一些調整,讓它能夠從Go接收回撥並呼叫它。

async function run() {
    console.clear()
    await go.run(ist)
    inst = await WebAssembly.instantiate(mod, go.importObject)

它啟動wasm二進位制檔案並等待它終止,然後重新例項化它以便下次執行。新增一個新的函式,它將接收並儲存Go回撥,並在完成後立即解析Promise

let printMessage
let printMessageReceived
let resolvePrintMessageReceived
function setPrintMessage(callback) {
    printMessage = callback
    resolvePrintMessageReceived()
}

現在調整run()函式以使用回撥:

async function run() {
    console.clear()
    printMessageReceived = new Promise(resolve => {
        resolvePrintMessageReceived = resolve
    })
    const run = go.run(inst)
    await printMessageReceived
    printMessage('Hello Wasm!')
    await run
    inst = await WebAssembly.instantiate(mod, go.importObject)

現在Go部分需要建立回撥,將其傳送給js端並等待它被呼叫。需要一個channel來通知回撥被呼叫了,然後編寫實際的printMessage()``func

var done = make(chan struct{})

func printMessage(args []js.Value) {
    message := args[0].String()
    fmt.Println(message)
    done <- struct{}{}
}

正如所看到的,引數是在js.Value的切片中接收到的,在第一個元素上呼叫js.Value.String()轉化為Gostring來獲取message。現在可以在回撥中包裝這個func,然後呼叫jssetPrintMessage()函式,就像呼叫window.alert()時一樣,最後就是等待回撥被呼叫,這個很重要,因為回撥是在goroutine中執行的,因此主goroutine必須等待回撥被呼叫,否則wasm二進位制會提前終止。

callback := js.NewCallback(printMessage)
defer callback.Close()

setPrintMessage := js.Global().Get("setPrintMessage")
setPrintMessage.Invoke(callback)

<-done

完整的Go程式應如下所示:

import (
    "fmt"
    "syscall/js"
)

var done = make(chan struct{})

func main() {
    callback := js.NewCallback(printMessage)
    defer callback.Close()

    setPrintMessage := js.Global().Get("setPrintMessage")
    setPrintMessage.Invoke(callback)
    <-done
}

func printMessage(args []js.Value) {
    message := args[0].String()
    fmt.Println(message)
    done <- struct{}{}
}

編輯wasm_exec.html,繼續重用wasm_exec.js。現在,當點選按鈕時,和之前的hello world類似Hello Wasm!訊息被輸出在console中。

完整程式碼

持續執行

js呼叫Go比從Go呼叫js更麻煩一些,特別是在js部分。這主要是因為需要等待Go回撥傳遞給js,而且執行完就終止了,如何讓wasm不會在呼叫回撥之後終止,卻繼續執行並接收其他呼叫?

這一次從Go開始,同樣需要建立一個回撥並將它傳送給js端。並新增一個呼叫計數器,以便跟蹤回撥被呼叫的次數。新的printMessage()函式將列印接收到的訊息和呼叫計數器的值:

var no int

func printMessage(args []js.Value) {
    message := args[0].String()
    no++
    fmt.Printf("Message no %d: %s\n", no, message)
}

建立回撥並將其傳送給js端與我們前面的示例中完全相同,但是這一次沒有完成的channel來通知什麼時候終止主goroutine。一種方法是使用空select無限制地阻塞主goroutine。這不是很優雅,wasm二進位制檔案永遠不會完全關閉,並且可能會在瀏覽器關閉wasm_exec.html時被kill。另一種方法就是監聽頁面事件來終止主goroutine

建立回撥來接收頁面的beforeunload事件並通過一個channel通知主goroutine。這次新的beforeUnload()函式將只接受一個js.Value引數用來接受事件:

var beforeUnloadCh = make(chan struct{})

func beforeUnload(event js.Value) {
    beforeUnloadCh <- struct{}{}
}

然後可以使用js.NewEventCallback()將它包裝在一個回撥中,並將其註冊到js端:

beforeUnloadCb := js.NewEventCallback(0, beforeUnload)
defer beforeUnloadCb.Close()
addEventListener := js.Global.Get("addEventListener")
addEventListener.Invoke("beforeunload", beforeUnloadCb)

最後用beforeUnloadCh通道上的接收替換空select

<-beforeUnloadCh
fmt.Println("Bye Wasm !")

最終Go程式如下所示:

package main

import (
    "fmt"
    "syscall/js"
)

var (
    no             int
    beforeUnloadCh = make(chan struct{})
)

func main() {
    callback := js.NewCallback(printMessage)
    defer callback.Close() // This is a good practice
    setPrintMessage := js.Global.Get("setPrintMessage")
    setPrintMessage.Invoke(callback)

    beforeUnloadCb := js.NewEventCallback(0, beforeUnload)
    defer beforeUnloadCb.Close()
    addEventListener := js.Global.Get("addEventListener")
    addEventListener.Invoke("beforeunload", beforeUnloadCb)

    <-beforeUnloadCh
    fmt.Println("Bye Wasm !")
}

func printMessage(args []js.Value) {
    message := args[0].String()
    no++
    fmt.Printf("Message no %d: %s\n", no, message)
}

func beforeUnload(event js.Value) {
    beforeUnloadCh <- struct{}{}
}

現在在js部分,這是wasm二進位制檔案的載入:

const go = new Go();
let mod, inst;
WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject).then((result) => {
    mod = result.module;
    inst = result.instance;
    document.getElementById("runButton").disabled = false;
});

修改讓它在載入後直接啟動wasm二進位制檔案:

let run
(async function() {
    const go = new Go()
    const { instance } = await WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject)
    run = go.run(instance)
})()

通過輸入框和按鈕來替換我們的Run按鈕來觸發printMessage()

<input id="messageInput" type="text" value="Hello Wasm!">
<button 
        onClick="printMessage(document.querySelector('#messageInput').value)" 
        id="printMessageButton" 
        disabled
>
    Print message
</button>

接收和儲存回撥的setPrintMessage()函式變得簡單了:

let printMessage
function setPrintMessage(callback) {
    printMessage = callback
    document.querySelector('#printMessageButton').disabled = false
}

現在,當點選Print message按鈕時,應該看到輸入的資訊和計數器輸出在console中。然後,如果勾選瀏覽器控制檯的Preserve log選項並重新整理頁面,則應該在console中看到Bye Wasm !

完整程式碼

最後

上面用簡單的例子和較少的程式碼測試了syscall/jsAPI,Gojs之間更容易的相互呼叫了。如果感興趣的可以做一些基準測試比較下Gowasm與等效的純js程式碼的效能。

原文連結: https://blog.keyboardman.me/2018/06/28/hello-webassembly/