Go进行wasm编程 - 封三郎

时间:2024-03-04 18:47:57

Go进行wasm编程

wasm即webAssemble,是一种不针对特定平台的二进制格式文件。Go从1.11开始支持wasm,最初通过js.NewCallBack()注册函数,1.12开始换成了FuncOf()。

Go开发wasm需要一个go文件用于编写实现代码,编译成.wasm文件;需要一个wasm_exec.js文件,这个是Go提供的,可以从 Go 安装目录的 misc 子目录里找到,将它直接拷贝过来。它实现了和 WebAssembly 模块交互的功能;另外就是需要一个HTML文件用于加载wasm文件。当然为了工作起来,我们还要实现一个简单的HTTP服务。

一、用Go编写代码并编译成wasm文件

  1 package main
  2 
  3 import (
  4     "fmt"
  5     "math/rand"
  6     "strconv"
  7     "syscall/js"
  8     "time"
  9 )
 10 
 11 const (
 12     width  = 400
 13     height = 400
 14 )
 15 
 16 // 生成 0 - 1 的随机数
 17 func getRandomNum() float32 {
 18     rand.New(rand.NewSource(time.Now().UnixNano()))
 19     n := float32(rand.Intn(10000))
 20     return n / 10000.0
 21 }
 22 
 23 // 生成 0 - 10 的随机数
 24 func getRandomNum2() float32 {
 25     rand.New(rand.NewSource(time.Now().UnixNano()))
 26     n := float32(rand.Intn(10000))
 27     return n / 1000.0
 28 }
 29 
 30 // 使用 canvas 绘制随机图
 31 func draw() {
 32     var canvas js.Value = js.
 33         Global().
 34         Get("document").
 35         Call("getElementById", "canvas")
 36 
 37     var context js.Value = canvas.Call("getContext", "2d")
 38 
 39     // reset
 40     canvas.Set("height", height)
 41     canvas.Set("width", width)
 42     context.Call("clearRect", 0, 0, width, height)
 43 
 44     // 随机绘制 50 条直线
 45     var clineStyle = `rgba(%d, %d, %d, 0.5)`
 46     for i := 0; i < 50; i++ {
 47         lineStyle := fmt.Sprintf(clineStyle, 155+int(getRandomNum2()*10), 155+int(getRandomNum()*100), 155+int(getRandomNum()*100))
 48         fmt.Println(lineStyle)
 49         context.Call("beginPath")
 50         context.Set("strokeStyle", lineStyle)
 51         context.Call("moveTo", getRandomNum()*width, getRandomNum()*height)
 52         context.Call("lineTo", getRandomNum()*width, getRandomNum()*height)
 53         context.Call("stroke")
 54     }
 55 
 56     context.Set("font", "30px Arial")
 57     context.Set("strokeStyle", "blue")
 58     for i := 0; i < 10; i++ {
 59         context.Call("strokeText", "hello wasm", (getRandomNum2()+1)*10+getRandomNum2()*10, (getRandomNum2()+1)*10+getRandomNum2()*50)
 60     }
 61 }
 62 
 63 func registerCallbackFunc() {
 64     cb := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
 65         fmt.Println("button clicked")
 66 
 67         num1 := getElementByID("num1").Get("value").String()
 68         v1, err := strconv.Atoi(num1)
 69         if nil != err {
 70             fmt.Println("button clicked:", num1, err.Error())
 71             jsAlert().Invoke(err.Error())
 72             // panic(err)
 73             return nil
 74         }
 75 
 76         num2 := getElementByID("num2").Get("value").String()
 77         v2, err := strconv.Atoi(num2)
 78         if nil != err {
 79             fmt.Println("button clicked:", num2, err.Error())
 80             // panic(err)
 81             return nil
 82         }
 83 
 84         rlt := v1 + v2
 85         getElementByID("rlt").Set("value", rlt)
 86 
 87         return nil
 88     })
 89 
 90     getElementByID("compute").Call("addEventListener", "click", cb)
 91 }
 92 
 93 func getElementByID(id string) js.Value {
 94     return js.Global().Get("document").Call("getElementById", id)
 95 }
 96 
 97 func jsAlert() js.Value {
 98     return js.Global().Get("alert")
 99 }
100 
101 func main() {
102     fmt.Println("Hello, Go WebAssembly!")
103     draw()
104     // 通过js.Global().Get()拿到全局alert函数的引用
105     alert := js.Global().Get("alert")
106     // 调用alert.Invoke来调用alert函数
107     alert.Invoke("hello world")
108 
109     registerCallbackFunc()
110 }
Go wasm代码

