Go 中的 WASM 很棒:全網最全示例教程

WASM 的概念,這幾年還是挺火的,新的語言,比如 Rust、Go、Swift 等,都對 WASM 提供支持。相比之下,Go 語言的簡單性,使得對 WASM 的支持,使用起來也較簡單。本文是目前公開資料中爲數不多較完整的教程,希望能對你有幫助。

01 WASM 是什麼

標題說:“Golang 中的 Wasm 太棒了。”,但請用幾句話來說 “Wasm” 是什麼?

WebAssembly 主頁說:“WebAssembly(縮寫爲 Wasm)是一種基於堆棧的虛擬機的二進制指令格式。Wasm 被設計爲編程語言的可移植編譯目標,支持在 Web 上部署客戶端和服務器應用程序。”

總結就是:

現在,你可以用 JavaScript 和 NodeJS 運行 Wasm,我們最近看到了像 Wasmer 項目這樣的 Wasm 運行時的誕生,允許在任何地方運行 Wasm。

我喜歡說 “一個 wasm 文件就像一個容器鏡像,但更小,沒有操作系統”

02 Wasm 是多語言的,但是...

你可以用多種語言編譯一個 Wasm 文件:C/C++、Rust、Golang、Swift …… 我們甚至看到了專門用於構建 Wasm 的語言的出現,比如 AssemblyScript[1] 或有前途的 Grain[2](可以密切關注它,語法很可愛)。

今年夏天,我決定開始使用 Wasm。這種趨勢似乎是使用 Rust,但我很快就明白我的小步驟會很複雜。困難不一定來自語言本身。最乏味和困難的部分是我在瀏覽器中運行一個簡單的 “Hello World” 所需的所有工具。經過一番搜索,我發現 GolangWasm 提供了非常簡單的支持(比 Rust 簡單得多)。所以,我的假期作業是用 Golang 完成的。

Golang 對 Wasm 的支持非常棒。通常,WebAssembly 有四種數據類型(32 和 64 位整數,32 和 64 位浮點數),使用帶有字符串參數(甚至 JSON 對象)的函數可能會很混亂。幸運的是,Go 提供了wasm_exec.js 與 JavaScript API 交互的文件。

03 先決條件

要運行此博客文章的示例,你需要:

順便說一句,爲了提供我的頁面,我使用帶有以下代碼的 Fastify[3] 項目:

index.js

const fastify = require('fastify')({ logger: true })
const path = require('path')

// Serve the static assets
fastify.register(require('fastify-static'){
  root: path.join(__dirname, ''),
  prefix: '/'
})

const start = async () ={
  try {
    await fastify.listen(8080, "0.0.0.0")
    fastify.log.info(`server listening on ${fastify.server.address().port}`)

  } catch (error) {
    fastify.log.error(error)
  }
}
start()

我使用這個package.json文件來安裝 Fastify(使用npm install):

package.json

{
    "dependencies"{
        "fastify""^3.6.0",
        "fastify-static""^3.2.1"
    }
}

我在這裏創建了一個項目 https://gitlab.com/k33g_org/suborbital-demo,如果你用 GitPod[4] 打開它,你會得到一個準備好的開發環境,你不需要安裝任何東西。

04 必不可少的 “Hello World!”

創建一個項目

首先,創建一個hello-world目錄,然後在該目錄中創建 2 個文件:

使用以下源代碼:

main.go

package main

import (
    "fmt"
)

func main() {
  fmt.Println("👋 Hello World 🌍")
    // Prevent the function from returning, which is required in a wasm module
    <-make(chan bool)
}

index.html

<html>
    <head>
        <meta charset="utf-8"/>
        <script src="wasm_exec.js"></script>

    </head>
    <body>
        <h1>WASM Experiments</h1>
        <script>
            // This is a polyfill for FireFox and Safari
            if (!WebAssembly.instantiateStreaming) { 
                WebAssembly.instantiateStreaming = async (resp, importObject) ={
                    const source = await (await resp).arrayBuffer()
                    return await WebAssembly.instantiate(source, importObject)
                }
            }

            // Promise to load the wasm file
           function loadWasm(path) {
             const go = new Go()

             return new Promise((resolve, reject) ={
               WebAssembly.instantiateStreaming(fetch(path), go.importObject)
               .then(result ={
                 go.run(result.instance)
                 resolve(result.instance)
               })
               .catch(error ={
                 reject(error)
               })
             })
           }

         // Load the wasm file
         loadWasm("main.wasm").then(wasm ={
             console.log("main.wasm is loaded 👋")
         }).catch(error ={
             console.log("ouch", error)
         }) 

</script>
    </body>
</html>

