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

4
我试图运行一个使用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

为了加载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!
});

WebAssembly.instantiate 崩溃:

LinkError: Import #0 module="env" error: module is not an object or function

我发现这个错误与某些缺失有关,而样板代码应该加载它,但是查看自动生成的HTML和JavaScript,我无法确定具体是什么。

1个回答

6

概述

您需要定义一系列由WASM模块导入的函数和值。当WASM模块导入未正确定义的内容时,会出现链接器错误。Emscripten生成了大量JS代码来定义WASM模块所需的所有导入项(在这种情况下,“容易”,因为Emscripten还生成了WASM模块本身)。

目前,您可以使用Emscripten运行时(JS文件),或者自己完成很多工作。

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


汇编语言和WASM

汇编语言机器码的可读形式(但这两个术语通常可以互换使用,因此在本文中我们也不会在意,只称之为汇编语言)。汇编语言是为机器/CPU执行而设计的,因此它非常简单。汇编语言基本上是一系列指令,每条指令都执行特定的微小操作。例如,有一个指令用于将两个数字相加,一个指令用于执行不同地址上的指令等等。

值得注意的是,缺少一个print指令。打印功能完全处于不同的抽象层级,并且执行的操作比单个指令复杂得多。此外,“打印”是什么意思?我们期望程序可以访问某种控制台。重要的是: WASM没有print指令或类似的东西!

像打印这样的操作需要由环境提供。对于大多数程序和计算机科学的大部分来说,这个环境就是操作系统。它管理着“控制台”,并且可以让你进行打印操作。然而,你的WASM程序的直接环境是浏览器!所以浏览器需要为您提供一种打印的方式。

链接

链接是将不同模块/编译单元之间的导入和导出连接(“解析”)到一起的过程。例如,在Rust中使用extern crate以及在C++中编译多个.cpp文件时都需要进行链接。

当你实例化一个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)

即使不知道如何阅读这个 wast 格式,我们也可以做出合理的猜测并且假设你的模块确实导入了一些东西。我们应该已经知道了,因为我们想要打印并且没有 print 指令!
你可能会想知道为什么没有一个 (import "env" "print" ...)。我无法完全解释,但原因基本上是:它比那更复杂。Emscripten 只使用了一小部分重要的导入,并使用这些导入来访问环境中的其他函数。

与 WASM (和 Emscripten) 链接

在 WASM 中进行链接是通过 WebAssembly.instantiate() 方法 完成的。正如所链接的文档中所示,此方法需要一个 importObject。如果在此对象中未定义每个 WASM 模块的导入函数/值,则会导致 WebAssembly.LinkError。这很有道理。
如果您想实例化由文件hello.wasm定义的WASM模块,则必须定义这62个导入项中的所有内容。这似乎非常麻烦,对吧?实际上,您不需要真正去做那些事情:这就是为什么Emscripten会为您生成必要的JS代码!Emscripten生成的WASM模块应该使用Emscripten生成的JS加载器进行加载!

在普通程序中打印?

值得一提的是,我们可以看看在本地环境(操作系统)运行的程序如何打印。它们肯定也需要与环境(即操作系统)链接,对吧?其实并不是。

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

据我所知,这些类型的syscalls在WASM中不可能(至少目前不可能)。

不使用Emscripten

编译为WASM需要两个主要的步骤:

  1. WASM代码生成:编译器必须输出WASM代码。
  2. 链接:由于通常有多个crate,我们需要进行链接(如上所述)。

Emscripten可以完成以上两个步骤¹,并且可以将代码生成与链接匹配,因为这两个部分都是由Emscripten完成的。是否有替代方案?

是的!你需要寻找的是Rust的wasm32-unknown-unknown目标。该目标使用LLVM的WASM后端来进行代码生成。使用此目标,您可以完全不使用Emscripten生成小型的WASM模块。更重要的是:您还可以自己编写JS加载程序,因为您可以决定导入内容,没有任何魔法添加。

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


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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接