将代码编译成Wasm文件,需要设置编译环境。我用的VsCode,用powershell设置环境变量始终不能生效,于是换成了Bash:

执行:go env 查看环境,注意GOOS和GOARCH,如果是win 系统的话,默认应该是windows和amd64,为了编译出wasm文件,需要修改如下:

export GOOS=js

export GOARCH=wasm

否则编译的时候会提示奇怪的信息(不是提示环境问题),如果还是不对,可以设置CGO:

export CGO_ENABLED=0

当然我设置的1是没问题的。

最后编译生成wasm文件:

go build -o lib.wasm main.go

-o 是编译参数,指定输出的文件。

在Go里面要引入:syscall/js

通过js.Global().Get()获取js对象,既可以获取函数、也可以获取DOM元素。类型是js.Value。

如:

js.Global().Get("alert")
js.Global().Get("document")

如果是设置元素的属性调用Set(),如果是呼叫(执行)方法,调用Call("函数名","参数")。

如:

js.Global().Get("document").Call("getElementById", id)
 
前面的代码演示了调用alert()、Input的读写、Canvas对象的操作。

二、编写HTML

 1 <html>
 2     <head>
 3         <meta charset="utf-8">
 4         <script src="wasm_exec.js"></script>
 5         <script>
 6             const go = new Go();
 7             WebAssembly.instantiateStreaming(fetch("lib.wasm"), go.importObject).then((result) => {
 8                 go.run(result.instance);
 9             });
10         </script>
11     </head>
12     <body>
13         <canvas id=\'canvas\'></canvas></br>
14         <input id="num1" type="number" />
15         +
16         <input id="num2" type="number" />
17         =
18         <input id="rlt" type="number" readonly="readonly" />
19         <button id="compute">compute</button>
20     </body>
21 </html>
index.html

HTML文件主要是定义界面元素,引入wasm_exec.js文件,调用刚才build的lib.wasm。

三、编写一个HTTP服务

Go 内置的 HTTP 服务器支持Content-Type 为 application/wasm。

 1 package main
 2 
 3 import (
 4     "flag"
 5     "log"
 6     "net/http"
 7 )
 8 
 9 var (
10     listen = flag.String("listen", ":8087", "listen address")
11     dir    = flag.String("dir", ".", "files directory to serve")
12 )
13 
14 func main() {
15     flag.Parse()
16     log.Printf("listening on %q...", *listen)
17     err := http.ListenAndServe(*listen, http.FileServer(http.Dir(*dir)))
18     log.Fatalln(err)
19 }
HTTP服务代码

这里要注意:之前为了编译wasm文件,修改了GOOS和GOARCH,现在为了运行http服务,我们必须恢复。

为了方便调试,我们可以在vscode里面新建一个终端,执行:

export GOOS=windows

export GOARCH=amd64

然后执行:

go run server.go

如果有防火墙提示网络访问,选择允许,然后会看到终端提示:

2020/03/10 09:27:12 listening on ":8087"...

这表示我们的HTTP服务启动好了。

四、测试效果

在浏览器里面输入:http://127.0.0.1:8087/

可以看到页面弹出了对话框:

 

 

然后出现了我们绘制的内容:

 

 

在浏览器调试器里面看到输出内容:

 

 页面上还有一个计算的功能,我们输入数字,点击按钮,发现没有反应,看调试器可以看见错误:

 

信息提示很明确,回头看我们的Go代码,main()函数在执行了registerCallbackFunc()就结束退出了,

这个时候再去调用肯定是失败的,所以我们要让程序不能退出:

 1 func main() {
 2     fmt.Println("Hello, Go WebAssembly!")
 3     draw()
 4     // 通过js.Global().Get()拿到全局alert函数的引用
 5     alert := js.Global().Get("alert")
 6     // 调用alert.Invoke来调用alert函数
 7     alert.Invoke("hello world")
 8     done := make(chan struct{}, 0) // 创建无缓冲通道
 9 
10     registerCallbackFunc()
11     <-done    // 阻塞
12 }

在第8行创建一个通道,然后在11行从通道读取内容,因为通道没有内容,所以会阻塞。

然后重新编译wasm文件,刷新网页,可以看到预期达到了:

 

 这就是用Go开发Wasm的基本套路了。