各编程语言与WebAssembly交互实践
WebAssembly(简称为Wasm)是一种可移植、体积小、加载快并且运行高效的二进制格式,它成为了在现代Web浏览器中执行高性能代码的标准。
除了与浏览器的交互,WebAssembly还具备与其它编程语言交互的能力,如Node.js、Deno、Go、Java等。 网上教程更多的是如何将Rust、Go、Java编译成WebAssembly,继而用JavaScript与之进行交互。
本文则反其道而行之,将介绍如何使用Rust编译成WebAssembly,并与其它语言的运行时环境进行无缝交互,为开发者展示如何充分利用WebAssembly的跨语言特性,实现更加灵活和高效的应用开发。这在某些特定的开发场景下,比如开发多语言的SDK时,是一种可以考虑的新型方案。
Rust代码
Rust语言是一种系统级编程语言,具有比其它语言更高的性能和更好的安全性。 与C 和 C++ 相比,它还提供了内存安全性和数据竞争安全性的保证。这使得Rust成为编写高质量,高性能应用程序的好选择,而这些特性也使它成为编写WebAssembly应用程序的好工具。
编写WebAssembly代码的最简单方式之一是使用Rust编写代码,然后使用Rust编译器将其编译为WebAssembly字节码。 由于Rust本身具有优秀的编译和优化工具,因此它可以生成高效的 WebAssembly 代码,提高 WebAssembly 的性能水平。 同时,Rust还提供了用于管理存储器安全的工具, 这些工具可以帮助开发人员避免常见的内存错误。
以下是一个简单的Rust函数示例,将两个数字相加:
#[no_mangle]
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[no_mangle]
表示这个方法要被暴露出来。
以下是Cargo.toml的内容:
[package]
name = "rust_wasm"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
原生构建
在本文中,我们将介绍如何使用Rust编译WebAssembly模块,并在不同的运行时环境中进行交互。我们将以原生Rust编译为WebAssembly为例,然后逐步介绍如何在Node.js、Deno、Go和Java中与WebAssembly模块进行交互。
cargo build --target=wasm32-unknown-unknown --release
上述命令将会生成一个名为rust_wasm.wasm
的WebAssembly模块文件。
Node.js运行
测试Node.js版本为v16.16.0。
async function main() {
const importObj = {};
const data = require("fs").readFileSync("./rust_wasm.wasm");
const { instance } = await WebAssembly.instantiate(data, importObj);
const res = instance.exports.add(40, 2) // returns 42
console.log("res: ", res); // 42
}
main();
这是在Node.js中与WebAssembly交互的简单示例。您可以根据自己的需求,使用更复杂的WebAssembly模块和函数。
Deno运行
除了在浏览器中使用 Rust 编译的 WebAssembly,我们还可以在 Deno 中利用它。Deno 是一个现代化的运行时环境,类似于 Node.js,但更注重安全性和开发者友好性。由于Deno的底层是使用Rust开发的,所以天然就支持WebAssembly。
const importObj = {};
const data = await Deno.readFile("./rust_wasm.wasm");
const { instance } = await WebAssembly.instantiate(data, importObj);
const res = instance.exports.add(40, 2) // returns 42
console.log("res: ", res); // 42
可以看出,Deno与Node.js的示例没有太大区别,只是读取文件的API有变化。
浏览器
WebAssembly是一种低级字节码格式,可以在现代Web浏览器中运行,因此可以作为一种高效的替代品来使用JavaScript。 与JavaScript相比,WebAssembly可以提供更快的性能和更小的文件大小,使其成为编写高性能Web应用程序的有力工具。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Page Title</title>
<script type="module">
const importObj = {};
const response = await fetch("./rust_wasm.wasm");
const { instance } =
await WebAssembly.instantiateStreaming(response, importObj);
const res = instance.exports.add(40, 2) // returns 42
console.log("res: ", res);
</script>
</head>
<body>
</body>
</html>
网络截图:
可以看到,这个wasm文件是比较大的。如果要缩减体积,需要有额外的优化处理。
Go支持
Go 是一种强大的编程语言,它除了可以编译成WebAssembly,供浏览器使用以外,也可以与 Rust 编译的 WebAssembly 进行交互。通过在 Go 中使用 Rust 编译的 WebAssembly,我们可以利用 Rust 的性能和低级别的特性,为 Go 应用程序添加更多功能。
首先,确保您已经安装了 Go,并配置了正确的环境。本文测试的Go版本:
go version go1.19.3 linux/amd64
要在 Go 中使用 Rust 编译的 WebAssembly,我们需要使用 Go 的 WebAssembly 支持和相应的库。这里我们推荐一个库:github.com/wasmerio/wa…。
Wasmer的目标是成为一个通用的WebAssembly运行时,可在各种环境中使用,包括嵌入式设备、云计算、边缘计算等。它提供了多种编程语言的绑定,包括Rust、Python、JavaScript和Go等,使开发人员能够使用自己喜欢的语言来编写和执行WebAssembly模块。
新建一个go.mod文件:
module wasm
go 1.19
main.go:
package main
import (
"fmt"
"io/ioutil"
wasmer "github.com/wasmerio/wasmer-go/wasmer"
)
func main() {
wasmBytes, _ := ioutil.ReadFile("./rust_wasm.wasm")
engine := wasmer.NewEngine()
store := wasmer.NewStore(engine)
// Compiles the module
module, _ := wasmer.NewModule(store, wasmBytes)
// Instantiates the module
importObject := wasmer.NewImportObject()
instance, _ := wasmer.NewInstance(module, importObject)
// Gets the `sum` exported function from the WebAssembly instance.
sum, _ := instance.Exports.GetFunction("add")
// Calls that exported function with Go standard values. The WebAssembly
// types are inferred and values are casted automatically.
result, _ := sum(5, 37)
fmt.Println(result) // 42!
}
通过以上代码,我们可以在 Go 中使用 Rust 编译的 WebAssembly 模块,并与之进行交互,为 Go 应用程序添加 Rust 的功能和性能。
Java运行
wasmer也提供了与Java交互的能力,github.com/wasmerio/wa…。在这个仓库里下载jar包
HelloWorld.java:
import org.wasmer.Instance;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
class HelloWorld {
public static void main(String[] args) throws IOException {
// 读取WASM汇编码
byte[] bytes = Files.readAllBytes(Paths.get("./rust_wasm.wasm"));
// 实例化WASM汇编模块
Instance instance = new Instance(bytes);
// 测试WASM例子API
Integer result = (Integer) instance.exports.getFunction("add").apply(5, 37)[0];
assert result == 42;
instance.close();
}
}
// javac -classpath ./wasmer-jni-amd64-linux-0.3.0.jar HelloWorld.java
// java -cp .:./wasmer-jni-amd64-linux-0.3.0.jar HelloWorld
我本地环境运行时报错,要求glibc版本必须大于2.18:
Exception in thread "main" java.lang.UnsatisfiedLinkError: /tmp/wasmer_jni2565959116804832578.lib: /lib64/libc.so.6: version `GLIBC_2.18' not found (required by /tmp/wasmer_jni2565959116804832578.lib)
at java.lang.ClassLoader$NativeLibrary.load(Native Method)
at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1941)
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1824)
at java.lang.Runtime.load0(Runtime.java:809)
at java.lang.System.load(System.java:1086)
at org.wasmer.Native.loadEmbeddedLibrary(Native.java:87)
at org.wasmer.Native.<clinit>(Native.java:21)
at org.wasmer.Instance.<clinit>(Instance.java:16)
at HelloWorld.main(HelloWorld.java:14)
因为升级glibc有风险,我吃过一次亏,就算了,直接在docker中测试。 Dockerfie文件:
# 使用基础的Java镜像
FROM u32lxivt.mirror.aliyuncs.com/library/maven:3-jdk-8
# 将当前目录下的所有文件复制到Docker容器中的/app目录
COPY . /app
# 设置工作目录
WORKDIR /app
# 将wasmer-jni-amd64-linux-0.3.0.jar添加到类路径
ENV CLASSPATH=/app/wasmer-jni-amd64-linux-0.3.0.jar
# 编译Java源代码
RUN javac HelloWorld.java
# 在容器中运行Java应用程序
CMD ["java", "-cp", ".:/app/wasmer-jni-amd64-linux-0.3.0.jar", "HelloWorld"]
# docker build -t my-java-app .
# docker run my-java-app
但是运行时报错:
Exception in thread "main" java.lang.RuntimeException: Failed to instantiate the module: Error while importing "__wbindgen_placeholder__"."__wbindgen_describe": unknown import. Expected Function(FunctionType { params: [I32], results: [] })
at org.wasmer.Instance.nativeInstantiate(Native Method)
at org.wasmer.Instance.<init>(Instance.java:45)
at HelloWorld.main(HelloWorld.java:12)
可能是生成的wasm格式或者哪里不对了。这时并不死心,我试下下面的wasm-pack。
wasm-pack
wasm-pack 是一个 Rust 工具集,用于将 Rust 代码编译成 WebAssembly 模块,并进行打包和发布。它可以让 Rust 开发者在编写 WebAssembly 应用程序时更加方便地进行开发、打包和发布。需要注意的是,wasm-pack 虽然是一个 Rust 工具,但它并不是专门为浏览器编写的 WebAssembly 应用程序而开发的,而是为 WebAssembly 应用程序提供了一个统一的打包、发布和部署方式。
安装wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
在Cargo.toml中添加依赖项
[dependencies]
wasm-bindgen = "0.2"
这时需要修改下代码,引入wasm_bindgen,以及加注解。
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn add(left: usize, right: usize) -> usize {
left + right
}
ESM
使用以下命令构建:
wasm-pack build --target web
可以看到生成文件在pkg目录下:
root@debian:/wk/rust/rust_wasm/pkg# ll
总用量 20
-rw-r--r-- 1 root root 231 5月 24 15:09 package.json
-rw-r--r-- 1 root root 168 5月 24 15:09 rust_wasm_bg.wasm
-rw-r--r-- 1 root root 134 5月 24 14:50 rust_wasm_bg.wasm.d.ts
-rw-r--r-- 1 root root 1024 5月 24 14:50 rust_wasm.d.ts
-rw-r--r-- 1 root root 2573 5月 24 14:50 rust_wasm.js
其中rust_wasm_bg.wasm,就是最终的wasm文件,这168B与上面的1.8M多简直是天差地别。
经测试,在Node.js、Deno、浏览器和Go都没有问题。
重点要测试下Java,居然跑通了,皆大欢喜。
值得一提的是,wasm-pack其实主要产物是给浏览器使用的(大部分情况下确实是),所以上面还有生成的rust_wasm.js,它是可以直接使用的。 我们修改下index.html的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Page Title</title>
<script type="module">
import init, { add } from "./rust_wasm.js";
await init();
const res = add(40, 2) // returns 42
console.log("res: ", res);
</script>
</head>
<body>
</body>
</html>
可以看到,代码清爽了许多,关键是一个init函数,它用来初始化wasm,其实就是加载wasm文件,并初始化。 核心代码就是这段:
async function __wbg_init(input) {
if (wasm !== undefined) return wasm;
if (typeof input === 'undefined') {
input = new URL('rust_wasm_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();
if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
input = fetch(input);
}
__wbg_init_memory(imports);
const { instance, module } = await __wbg_load(await input, imports);
return __wbg_finalize_init(instance, module);
}
async function __wbg_load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
export default __wbg_init;
__wbg_init
就是我们上面的init函数。可以看到它有个参数input,如果不传递,就会找相对路径下的wasm文件。所以实际生产中,我们可以考虑把生成的wasm文件放CDN上,加快下载速度。
看网络请求:
除了Web外,wasm-pack的新版本已经支持Deno、Node.js,分别是--target deno与--target nodejs,就不赘述了。
no-modules
上面生成的ESM的包,可以在框架里使用,或者像我们一个使用type="module"引用。对于某些项目,可能会有需求,不使用import引用,那么可以选择:
wasm-pack build --target no-modules
这次生成的文件与上面的差不多,不过rust_wasm.js
文件大体是这样的:
let wasm_bindgen;
(function() {
const __exports = {};
let script_src;
if (typeof document !== 'undefined' && document.currentScript !== null) {
script_src = new URL(document.currentScript.src, location.href).toString();
}
let wasm = undefined;
/**
* @param {number} left
* @param {number} right
* @returns {number}
*/
__exports.add = function(left, right) {
const ret = wasm.add(left, right);
return ret >>> 0;
};
wasm_bindgen = Object.assign(__wbg_init, { initSync }, __exports);
})();
使用
const { add } = wasm_bindgen;
async function run() {
await wasm_bindgen('./pkg/wasm-pack.wasm');
const result = add(1, 2);
console.log(`1 + 2 = ${result}`);
}
run();
TIPS:升级GLIBC
我的Linux机器是CentOS 7.6:
[root]# lsb_release -a
LSB Version: :core-4.1-amd64:core-4.1-noarch:cxx-4.1-amd64:cxx-4.1-noarch:desktop-4.1-amd64:desktop-4.1-noarch:languages-4.1-amd64:languages-4.1-noarch:printing-4.1-amd64:printing-4.1-noarch
Distributor ID: CentOS
Description: CentOS Linux release 7.6.1810 (Core)
Release: 7.6.1810
Codename: Core
我的默认glibc版本是
[root]# ldd --version
ldd (GNU libc) 2.17
由它升一个小版本其实是可以的:
cd /usr/local
wget http://mirrors.ustc.edu.cn/gnu/libc/glibc-2.18.tar.gz
tar -xzvf glibc-2.18.tar.gz
cd glibc-2.18
mkdir build && cd build/
../configure --prefix=/usr --disable-profile --enable-add-ons --with-headers=/usr/include --with-binutils=/usr/bin
make -j4
make install
# 解决中文乱码问题
make localedata/install-locales
cd ../../
rm -rf glibc-2.18.tar.gz
重要提醒:升级GLIBC需要谨慎操作,并且可能会对系统产生其他影响。确保在执行任何系统级别操作之前备份你的数据,并遵循操作系统提供的指南和最佳实践。
为什么提醒呢?我有台机器升级到25时曾经搞崩过。。。
总结
WebAssembly已经成为一种广泛采用的标准,它不仅可以与浏览器进行交互,还可以与其它编程语言进行无缝集成和交互。该技术的快速发展和标准化使得开发人员可以在不同的语言之间共享和重用代码,实现更高效、更灵活的开发。
作为一种标准,WebAssembly的目标是提供一种通用的字节码格式,使得各种编程语言都能编译成WebAssembly模块,并在各种环境中运行。这种通用性使得WebAssembly成为一种理想的技术选择,特别是在需要高性能和跨平台兼容性的应用场景。
通过使用Rust编译为WebAssembly,我们可以利用Rust的强大特性和生态系统,同时还能与其它语言进行交互。无论是在浏览器端还是服务器端,通过WebAssembly,我们可以将不同语言的模块相互调用,实现更丰富的功能和更高效的性能。
在本文中,我们重点介绍了使用Rust编译WebAssembly并与Node.js、Deno、Go和Java进行交互的示例。这些示例展示了WebAssembly在不同环境中的应用和优势,为开发人员提供了更多选择和灵活性。
参考资料
转载自:https://juejin.cn/post/7246672925666869305