備註:最重要的部分是:

  • 這行代碼 <script src="wasm_exec.js"></script>

  • 和這一行WebAssembly.instantiateStreaming,它是允許加載 wasm 文件的 JavaScript API。

你還需要go.mod文件,使用以下命令生成一個:go mod init hello-world,它的內容如下:

module hello-world

go 1.16

構建你的第一個 Wasm 模塊

在構建 Wasm 模塊之前,你需要獲取wasm_exec.js文件,然後才能啓動編譯:

$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
$ GOOS=js GOARCH=wasm go build -o main.wasm

現在,使用命令 node index.js 爲你的 html 頁面提供服務,以運行 Fastify http 服務器,並使用你喜歡的瀏覽器打開 http://localhost:8080,然後打開開發人員控制檯工具:

wasm experiments

所以,上手很簡單,但是如果你查看 main.wasm 的大小,你會發現生成的文件大小在 2.1M 左右!!!老實說,我覺得這是不可接受的。幸運的是,使用 TinyGo 是一個較友好的解決方案。讓我們看看。

source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/01-hello-world

05 TinyGo 版的 “Hello World”

首先,什麼是 TinyGo?TinyGo 允許爲微控制器編譯 Golang 源代碼,它也可以將 Go 代碼編譯爲 Wasm。TinyGo 是一個用於 “小地方” 的編譯器,因此生成的文件要小得多

將你的 hello-world 項目複製到一個新目錄 hello-world-tinygo 並更改go.mod文件的內容:

module hello-world-tinygo

go 1.16

在構建 Wasm 文件之前,這一次,你需要獲取 wasm_exec.js 與 TinyGo 的相關信息,然後才能編譯:

$ wget https://raw.githubusercontent.com/tinygo-org/tinygo/v0.19.0/targets/wasm_exec.js
$ tinygo build -o main.wasm -target wasm ./main.go

如果你提供 html 頁面,你將獲得與前一個示例相同的結果。但是看看 main.wasm 的大小。現在大小是 223K,小好多。

