大多数WebAssembly在线教程和示例都侧重于在浏览器中使用它,以加速网站或Web应用程序的各种功能。但是,有一个领域的WebAssembly功能非常强大,但没有太多讨论:浏览器场景之外的领域。这就是我们将在这一系列帖子中关注的。
网络人经常给事物带来恶名(web-GPU就是另一个例子)。WebAssembly既不是Web,也不是汇编,而是一种字节码,可以从C++、C#、Rust和其他语言中定位。这意味着您可以编写一些Rust代码,将其编译成WebAssembly,然后在WebAssembly虚拟机中运行该代码。
这是非常强大的,因为您将不再需要处理垃圾收集的脚本语言,基本上使用Rust或C++作为您的脚本语言。WebAssembly支持可预测和稳定的性能,因为它不像通常的选项(Lua/JavaScript)那样需要垃圾收集。
这是一个相对较新的产品,有很多粗糙的地方,特别是在浏览器之外的情况下。在我的经历中,最艰难的事情之一是为浏览器外的场景编写文档,这就是我发表博客文章的原因,目的是记录我的发现,希望能帮助一些可能对这个主题感兴趣的人。
对于浏览器之外的场景,它的主要优势之一是提供系统级访问,而不会影响安全性。这是通过WASI(Web组装系统接口)完成的。WASI是一组类似C的函数,它们以安全的方式提供对FD_READ、RAND、FD_WRITE、THREADS(WIP)等功能的访问。
在以下几种情况下,您可以在浏览器之外使用Web程序集:
以最小的开销运行一些代码,就像Fastly/Cloudflare在边缘计算场景中所做的那样。
在物联网设备上安全地运行一些易于更新的代码,并将运行时开销降至最低。
为了在这次冒险中获得最佳体验,我建议使用Visual Studio代码作为您的IDE,并安装以下扩展:
首先,您需要一个可以运行WebAssembly程序的虚拟机(VM)。此虚拟机需要是可嵌入的,以便您可以将其添加到您的游戏引擎中,或者从现在开始我们将在主机程序中将其添加到游戏引擎中。有几个可供选择:WASM3、Wasmtime、WAMR和许多其他的。它们具有各种特性,例如支持JIT、使用尽可能少的内存等等,您必须选择一种适合您的目标平台和场景的特性。
除了调试之外,除了运行时属性外,您选择什么VM都无关紧要。我发现的唯一允许无缝调试体验的VM是Wasmtime(这是另一个粗糙的边缘)。因此,即使您由于其他限制而不打算将其部署到任何地方,我也建议您将其用作调试VM。无论何时需要调试一些WASM代码,都可以使用Wasmtime启动它。
现在,我们可以编辑lib.rs并从中导出以下与CFFI兼容的函数:
#[NO_MANGLE]外部";C";FN SUM(a:I32,b:I32)->;I32{let s=a+b;println!(";from WASM:SUM is:{:?}";,s);s}。
这是一个函数,它接受两个数字,将它们相加,然后在返回它们的和之前打印结果。WebAssembly没有定义在加载模块之后执行的默认函数,因此在宿主程序中,您需要根据函数的签名获取函数并运行它(非常类似于dlopen/dlsym的工作方式)。
我们使用[#no_manger]和pub extern";C";将这个SUM函数(以及我们希望从主机VM调用的任何其他函数)公开为可从C调用的函数。如果您是从一些WASM来这里学习浏览器教程,您可能会注意到我们根本不需要使用wasm-bindgen。
Rust支持WebAssembly的两个目标:wam32-未知-未知和wam32-wai。第一个是基本的WebAssembly。可以将其视为WebAssembly的[#no-std]。它是您在浏览器中使用的那种,它不假定有任何系统功能可用。
在另一端,wam32-wasi假设VM公开了WASI功能,允许使用标准库的不同实现(该实现依赖于可用的WASI函数)。
您可以在这里查看Rust的stdlib的可用实现:https://github.com/rust-lang/rust/tree/master/library/std/src/sys这是假设当在WebAssembly VM:https://github.com/rust-lang/rust/tree/master/library/std/src/sys/wasi.中运行时,Rust程序可以使用WASI函数的实现。
#只运行此命令一次,然后为wam32-wai目标添加wasm32-wai#编译。Cargo build wasm32-wai。
您可能已经注意到,我们调用println!()并期望程序工作并打印到控制台,但是WebAssembly程序如何知道如何做到这一点呢?
这就是我们使用wam32-wai的原因。此目标为rust stdlib选择假定具有某些功能的版本(WASI函数)。打印到控制台意味着只需写入特殊的文件描述符。大多数虚拟机在默认情况下都允许这样做,因此除了编译正确的wam32-wai目标之外,我们不需要进行任何特殊设置。
如果您已经为vscode安装了所需的扩展,您现在可以右键单击target/wasm32-wai/debug/wasm_example.wasm并选择Show WebAssembly,您应该会在vscode中打开一个新文件,如下所示:
(模块...。(类型$T15(函数(参数i64 I32 I32)(结果I32)(IMPORT";WASI_SNAPSHOT_PREVIE1";";FD_WRITE&34;(FUNC$_ZN4wasi13lib_generated22wasi_snapshot_preview18fd_write17h6ec13d25aa9fb6acE(类型$T8)(IMPORT";WASI_SNAPSHOT_PREVIE1";";PROC_EXIT";(函数$__WASI_PROC_EXIT(类型$t0)(IMPORT";WASI_SNAPSHOT_PREVIE1";";ENVIRON_SIZES_GET";(FUNC$__WASI_ENVIRON_SIZES_GET(类型$T2)(IMPORT";WASI_SNAPSHOT_PREVIE1";";ENVIRON_GET";(函数$__WASI_ENVIRON_GET(类型$T2)(函数$_ZN4core3fmt9Arguments6new_v117hb11611244be67330E(类型$t9)(参数$P0 I32)(参数$p1 I32)(参数$p2 I32)(参数$p3 I32)(参数$p4 I32)(本地$L5 I32)(本地$16 I32)(本地$L7 I32)。(LOCAL$18 I32)(LOCAL$L9 I32)(LOCAL$L10 I32)global.get$G0 local.set$L5...。
这是一份水务档案。WAT代表WebAssembly文本格式。这有点像在反汇编二进制文件时查看x64/arm ASM指令,只是更难看,更难理解。我读到这是因为WebAssembly的创建者不能决定文本格式,所以他们只是把它留在这个难看的s表达式形式中。
这里的import语句告诉我们,WASM程序需要以下函数来运行存在于WASI_SNAPSHOT_PREVIEW 1名称空间中的PROC_EXIT、FD_WRITE、ENVIRON_GET、ENVIRON_SIZES_GET。从WebAssembly模块导入或导出的所有函数都需要命名空间。WASI_SNAPSHOT_PREVIE1是WASI名称空间,因此您可以将其视为这些函数的保留名称空间。普林特恩!需要WASI_SNAPSHOT_PREVIE1::FD_WRITE才能写入标准输出。
您可以选择任何具有WASI可用的虚拟机。我将使用Wasmtime,因为稍后我想向您展示如何调试WebAssembly,而这个VM是目前唯一可以调试的虚拟机。
该程序从路径:Examples/wasm_example.wasm加载wasm二进制文件。这是您以前编译过的文件,可以在wasm_example/target/wasm32-wasi/debug/wasm_example.wasm.中找到。在运行主机程序之前,请确保将其移动到正确的位置。
下面是主机VM rust程序的完整清单,该程序初始化Wasmtime VM、加载模块、链接WASI,并从WASM模块加载并执行导出的SUM函数:
Use std::Error::Error;use wasmtime::*;use wasmtime_wasi::{wasi,WasiCtx};fn main()->;result<;(),Box<;dyn error>;{//A`Store`在某种意义上是一种全局对象,但现在说它通常传递给大多数构造就足够了。//让store=Store::Default();让Engine=Engine::New(Config::New().debug_info(True));让store=Store::New(&;Engine);//我们首先创建一个`Module`,表示输入wasm模块的编译形式//。在本例中,它将在//我们解析文本格式之后进行JIT编译。Let module=Module::from_file(&;engine,&;engine/wasm_example.wasm";)?;//将WASI模块链接到我们的VM。Wasmtime允许我们决定WASI是否在场。//因此我们需要在这里加载它,因为我们的模块需要从//WASI_SNAPSHOT_PREVIE1名称空间中提供某些函数,如上所述。//这将使我们的WASM程序中的println!()正常工作。(它使用FD_WRITE)。让wasi=wasi::new(&;store,WasiCtx::New(std::env::args()?);让mut Imports=VEC::New();用于模块.Imports()中的导入{if import.module()==";wasi_snap_preview1";{if let ome(Export)=wasi.get_export(import.name(){Imports.ush(Extern::from(export.。}}死机!(";找不到`{}::{}`";,import.module(),import.name());}//在我们编译了一个`Module`之后,我们可以实例化它,创建//一个我们可以实际插入函数的`Instance`。让instance=instance::new(&;store,&;module,&;Imports)?;//`Instance`让我们可以访问各种导出的函数和项,//我们可以在这里访问它们来拉出我们的`swer`导出函数并//运行它。让main=instance.get_func(";sum";).Expect(";`main`不是导出函数";);//有几种方法可以调用`main``函数`值。//最简单的方法是使用`get2`静态断言其签名(本例中断言//它接受2个I32参数并返回1个I32),然后调用它。让main=main.get2::<;I32,I32,I32>;()?;//最后我们可以调用我们的函数了!请注意,使用`?`进行错误传播//是为了处理wasm函数陷入陷阱的情况。让result=main(5,4)?;println!(";from host:答案返回给主机VM:{:?}";,result);确定(())}。
编译wasm_host v0.1.0(Wasm_Host)在运行`target\debug\wasm_host.exe`FROM WASM:SUM IS:9FROM HOST:答案返回到主机VM:9的35.38秒内完成了dev[未优化+调试信息]目标。
我们可以观察到Printn!从wasm模块返回的信息已正确打印到控制台,返回的答案如预期的那样为9。
在我的WebAssembly Outside the Browser系列的这篇文章中,我们学习了如何编译WebAssembly的程序、设置主机程序来加载和运行您的WASM二进制文件、执行WASM程序导出的函数以及将所有这些放在一起,最后我们将两个数字相加并打印它们的结果(从WebAssembly和主机程序)。在接下来的部分中,我们将涉及调试、优化程序大小、将主机VM的函数公开给WASM程序以及在两个VM之间共享内存等领域。
以下是完整的WASM规范。对我来说,这是我读过的最难的规范之一。我更希望这个规范类似于CPU用户手册(例如VR4300),而不是将当前的形式强行转换成某种数学语言,虽然正确,但不会给读者带来额外的清晰度或洞察力。我强烈认为这里描述的概念本可以用一种更容易理解和解析的语言很好地表达出来,我不相信“实际上,它针对的是VM作者,而不是普通人”这种常见的借口。我们应该接受它根本无法访问的事实,我们可以做得更好。