下面通过示例说明 WebAssembly 的应用场景:
下面是官网(https://webassembly.org/)的定义:
WebAssembly(缩写为 Wasm)是一种用于基于栈的虚拟机的二进制指令格式(bytecode)。Wasm 被设计为编程语言的可移植编译目标,支持在 Web 上部署客户端和服务端应用程序。
WebAssembly 旨在维护 Web 的无版本、经过功能测试和向后兼容的本性(Web Embedding)。WebAssembly 模块能够调用 JavaScript 上下文,并且通过能够从 JavaScript 访问的相同 Web API 访问浏览器功能。WebAssembly 还支持非 Web 嵌入(Non-Web Embedding)。
WebAssembly 分为两个版本:
比较流行的 WASI Runtime 包括 Wasmtime(https://docs.wasmtime.dev/)、Wasmer、Wazero、WasmEdge 等。后面将使用 Wasmtime。
Go 标准库 syscall/js
只支持 WebAssembly 1.0。TinyGo(https://tinygo.org/)既支持编译在浏览器中使用的程序(WASM),也支持编译在服务端及其它边缘设备上使用的程序(WASI)。
TinyGo 是 Go 语言的新编译器。TinyGo 聚焦于编译 Go 编写的代码,但适用于较小类型的系统:
比如,下面这段代码在两个编译器中做完全相同的事情:
package main
import "fmt"
func main() {
fmt.Println("Hello world!")
}
可以使用相似的命令编译这段代码:
go build -o hello ./hello.go
或
tinygo build -o hello ./hello.go
编程语言是相同的。标准库(包括 fmt 包)是相同的。唯一的不同是使用的编译器及相关的运行时。
显著不同的是输出二进制大小。以下是每个编译器生成的可执行文件:
1937340 may 12 12:42 helloworld-with-fmt.go1.16.3
251376 may 12 12:42 helloworld-with-fmt.tinygo0.18
在每个可执行文件上执行 strip
命令移除符号和调试信息后:
837624 may 12 20:03 helloworld-with-fmt.go1.16.3-stripped
10392 may 12 20:05 helloworld-with-fmt.tinygo0.18-stripped
在这种情况下,Go 编译的二进制文件大小为 837k(strip 前为 1.9MB),而 TinyGo 生成的二进制文件大小仅为 10k(strip 前为 251k)!
这是原始二进制文件大小的 1%,这使得这样的二进制文件可以在以前因二进制文件大小而不支持的更小的系统上使用。
通过使用 TinyGo,可以在各种裸机硬件平台上编译和运行二进制文件。例如,可以直接在 BBC micro:bit 上运行该程序:
tinygo flash -target=microbit ./hello.go
如何在浏览器中从 JavaScript 调用 WebAssembly。
可以从 Go 中调用 JavaScript 函数,以及从 WebAssembly 中调用 Go 函数。
package main
// This calls a JS function from Go.
func main() {
println("adding two numbers:", add(2, 3)) // expecting 5
}
// This function is imported from JavaScript, as it doesn't define a body.
// You should define a function named 'add' in the WebAssembly 'env'
// module from JavaScript.
//
//export add
func add(x, y int) int
// This function is exported to JavaScript, so can be called using
// exports.multiply() in JavaScript.
//
//export multiply
func multiply(x, y int) int {
return x * y;
}
相关的 JavaScript 类似:
// Providing the environment object, used in WebAssembly.instantiateStreaming.
// This part goes after "const go = new Go();" declaration.
go.importObject.env = {
'add': function(x, y) {
return x + y
}
// ... other functions
}
// Calling the multiply function:
console.log('multiplied two numbers:', wasm.exports.multiply(5, 3));
也可以简单地在 func main()
中执行代码,就像在 WebAssembly 的标准库实现中一样。
如果已安装 tinygo
,那么只需提供正确的目标:
GOOS=js GOARCH=wasm tinygo build -o wasm.wasm ./main.go
查看 wasm examples,获取更多完整示例。
内容的执行需要一些从 WebAssembly 中调用的 JS 辅助函数。tinygo
在 tinygo/targets/wasm_exec.js
中定义这些辅助函数。它基于标准库中的 $GOROOT/misc/wasm/wasm_exec.js
,但略有不同。确保 tinygo
的版本与 wasm_exec.js
的版本相同。
在浏览器中运行 WebAssembly 文件所需的步骤包括使用 WebAssembly.instantiateStreaming
或 WebAssembly.instantiate
将其加载到 JavaScript 中:
const go = new Go(); // Defined in wasm_exec.js
const WASM_URL = 'wasm.wasm';
var wasm;
if ('instantiateStreaming' in WebAssembly) {
WebAssembly.instantiateStreaming(fetch(WASM_URL), go.importObject).then(function (obj) {
wasm = obj.instance;
go.run(wasm);
})
} else {
fetch(WASM_URL).then(resp =>
resp.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, go.importObject).then(function (obj) {
wasm = obj.instance;
go.run(wasm);
})
)
}
如果使用显式导出,那么可以在 wasm.exports
名称空间下调用它们。请参阅示例中的 export
目录,以获取此示例。
除 JavaScript 之外,需要将 wasm 文件的 Content-Type
头设置为 application/wasm
。否则,大多数浏览器都不会运行它。
package main
import (
"log"
"net/http"
"strings"
)
const dir = "./html"
func main() {
fs := http.FileServer(http.Dir(dir))
log.Print("Serving " + dir + " on http://localhost:8080")
http.ListenAndServe(":8080", http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
resp.Header().Add("Cache-Control", "no-cache")
if strings.HasSuffix(req.URL.Path, ".wasm") {
resp.Header().Set("content-type", "application/wasm")
}
fs.ServeHTTP(resp, req)
}))
}
这个简单的服务器在端口 8080
上为 ./html
目录提供服务,并且适当地设置任何 *.wasm
文件的 Content-Type
头。
TinyGo 如何使用 WebAssembly System Interface(WASI)。
TinyGo 可以编译在服务端及其它边缘设备上使用的程序(WASI)。
TinyGo 程序可以运行在 Extism、Fastly Compute@Edge、Fermyon Spin、tau、wazero 及许多其它 WebAssembly 运行时中。
TinyGo 当前同时支持 WASI Preview 1(wasip1)及 WASI Preview 2(wasip2)。
下面是一个在 WASI 主机应用程序中使用的 TinyGo 程序:
package main
//export add
func add(x, y uint32) uint32 {
return x + y
}
// main is required for the `wasip1` target, even if it isn't used.
func main() {}
编译上述 TinyGo 程序,使其可以在任何 WASI 运行时上使用:
GOOS=wasip1 GOARCH=wasm tinygo build -o main.wasm main.go
py2wasm
将 Python 代码编译为 WebAssembly说明:
py2wasm
版本是 2.6.3Python 版本是 3.11
暂未找到导出特定函数的方法
源代码 test.py 的内容如下:
def multiply(a: int, b: int) -> int:
return a * b
def main() -> None:
print(multiply(3, 4))
if __name__ == "__main__":
main()
执行如下命令将 Python 代码编译为 WebAssembly:
py2wasm -o test.wasm test.py
执行如下命令运行上一步生成 .wasm
文件:
wasmtime test.wasm
.wasm
文件源代码 main.go 的内容如下:
package main
import (
"errors"
"flag"
"github.com/bytecodealliance/wasmtime-go/v24"
"log"
"os"
)
func main() {
flag.Parse()
wasmFilename := flag.Arg(0)
if wasmFilename == "" {
wasmFilename = "main.wasm"
}
// 读取 Wasm 文件
wasm, err := os.ReadFile(wasmFilename)
if err != nil {
log.Fatal(err)
}
// 创建新 Engine
engine := wasmtime.NewEngine()
// 使用 Engine 中的配置,从提供的 WASM 编译新 Module
module, err := wasmtime.NewModule(engine, wasm)
if err != nil {
log.Fatal(err)
}
// Linker 实现 wasmtime Linking 模块,可以将实例化的模块链接在一起
linker := wasmtime.NewLinker(engine)
// DefineWasi 将 WASI 模块链接到该链接器中,确保所有导出函数都可用
err = linker.DefineWasi()
if err != nil {
log.Fatal(err)
}
// 配置 WASI,然后使用该 wasi 配置创建 Store
wasiConfig := wasmtime.NewWasiConfig()
wasiConfig.InheritEnv()
wasiConfig.InheritArgv()
wasiConfig.InheritStdout()
wasiConfig.InheritStderr()
wasiConfig.InheritStdin()
store := wasmtime.NewStore(engine)
store.SetWasi(wasiConfig)
// 使用该链接器中定义的所有导入实例化模块
instance, err := linker.Instantiate(store, module)
if err != nil {
log.Fatal(err)
}
// 运行启动函数
start := instance.GetFunc(store, "_start")
// 使用提供的参数调用函数
_, err = start.Call(store)
if err != nil {
var wError *wasmtime.Error
ok := errors.As(err, &wError)
if !ok {
log.Fatal(err)
}
exitCode, isDefined := wError.ExitStatus()
if !isDefined || exitCode != 0 {
log.Panic(err)
}
}
}
执行如下命令在 Go 中运行 .wasm
文件:
go run main.go test.wasm
源代码 add.go 的内容如下:
package main
//export add
func add(x, y uint32) uint32 {
return x + y
}
// main is required for the `wasip1` target, even if it isn't used.
func main() {
println("Hello, I'm TinyGo~")
}
执行如下命令将上述源代码编译为 .wasm
文件:
GOOS=wasip1 GOARCH=wasm tinygo build -o add.wasm add.go
使用上一节的 Go 程序运行 add.wasm
:
go run main.go add.wasm
也使用 wasmtime
运行该文件:
wasmtime add.wasm
wasmtime --invoke add add.wasm 2 3
修改上一节的 Go 程序调用 add.wasm
中导出的 add
函数,在 main
函数的尾部添加:
addFunc := instance.GetFunc(store, "add")
if addFunc == nil {
log.Println("no exported function named add")
return
}
result, err := addFunc.Call(store, 3, 4)
if err != nil {
log.Fatal(err)
}
log.Println("3 + 4 =", result)
执行如下命令运行 add.wasm
:
go run main.go add.wasm