使用 WebAssembly 在 Rust 中尝试 Hello World 时出现链接错误

The*_*hef 4 javascript rust webassembly

我正在尝试使用 WebAssembly 运行一个用 Rust 编写的 hello world 程序,但是当我尝试加载该程序时收到一条错误消息。

按照我发现的一些教程,我能够让它运行,问题是他们使用 Emscripten 创建 JavaScript 和 HTML 来加载代码,但是这个 JavaScript 和 HTML 包含大量的样板文件和其他东西。我有点迷路了,而是想尝试获得一个非常简单的例子,我正在加载自己。

我运行以下命令来编译 hello.wasm

echo 'fn main() { println!("Hello, Emscripten!"); }' > hello.rs
rustc --target=wasm32-unknown-emscripten hello.rs
Run Code Online (Sandbox Code Playgroud)

为了加载 hello.wasm,我从 Mozilla WebAssembly 文档中获取示例并尝试运行它。

var importObject = {
  imports: {
    imported_func: function(arg) {
      console.log(arg);
    }
  }
};

fetch('hello.wasm').then(response =>
  response.arrayBuffer()
).then(bytes =>
  WebAssembly.instantiate(bytes, importObject)
).then(results => {
  // Do something with the compiled results!
});
Run Code Online (Sandbox Code Playgroud)

WebAssembly.instantiate 崩溃:

var importObject = {
  imports: {
    imported_func: function(arg) {
      console.log(arg);
    }
  }
};

fetch('hello.wasm').then(response =>
  response.arrayBuffer()
).then(bytes =>
  WebAssembly.instantiate(bytes, importObject)
).then(results => {
  // Do something with the compiled results!
});
Run Code Online (Sandbox Code Playgroud)

我发现这个错误与缺少的东西有关,应该加载样板代码,但是查看自动生成的 HTML 和 JavaScript,我无法弄清楚它可能是什么。

Luk*_*odt 5

概括

您必须定义一组由 WASM 模块导入的函数和值。当 WASM 模块导入您未正确定义的内容时,您会收到此链接器错误。Emscripten 生成了一大堆 JS 代码,这些代码定义了 WASM 模块需要的所有导入(在这种情况下这很“容易”,因为 Emscripten 也生成了 WASM 模块本身)。

现在,您要么使用 Emscripten 运行时(JS 文件),要么必须自己做很多事情。

我会尝试更详细地解释,请耐心等待:


组装和 WASM

汇编机器代码的人类可读形式(但是这两个术语通常可以互换使用,因此我们在这篇文章中也不关心,只是将其称为assembly)。汇编是为机器/CPU 执行而设计的,因此非常简单。汇编基本上是一个指令列表,其中每条指令都做一个特定的、微小的事情。例如,有一条指令将两个数字相加,在不同的地址执行指令,等等。

值得注意的是缺少一条print指令。的某些功能print是完全不同的抽象级别,并且不仅仅是一条指令。另外,我们所说的“打印”是什么意思?我们希望我们的程序可以访问某种控制台。重复重要的部分:WASM 没有print指令或类似的东西!

诸如打印之类的东西需要由环境提供。对于大多数程序和大多数计算机科学来说,这个环境就是操作系统。它管理“控制台”并让您打印。然而,WASM 程序的直接环境是浏览器!因此浏览器必须为您提供一种打印方式。

链接

链接是将来自不同模块/编译单元的导入和导出相互连接(“解析”)的过程。例如,extern crate在 Rust 中使用s 以及.cpp在 C++ 中编译多个文件时,链接是必需的。

当您实例化 WASM 模块时,这也是必要的,因为该模块可能有导入。并且在我们执行模块之前需要解析这些导入。

那么你的模块有导入吗?我们来看一下!您可以使用该工具wasm-dis(反汇编程序)将二进制wasm代码转换为或多或少可读的汇编代码:$ wasm-dis hello.wasm > hello.wast. 查看此文件,我们可以看到以下内容:

(import "env" "DYNAMICTOP_PTR" (global $import$0 i32))
(import "env" "STACKTOP" (global $import$1 i32))
(import "env" "STACK_MAX" (global $import$2 i32))
(import "env" "abort" (func $import$3 (param i32)))
...
(58 more)
Run Code Online (Sandbox Code Playgroud)

即使不知道如何阅读这种wast格式,我们也可以做出合理的猜测并假设您的模块确实导入了东西。我们应该知道,因为我们要打印而没有print说明!

您可能想知道为什么没有(import "env" "print" ...)。我无法完全解释这一点,但原因基本上是:它比那更复杂。Emscripten 只使用一小组重要的导入,并使用这些导入来访问其他功能环境。

与 WASM(和 Emscripten)链接

WASM 中的链接是通过WebAssembly.instantiate()方法完成的。正如您在链接的文档中所见,此方法采用importObject. 未能在此对象中定义函数/值,WASM 模块的每次导入都会导致一个WebAssembly.LinkError. 说得通。

如果要实例化文件定义的 WASM 模块hello.wasm,则必须定义所有 62 个导入。这看起来真的很烦人,对吧?实际上,您并不真的希望这样做:这就是 Emscripten 为您生成必要的 JS 代码的原因!Emscripten 生成的 WASM 模块应该使用 Emscripten 生成的 JS-loader 加载!

在正常程序中打印?

值得一看的是在本机环境(操作系统)中运行的程序如何进行打印。它们肯定也需要与环境(即操作系统)相关联,对吗?并不真地。

虽然像 Rust、C 和 C++ 这样的编程语言确实有一个用于打印的标准库,但这个标准库不是操作系统的一部分。它只使用操作系统本身。最后,为了打印,使用了系统调用。系统调用使用 CPU 中断来调用操作系统的功能。这有一些优点(例如您不需要将程序与操作系统链接),但也有一些重要的缺点(例如它不是很快)。

AFAIK,这些类型的系统调用在 WASM 中是不可能的(至少现在是这样)。

不使用 Emscripten

编译为 WASM 需要做两件大事:

  1. WASM 代码生成:你的编译器不得不吐出 WASM 代码
  2. 链接:因为它通常不止一个板条箱,我们需要链接(如上所述)

Emscripten 可以同时执行¹并且可以将代码生成与链接相匹配,因为这两个部分都是由 Emscripten 完成的。有替代品吗?

是的!您正在寻找的是wasm32-unknown-unknownRust的目标。该目标使用 LLVM 的 WASM 后端进行代码生成。有了这个目标,你就可以在没有 Emscripten 的情况下完全生成小的 WASM 模块。更重要的是:你也可以自己编写 JS-loader,因为决定了你的导入,并且没有任何神奇的添加。

要了解有关这个令人兴奋的主题的更多信息,我建议您访问hellorust.com。在该网站上,您可以找到有关如何设置构建环境的简单示例和说明。


¹ Emscripten 不直接生成 WASM。它生成 asm.js 代码,然后将其转换为 WASM。