請記住,TinyGo 支持 Go 語言的一個子集,因此並非所有語言都可用(https://tinygo.org/docs/reference/lang-support/)。對於我的實驗來說,這就足夠了。

源代碼:https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/02-hello-world-tinygo

06 我的私藏貨

我看過太多冗長的教程,最終停留在這個簡單的 “hello world” 上而沒有進一步。他們甚至沒有解釋如何將參數傳遞給函數。通常,這只是項目 “入門” 的掩飾,沒有後續深入的講解。

今天,我給你分享所有讓你對 WASM 理解更多的私藏貨。

以下是我今天將介紹的 Wasm 和瀏覽器之間的不同交互:

與 DOM 交互

我們將使用"syscall/js" 這個 Go 包從 Go 代碼向 Html 文檔對象模型添加子標籤。根據文檔:“使用 js/wasm 架構時,Package js 可以訪問 WebAssembly 主機環境。它的 API 基於 JavaScript 語義。” . 這個包公開了一小組功能:類型 Value(Go JavaScript 數據表示)和從 JavaScript 主機請求 Go 的方式。

只需更改以下代碼 main.go

package main

import (
    "syscall/js"
)

func main() {
  message := "👋 Hello World 🌍"

  document := js.Global().Get("document")
  h2 := document.Call("createElement""h2")
  h2.Set("innerHTML", message)
  document.Get("body").Call("appendChild", h2)

  <-make(chan bool)
}

編譯代碼:tinygo build -o main.wasm -target wasm ./main.go 並提供 html 頁面 node index.js,然後打開 http://localhost:8080/

完整源碼:https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/03-dom

現在,讓我們看看如何創建一個可調用的 Go 函數,我們將在我們的 html 頁面中使用它。

調用 Go 函數

這一次,我們需要將函數 “導出” 到全局上下文中(即瀏覽器中的 window,或 NodeJS 中的 global)。"syscall/js" 這個 Go 包再次提供了必要的幫助程序來做到這一點。

像往常一樣,創建一個新目錄 first-function(使用前面的示例)並通過 go.mod 更改模塊的值來更新文件:module first-function

這是的源代碼main.go

package main

import (
    "syscall/js"
)

func Hello(this js.Value, args []js.Value) interface{} {
    message := args[0].String() // get the parameters
    return "Hello " + message
}

func main() {
    js.Global().Set("Hello", js.FuncOf(Hello))

    <-make(chan bool)
}

我們需要修改 index.html 文件來調用 Go 函數 Hello

index.html:

<html>
    <head>
        <meta charset="utf-8"/>
        <script src="wasm_exec.js"></script>

    </head>
    <body>
        <h1>WASM Experiments</h1>
        <script>
            // polyfill
            if (!WebAssembly.instantiateStreaming) { 
                WebAssembly.instantiateStreaming = async (resp, importObject) ={
                    const source = await (await resp).arrayBuffer()
                    return await WebAssembly.instantiate(source, importObject)
                }
            }

      function loadWasm(path) {
        const go = new Go()
        return new Promise((resolve, reject) ={
          WebAssembly.instantiateStreaming(fetch(path), go.importObject)
          .then(result ={
            go.run(result.instance)
            resolve(result.instance)
          })
          .catch(error ={
            reject(error)
          })
        })
      }

    loadWasm("main.wasm").then(wasm ={
        console.log("main.wasm is loaded 👋")
        console.log(Hello("Bob Morane"))
        document.querySelector("h1").innerHTML = Hello("Bob Morane")
            }).catch(error ={
        console.log("ouch", error)
      }) 

</script>
    </body>
</html>

發生了什麼變化?,只有這兩行:

所以,

你可以看到頁面內容已更新,但我們在控制檯中有一些錯誤消息。不用擔心,它很容易修復;這是一個已知錯誤 https://github.com/tinygo-org/tinygo/issues/1140,解決方法很簡單:

function loadWasm(path) {
    const go = new Go()
    //remove the message: syscall/js.finalizeRef not implemented
    go.importObject.env["syscall/js.finalizeRef"] = () ={}

    return new Promise((resolve, reject) ={
        WebAssembly.instantiateStreaming(fetch(path), go.importObject)
        .then(result ={
            go.run(result.instance)
            resolve(result.instance)
        })
        .catch(error ={
            reject(error)
        })
    })
}

刷新頁面,沒有問題了。

現在,你幾乎擁有深入 WASM 所需的一切。不過,我想把我的更多私藏貨分享給你。

完整代碼:https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/04-first-function

07 我的其他私藏貨

如何通過 JavaScript 返回 “可讀” 的對象?

這一次,我們將 2 個字符串參數傳遞給 Hello 函數 ( firstNamelastName),並使用類型 map[string]interface{} 返回一個 json 對象 :

Golang 函數

func Hello(this js.Value, args []js.Value) interface{} {

    firstName := args[0].String()
    lastName := args[1].String()

    return map[string]interface{}{
        "message""👋 Hello " + firstName + " " + lastName,
        "author":  "@k33g_org",
    }

}

從 JavaScript 調用 Hello 函數很簡單

loadWasm("main.wasm").then(wasm ={
    let jsonData = Hello("Bob""Morane")
    console.log(jsonData)
    document.querySelector("h1").innerHTML = JSON.stringify(jsonData)

}).catch(error ={
    console.log("ouch", error)
})

爲你的頁面提供服務 node index.js

完整源碼:https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/05-return-object

調用 Hello 時如何使用 Json 對象作爲參數?

如果我想在 JavaScript 中使用 Json 對象作爲參數,就像這樣:

let jsonData = Hello({firstName: "Bob", lastName: "Morane"})

我會像這樣寫我的 Golang 函數:

func Hello(this js.Value, args []js.Value) interface{} {

    // get an object
    human := args[0]
    // get members of an object
    firstName := human.Get("firstName")
    lastName := human.Get("lastName")

    return map[string]interface{}{
        "message""👋 Hello " + firstName.String() + " " + lastName.String(),
        "author":  "@k33g_org",
    }

}

完整代碼:https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/06-json-as-parameter

調用 Hello 時如何使用數組作爲參數?

JavaScript 調用

let jsonData = Hello(["Bob""Morane", 42, 1.80])

Golang 函數

func Hello(this js.Value, args []js.Value) interface{} {

    // get members of an array
    firstName := args[0].Index(0)
    lastName := args[0].Index(1)
    age := args[0].Index(2)
    size := args[0].Index(3)

    return map[string]interface{}{
        "message":   "👋 Hello",
        "firstName": firstName.String(),
        "lastName":  lastName.String(),
        "age":       age.Int(),
        "size":      size.Float(),
        "author":    "@k33g_org",
    }
}

完整代碼:https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/07-array-as-parameter

如何返回一個數組?

Golang 函數

func GiveMeNumbers(_ js.Value, args []js.Value) interface{} {
    return []interface{} {1, 2, 3, 4, 5}
}

完整代碼:https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/08-return-an-array

就這些了吧。目前,我仍在學習 Wasm 和 Golang 的 Js 包,但我已經從中獲得了一些樂趣。希望你也一樣。

原文鏈接:https://blog.suborbital.dev/foundations-wasm-in-golang-is-fantastic

參考資料

[1]

AssemblyScript: https://www.assemblyscript.org/

[2]

Grain: https://grain-lang.org/

[3]

Fastify: https://www.fastify.io/

[4]

GitPod: https://www.gitpod.io/


本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/HNaH775qMLqY0-Oq7BQEsQ