skip to content
pureink

WASM初探

/ 6 min read

目录

前言

最近开发上线了xhair.pro, 其核心的数据是通过爬虫下载.dem文件并使用一些开源的解析库如demoinfocs-golang,demoparser解析获取最终的数据

bilibili

在本地下载解析上传可以随意配置环境,但浏览器无法直接运行Golang或Rust的代码。想要将解析的能力制作成web应用,需要使用WebAssembly技术。

WASM

WebAssembly 是一种新的编码方式,可以在现代的 Web 浏览器中运行——它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如 C/C++、C# 和 Rust 等语言提供编译目标,以便它们可以在 Web 上运行。它也被设计为可以与 JavaScript 共存,允许两者一起工作。

我们可以把Rust、Go等语言编译成.wasm后缀的二进制文件,浏览器可以通过WebAssembly的API加载该模块,并调用相关能力

实践

不同编程语言需要通过不同的方式编译至wasm,我尝试了Go与Rust。

Golang实践

  1. 首先需要配置Go的环境 go 安装

  2. 初始化项目

Terminal window
mkdir wasm-example
cd wasm-example
go mod init wasm-example
  1. 编写代码

创建main.go文件,与普通go代码区别在于需要引用”syscall/js”包,示例代码如下

package main
import (
"fmt"
"syscall/js"
dem "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs"
events "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/events"
)
func main() {
c := make(chan struct{}, 0)
dem.DefaultParserConfig = dem.ParserConfig{
MsgQueueBufferSize: msgQueueBufferSize,
}
// 注册后,将下面的parse函数全局注册,浏览器可以调用parseDemo函数方法
js.Global().Set("parseDemo", js.FuncOf(parse))
fmt.Println("WASM Go Initialized")
<-c
}
func parse(){
// 调用demoinfocs包解析数据
......
}
  1. 编译至wasm

需要设置环境变量GOOS=js GOARCH=wasm

Terminal window
GOOS=js GOARCH=wasm go build -o main.wasm main.go
  1. 前端调用
  • 浏览器需先加载wasm_exec.js

所有编译至wasm的Go应用通用,代码可见 wasm_exec.js

  • 加载wasm模块
// wasm_exec.js内声明的Go类,在此实例化
const go = new Go();
// 调用WebAssembly Api加载wasm模块
WebAssembly.instantiateStreaming(
fetch("http://example.com/main.wasm"),
go.importObject,
)
.then((result) => {
console.log("WASM loaded");
go.run(result.instance);
})
.catch((err) => {
console.error("WASM load failed:", err);
});
// parseDemo函数已在全局注册,可以直接调用window.parseDemo()
Terminal window
wasm-example/
├── go.mod (约为package.json)
├── main.go (约为index.js)
├── main.wasm (编译产物,浏览器下载导入该模块)
├── wasm_exec.js (js和Go代码的桥梁,需加载执行后才可导入wasm模块)
├── index.html (前端代码)

Rust实践

类似Go,借助wasm_bindgenwasm-pack

简要描述下~

  1. 安装Rust环境
  2. 使用cargo(类似npm)创建项目并安装依赖
  3. 编写rust代码

导入wasm_bindgen并在需要的函数上添加#[wasm_bindgen]

use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn hello() -> String {
"Hello, WebAssembly!".to_string()
}
  1. 编译产物
Terminal window
wasm-pack build --target web --release
  1. 前端使用

Go使用的wasm_exec.js代码是固定的,需要手动处理一些绑定,而Rust则是每次生成,因此使用更便捷一些

<script type="module">
import init, { hello } from './pkg/wasm_example.js';
async function run() {
await init();
hello();
}
run();
</script>

效果

我开发的工具部署在 https://www.xhair.pro/zh-CN/tools/demo xhair.pro

优化

上线后我注意到在调用parse函数解析.dem文件时Loading动画不正常同时鼠标无法与任何内容交互

可以将解析动作放入web worker中执行

以下示例worker.js,加载了wasm模块,并执行wasm函数并与主线程通信

self.onmessage = async (e) => {
if (e.data.type === "loadWasm") {
try {
await import("/demoparser.js");
await self.wasm_bindgen("/demoparser.wasm");
self.postMessage({ type: "wasmLoaded", success: true });
} catch (error) {
self.postMessage({ type: "wasmError", error: error.message });
}
} else if (e.data.type === "callFunction") {
try {
const { functionName, args = [] } = e.data.data;
const result = self.wasm_bindgen[functionName](...args);
self.postMessage({ type: "functionResult", functionName, result });
} catch (error) {
self.postMessage({
type: "functionError",
functionName: e.data.functionName,
error: error.message,
});
}
}
};

需要一个Context Provider用于初始化worker并传递到子组件

export function WasmWorkerProvider({ children }: { children: ReactNode }) {
const workerRef = useRef<Worker>(null);
useEffect(() => {
workerRef.current = new Worker("/wasmworker.js");
workerRef.current.postMessage({ type: "loadWasm" });
return () => workerRef.current?.terminate();
}, []);
return (
<WasmWorkerContext.Provider
value={{
callFunction: (data) =>
new Promise((resolve) => {
workerRef.current.postMessage({ type: "callFunction", data });
workerRef.current.onmessage = (e) => resolve(e.data.result);
}),
}}
>
{children}
</WasmWorkerContext.Provider>
);
}

使用wasm函数,例如

// context获取
const { callFunction } = useWasmWorker();
const header = await callFunction({
functionName: "parseHeader",
args: [data],
});

需要注意的是,在worker环境中只有self没有window,所以可能需要修改部分已有代码

// rust wasm_bindgen自动生成的
window.wasm_bindgen = Object.assign(__wbg_init, { initSync }, __exports);
// 修改为
self.wasm_bindgen = Object.assign(__wbg_init, { initSync }, __exports);

总结

wasm可以在浏览器运行Go、Rust等代码,虽然有些性能损耗,但对于我的应用场景,从一个几百M的文件中解析数据,依然还是会比上传文件至服务端计算快得多。

在Next.js中使用worker和wasm以及Golang和Rust编译至wasm运行都没找到什么优质的文档,也许是对于大多数场景直接请求API让服务端执行代码更常见吧。