2.6 构建Web服务器
既然,我们通过上述的魔法,将Rust程序编译为了可以在浏览器环境下引用执行的格式。为了这口醋,我们还专门包顿饺子。
我们需要一个Web服务器来测试我们的WebAssembly程序。我们将使用Webpack,我们需要创建三个文件:index.js、package.json和webpack.config.js。
下面的代码,我们最熟悉不过了,就不解释了。
index.js
// 直接引入了,刚才编译后的文件 const rust = import('./pkg/hello_world.js'); rust .then(m => m.helloworld('World!')) .catch(console.error);
package.json
{ "scripts": { "build": "webpack", "serve": "webpack-dev-server" }, "devDependencies": { "@wasm-tool/wasm-pack-plugin": "0.4.2", "text-encoding": "^0.7.0", "html-webpack-plugin": "^3.2.0", "webpack": "^4.29.4", "webpack-cli": "^3.1.1", "webpack-dev-server": "^3.1.0" } }
webpack.config.js
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const webpack = require('webpack'); const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); module.exports = { entry: './index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'index.js', }, plugins: [ new HtmlWebpackPlugin(), new WasmPackPlugin({ crateDirectory: path.resolve(__dirname, ".") }), // 让这个示例在不包含`TextEncoder`或`TextDecoder`的Edge浏览器中正常工作。 new webpack.ProvidePlugin({ TextDecoder: ['text-encoding', 'TextDecoder'], TextEncoder: ['text-encoding', 'TextEncoder'] }) ], mode: 'development' };
安装指定的依赖。
npm install webpack --save-dev npm install webpack-cli --save-dev npm install webpack-dev-server --save-dev npm install html-webpack-plugin --save-dev npm install @wasm-tool/wasm-pack-plugin --save-dev npm install text-encoding --save-dev
2.7 构建&运行程序
使用npm run build构建程序。
使用npm run serve运行Hello World程序
在浏览器中打开localhost:8080,我们将看到一个显示 Hello World! 的弹窗。
到目前为止,我们已经构建了一个wasm并且能够和js实现功能交互的项目。其实,到这里已经完成了,我们这篇文章的使命。但是,在这里戛然而止,感觉缺失点啥。所以,我们继续深挖上面的项目的实现原理。
3. 原理探析
在使用cargo和wasm_bindgen编译源代码时,会在pkg文件中自动生成以下文件:
- "hello_world_bg.wasm"
- "hello_world.js"
- "hello_world.d.ts"
- "package.json"
这些文件也可以通过使用以下wasm-bindgen命令手动生成:
bash
复制代码
wasm-bindgen target/wasm32-unknown-unknown/debug/hello_world.wasm --out-dir ./pkg
浏览器调用顺序
以下显示了当我们在浏览器中访问localhost:8080时发生的函数调用序列。
- index.js
- hello_world.js (调用hello_world_bg.js)
- helloworld_bg.wasm
index.js
const rust = import('./pkg/hello_world.js'); rust .then(m => m.helloworld('World!')) .catch(console.error);
index.js 导入了 hello_world.js 并调用其中的 helloworld 函数。
hello_world.js
下面是hello_world.js的内容,在其中它调用了helloworld_bg.wasm
import * as wasm from "./hello_world_bg.wasm"; import { __wbg_set_wasm } from "./hello_world_bg.js"; __wbg_set_wasm(wasm); export * from "./hello_world_bg.js";
hello_world_bg.js
// ...省去了部分代码 export function helloworld(name) { const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len0 = WASM_VECTOR_LEN; wasm.helloworld(ptr0, len0); }
hello_world_bg.js文件是由wasm-bindgen自动生成的,它包含了用于将DOM和JavaScript函数导入到Rust中的JavaScript粘合代码。它还在生成的WebAssembly函数上向JavaScript公开了API。
Rust WebAssembly专注于将WebAssembly与现有的JavaScript应用程序集成在一起。为了实现这一目标,我们需要在JavaScript和WebAssembly函数之间传递不同的值、对象或结构。这并不容易,因为需要协调两个不同系统的不同对象类型。
更糟糕的是,当前
WebAssembly仅支持整数和浮点数,不支持字符串。这意味着我们不能简单地将字符串传递给WebAssembly函数。
要将字符串传递给WebAssembly,我们需要将字符串转换为数字(请注意在webpack.config.js中指定的TextEncoderAPI),将这些数字放入WebAssembly的内存空间中,最后返回一个指向字符串的指针给WebAssembly函数,以便在JavaScript中使用它。在最后,我们需要释放WebAssembly使用的字符串内存空间。
如果我们查看上面的JavaScript代码,这正是自动执行的操作。helloworld函数首先调用passStringToWasm。
- 这个函数在
WebAssembly中创建一些内存空间,将我们的字符串转换为数字,将数字写入内存空间,并返回一个指向字符串的指针。
- 然后将指针传递给
wasm.helloworld来执行JavaScript的alert。最后,wasm.__wbindgen_free释放了内存。
如果只是传递一个简单的字符串,我们可能可以自己处理,但考虑到当涉及到更复杂的对象和结构时,这个工作会很快变得非常复杂。这说明了wasm-bindgen在Rust WebAssembly开发中的重要性。
反编译wasm到txt
在前面的步骤中,我们注意到wasm-bindgen生成了一个hello_world.js文件,其中的函数调用到我们生成的hello_world_bg.wasm中的WebAssembly代码。
基本上,
hello_world.js充当其他JavaScript(如index.js)与生成的WebAssembly的helloworld_bg.wasm之间的桥梁。
我们可以通过输入以下命令进一步探索helloworld_bg.wasm:
wasm2wat hello_world_bg.wasm > hello_world.txt
这个命令使用wabt将WebAssembly转换为WebAssembly文本格式,并将其保存到一个hello_world.txt文件中。打开helloworld.txt文件,然后查找$helloworld函数。这是我们在src/lib.rs中定义的helloworld函数的生成WebAssembly函数。
$helloworld函数
在helloworld.txt中查找以下行:
(export "helloworld" (func $helloworld))
这一行导出了wasm.helloworld供宿主调用的WebAssembly函数。我们通过hello_world_bg.js中的wasm.helloworld来调用这个WebAssembly函数。
接下来,查找以下行:
(import "./hello_world_bg.js" "__wbg_alert_9ea5a791b0d4c7a3" (func $hello_world::alert::__wbg_alert_9ea5a791b0d4c7a3::h93c656ecd0e94e40 (type 4)))
这对应于在hello_world_bg.js中生成的以下JavaScript函数:
export function __wbg_alert_9ea5a791b0d4c7a3() { return logError(function (arg0, arg1) { alert(getStringFromWasm0(arg0, arg1)); }, arguments) };
这是wasm-bindgen提供的粘合部分,帮助我们在WebAssembly中使用JavaScript函数或DOM。
最后,让我们看看wasm-bindgen生成的其他文件。
hello_world.d.ts
这个.d.ts文件包含JavaScript粘合的TypeScript类型声明,如果我们的现有JavaScript应用程序正在使用TypeScript,它会很有用。我们可以对调用WebAssembly函数进行类型检查,或者让我们的IDE提供自动完成。如果我们不使用TypeScript,可以安全地忽略这个文件。
package.json
package.json文件包含有关生成的JavaScript和WebAssembly包的元数据。它会自动从我们的Rust代码中填充所有npm依赖项,并使我们能够发布到npm。
4. 内容拓展
再次看一下以下代码:
hello_world_bg.js
function helloworld(name) { const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len0 = WASM_VECTOR_LEN; wasm.helloworld(ptr0, len0); }
该代码用于分配和释放内存,这一切都是由程序自动处理的。不需要垃圾回收器或完整的框架引擎,使得使用Rust编写的WebAssembly应用程序或模块变得小巧且优化。其他需要垃圾回收器的语言将需要包含用于其底层框架引擎的wasm代码。因此,无论它们有多么优化,其大小都不会小于Rust提供的大小。这使得Rust WebAssembly成为一个不错的选择,如果我们需要将小型WebAssembly模块集成或注入到JavaScript Web应用程序中。
除了Hello World之外,还有一些其他需要注意的事项:
web-sys
使用wasm-bindgen,我们可以通过使用extern在Rust WebAssembly中调用JavaScript函数。请记住src/lib.rs中的以下代码:
#[wasm_bindgen] extern "C" { fn alert(s: &str); }
Web具有大量API,从DOM操作到WebGL再到Web Audio等等。因此,如果我们的Rust WebAssembly程序增长,并且我们需要对Web API进行多次不同的调用,我们将需要花时间编写大量的extern代码。
web-sys充当wasm-bindgen的前端,为所有Web API提供原始绑定。
这意味着如果我们使用web-sys,可以节省时间,而不必编写extern代码。
引入web-sys
将web-sys添加为Cargo.toml的依赖项:
[dependencies] wasm-bindgen = "0.2" [dependencies.web-sys] version = "0.3" features = [ ]
为了保持构建速度非常快,web-sys将每个Web接口都封装在一个Cargo特性后面。在API文档中找到我们要使用的类型或方法;它将列出必须启用的特性才能访问该API。
例如,如果我们要查找window.resizeTo函数,我们会在API文档中搜索resizeTo。我们将找到web_sys::Window::resize_to函数,它需要启用Window特性。要访问该函数,我们在Cargo.toml中启用Window特性:
[dependencies.web-sys] version = "0.3" features = [ "Window" ]
调用这个方法:
use wasm_bindgen::prelude::*; use web_sys::Window; #[wasm_bindgen] pub fn make_the_window_small() { // 调整窗口大小为500px x 500px。 let window = web_sys::window().unwrap(); window.resize_to(500, 500) .expect("无法调整窗口大小"); }
这段代码的目的是调整浏览器窗口的大小为500x500像素,并演示了如何使用web-sys和启用的Cargo特性来调用Web API。
后记
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。








