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。
后记
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。