本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2021年11月21日 统计字数: 14796字 阅读时间: 30分钟阅读 本文链接: https://soulteary.com/2021/11/21/use-docker-and-golang-to-quickly-get-started-with-webassembly.html ----- # 使用 Docker 和 Golang 快速上手 WebAssembly 本文将聊聊,如何使用 Docker 和 Golang 快速上手 WebAssembly。我会分别从浏览器场景和“通用应用”场景来进行叙述,如果你还徘徊在 WebAssembly 的门前,或许这篇文章会对你所有帮助。 ## 写在前面 如果从 2017 年浏览器纷纷开始以实验性的方式,支持 Web WebAssembly 功能来看,在浏览器使用非 JavaScript 来完成计算的风已经吹了五年了。不过,感受到 Wasm 生态真正发力的是近三年。 大环境的变化,让行业生态中音视频、云计算、物联网有了更广阔的市场,以及在降本提效上更高的追求,此为天时。如果说 Wasm 生态中的 C 位是 Mozilla,那么去年在 Mozilla 裁员事件出现后,他们迅速成立 Rust 的基金会,以保障 Rust 开发团队能够独立、稳定地运行,保护 Rust 以及周边项目的持续发展,为生态提供土壤,此可谓地利。 天时地利,只待人和。 国内外经济环境均有了前所未有的变化,在少了不少外部资本诱惑之后,能够感受到这几年来,基础技术设施的蓬勃发展,这里面少不了各种优秀的工程师正在将注意力从“业务”,逐步转移到“技术”上。目前 Wasm 王国在它的一等公民 Rust 高速发展和推动下,已经吸引了不少其他语言生态、知名商业公司的注意力。至于何时爆发,我个人认为,只是时间问题。 不过需要注意的是,**没有技术会是银弹,只有把技术放在适用的场景下才能达到事半功倍的效果**。那么哪些场景适合 `WebAssembly` 呢? 为了行文方便,接下来 `WebAssembly` 会简称为 `Wasm`。 ## 适用场景 & 优势 先来看看,近三年业界公开表明已使用它的场景: - **做在线设计工具的业务场景**,比如 Figma:早在 2017 年,Figma 就借助这项技术进行了产品优化,[《WebAssembly cut Figma's load time by 3x》](https://www.figma.com/blog/webassembly-cut-figmas-load-time-by-3x/),他们的工程师 Rasmus Andersson 也从比较底层的角度分析了 [wasm](https://rsms.me/wasm-intro)。 - **复杂的在线 IDE 产品**,比如 `vim.wasm`:在 2019 开始正式进行开发的 基于 wasm 的 VIM 完整移植版。使用 [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) 和 [SharedArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) 解决了在浏览器端 JS 和 wasm 进行数据交互时的延迟问题。 - **云计算场景的边缘计算**,比如 Cloudflare、fastly :[《WebAssembly on Cloudflare Workers》](https://blog.cloudflare.com/webassembly-on-cloudflare-workers/)、[《Compute@Edge》](https://docs.fastly.com/products/compute-at-edge) - **云计算场景网关能力扩展**,比如 Envoy & istio、蚂蚁金服、MegaEase、ApiSix:[《Istio1.5 & Envoy 数据面 WASM 实践》](https://cloudnative.to/blog/202004-istio-envoy-wasm/)、[《WebAssembly 在 MOSN 中的实践 - 基础框架篇》](https://mosn.io/blog/posts/mosn-wasm-framework/) 、[《用 Easegress + WebAssembly 做秒杀》](https://megaease.com/zh/blog/2021/09/08/how-to-do-an-online-flash-sale-event-with-easegress-and-webassembly/)、[《云原生网关 APISIX 核心流程源码分析与进化方向思考》](https://cloudnative.to/blog/apisix-source-code-reading/) - **在线音视频处理**,比如 Zoom、声网 agora、字节跳动:[《Zoom on Web: WebAssembly SIMD, WebTransport, and WebCodecs》](https://www.infoq.com/news/2020/08/zoom-web-chrome-apis/)、[《How WebRTC & WASM are opening new opportunities for web apps》](https://rtcweb.in/how-webrtc-and-wasm-are-opening-new-opportunities-for-web-apps/)、[《如何通过 WebAssembly 在 Web 进行实时视频人像分割》](https://www.agora.io/cn/community/blog-121-category-21976)、[《Bilibili - WebAssembly在多媒体场景的实践与思考》](https://www.w3.org/2020/08/wasm-media.pdf) - **高性能的复杂在线数据可视化**,比如 perspective:[https://github.com/finos/perspective](https://github.com/finos/perspective) - **浏览器端的前端加密场景**,比如coupang :[《WebAssembly 在性能及加密场景的深度探索》](https://www.infoq.cn/article/vpugdyvudfuvdepfyu1s) 如果将上面的场景进行归纳,我们可以看到,在浏览器端、云计算、嵌入式方向,`WebAssembly` 的优势还是比较大的: - 用比较低的代码量来扩展现有业务能力(云端、浏览器端) - 充分使用客户端的计算能力,节约云服务器带宽和计算资源成本(浏览器端) - 利用 wasm 高性能计算方面带来的优势,解决复杂的计算的执行效率问题(可视化、通用计算场景) - 快速复用其他语言技术栈道能力,借助容器化的思路快速迭代产品(云端、浏览器端、嵌入式) - 使用更流行、易于开发维护,或者贴合自己团队的语言来进行产品迭代(嵌入式) - 前端敏感内容的加密处理(浏览器端) ## 简单起步:浏览器中的 WebAssembly 循序渐进,我们先从最简单的场景开始:浏览器。 ### 环境准备 如果你不想折腾 golang 的本地开发环境,我们可以使用 Docker 来快速创建一个运行环境: ```bash docker run --rm -it -v `pwd`/code:/app -p 8012:8012 golang:1.17.3-buster bash ``` 这里,我们将本地的 `code` 目录,映射到容器内的 `/app` 目录中,并将本地和容器中的 8012 端口打通,以备后续使用。 接着,在命令执行完毕后的容器的终端控制台中进行项目的初始化: ```bash cd /app go mod init soulteary.com/wasm-demo/v2 ``` 然后,使用你喜欢的方式(在容器内或者在本地 IDE中),创建一个 golang 的程序文件,比如 `main.go`: ```go package main import "fmt" func main() { fmt.Println("一切都将从这里开始") } ``` 完成之后,在容器控制台内执行 `go run main.go`,不出意外,将看到 “一切都将从这里开始”的文本输出结果。 因为我们要演示的场景包含前端,所以还需要有一个简单的 Web 服务器,继续使用 golang 写一个简单的 Web 服务器吧。 ```go package main import ( "log" "net/http" ) func main() { log.Fatal(http.ListenAndServe(":8012", http.FileServer(http.Dir(".")))) } ``` 将上面的内存保存为 `server.go`,当我们执行它的时候,它会将本地作为服务器根目录,对访问者提供 Web 服务。 在这个场景下,工程师们一般会有几个问题: - 如何得到一个 Wasm 程序 - 如何将这个程序放在浏览器中运行; - 如何让浏览器中的 JavaScript 能够调用 WASM 的导出函数。( Golang 程序中的函数) - 以及如何针对整个程序进行进一步性能优化 在 “Show You The Code” 的过程中,我们将依次解答上面的问题。 ### 从 Golang 创建 WebAssembly 程序 将 Golang 程序“变成” WebAssembly 一般会采取两种方案: - 使用 Golang 原生编译器进行编译。 - 使用 TinyGo 编译器进行编译。 Golang “原生编译器方案”适用性非常好,适合项目初期开发、或者不太介意编译产物尺寸、程序首次分发时间的 B 端产品使用,如果你愿意投入时间做产物体积裁剪,也能够获得不错的结果。构建命令一般会类似 `GOOS=js GOARCH=wasm go build -o YOUR_MODULE_NAME.wasm .`,构建产物需要配合 Golang `wasm_exec.js` 使用。 相比较前者,TinyGo 的编译结果更小巧,可以用于嵌入式场景(官方目前支持60多种单片机)、支持 WASI 接口的云计算场景,以及本文本小节提到的 Web 场景。经过 GZip 压缩后,你的程序甚至不如一张图片大。构建命令和原生类似 `tinygo build --no-debug -o YOUR_MODULE_NAME.wasm -target wasi .`,不同的是,除了支持构建结果为 `wasm` 之外,支持沟通通用的 `wasi` ,方便你进行多端功能复用。(这个能力在分发模式上类似 Docker、在应用角度来看,则有些类似 Node 刚出现时,我在淘宝团队实践的前后端代码复用。) 我们先以原生方式为例,基于“环境准备”小节中的内容,使用下面的命令就能够完成对 wasm 的编译啦: ```bash GOOS=js GOARCH=wasm go build -o module.wasm main.go ``` 接着,将 Golang 提供的 “JS Bridge” 复制到项目根目录。 ```bash cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" . ``` 然后,编写一个落地页,让它能够加载上面的 JS Bridge,自动下载我们编译好的 wasm 程序,在程序下载完成后自动执行: ```html
Standard Output:
${stdout}`) } startWasiTask(wasmFilePath) ``` 文件都准备继续之后,使用 `./node_modules/.bin/parcel index.html --port=8081` 启动服务,在浏览器中访问 `localhost:8081`,你将会看到调用 Wasm 程序输出的内容: ```html Standard Output: _ _ _ _ __ __ _ _ | | | | ___ | | | | ___ \ \ / / ___ _ __ | | __| | | |_| | / _ \ | | | | / _ \ \ \ /\ / / / _ \ | '__| | | / _` | | _ | | __/ | | | | | (_) | \ V V / | (_) | | | | | | (_| | |_| |_| \___| |_| |_| \___/ \_/\_/ \___/ |_| |_| \__,_| ``` ![浏览器中 Go Wasm 的程序输出](https://attachment.soulteary.com/2021/11/21/wasm-by-go.jpg) ### 在 Go 程序中运行 WASI 标准的 WebAssembly 想在 Golang 中运行由 Golang 编写的具备 WASI 标准接口的 Wasm,其实还是有一点挑战的。一般情况下,你可能会遇到下面这些问题: - 首先,你暂时不能通过 Golang 的编译器构建一个标准的支持 WASI 标准的 Wasm 程序。 - 其次,如果你使用比较流行的 wasmer-go 或者其他的运行时尝试执行标准的 WASI 程序,可能会遇到一些因为兼容性问题导致的报错,比如:``Missing import: \`wasi_snapshot_preview1\`.\`fd_write\```。 - 最后,如果你侥幸修复了这些问题,你会发现在没有 WAT 文本格式文件的前提下,你还需要手动补全 Wasm 程序的导出函数,才能够正常使用程序。 关于上面的这些问题,[在 wasmer-go 维护者的回答中](https://github.com/wasmerio/wasmer-go/issues/275)曾提到,关于生成 WASI 标准的程序的方式,wasmer-go 项目的维护者们也不止一次的建议我们使用 TinyGo 替代默认的 Golang 编译器。如果你想使用 wasmer-go 来完成这件事,会遇到[一些问题](https://github.com/wasmerio/wasmer-go/issues/130),维护者目前并不考虑朝着这个方向完善,并推荐我们使用 [https://github.com/go-wasm-adapter/go-wasm](https://github.com/go-wasm-adapter/go-wasm) 这个项目,来将上文中在浏览器中起到桥接作用的 JS Bridge 代码,在 Go 的代码中“运行一次”,将运行环境“垫平”。或考虑使用 [https://github.com/mattn/gowasmer](https://github.com/mattn/gowasmer) 的项目,针对 TinyGo 的产物进行“垫平”操作。 回想起文章一开始提到的,各种云服务网关都陆陆续续开始支持 WASM 的方式来扩展能力,而我们之前熟悉的 Traefik 却采用了类似 Nginx 的方案,则使用了另外一种更笨重的方案,官方团队提供[基于 Golang 的 SDK](https://github.com/traefik/plugindemo),然后使用基于[约定的方式](https://github.com/traefik/traefik/tree/master/pkg/plugins)动态从本地或远程加载这些同构的应用。采取这个技术路线的原因里,或许有一大部分正是出于上面的种种现实问题。 **不过,2021 即将结束,这个问题还会是问题吗?** 其实,早在今年年中的时候,wasmer-go 就可以通过 WASI 的方式来运行 Wasm 了,不过官方的项目缺少一个可以使用的示例。在经过一些尝试之后,我解决了这个问题,下面跟着我一起来玩吧。 先创建项目目录,进行一些初始化操作: ```bash mkdir /app/go-app cd /app/go-app/ go mod init soulteary.com/go-app/v2 cp /app/wasm/module.wasm . ``` 接着,安装最新版本的 wasmer-go 项目运行时: ```go go get github.com/mattn/gowasmer ``` 然后,编写一个简单的 Golang 程序,来加载 Wasm 程序,并执行它: ```go package main import ( "fmt" "io/ioutil" wasmer "github.com/wasmerio/wasmer-go/wasmer" ) func main() { wasmBytes, _ := ioutil.ReadFile("module.wasm") store := wasmer.NewStore(wasmer.NewEngine()) module, _ := wasmer.NewModule(store, wasmBytes) wasiEnv, _ := wasmer.NewWasiStateBuilder("wasi-program"). // Choose according to your actual situation // Argument("--foo"). // Environment("ABC", "DEF"). // MapDirectory("./", "."). Finalize() importObject, err := wasiEnv.GenerateImportObject(store, module) check(err) instance, err := wasmer.NewInstance(module, importObject) check(err) start, err := instance.Exports.GetWasiStartFunction() check(err) start() HelloWorld, err := instance.Exports.GetFunction("HelloWorld") check(err) result, _ := HelloWorld() fmt.Println(result) } func check(e error) { if e != nil { panic(e) } } ``` 将上面的内容保存为 `main.go`,然后执行 `go run main.go`。不出意外,你将会看到类似下面的结果: ```html _ _ _ _ __ __ _ _ | | | | ___ | | | | ___ \ \ / / ___ _ __ | | __| | | |_| | / _ \ | | | | / _ \ \ \ /\ / / / _ \ | '__| | | / _` | | _ | | __/ | | | | | (_) | \ V V / | (_) | | | | | | (_| | |_| |_| \___| |_| |_| \___/ \_/\_/ \___/ |_| |_| \__,_|