学习webassembly
时,很多人一开始都会根据编译 Rust 为 WebAssembly - WebAssembly | MDN上的代码照猫画虎一次,从而会使用wasm_bindgen
这个库,在这个学习的过程中我会使用trae来辅助学习。
其中我在使用wasm_bindgen
这个库将rust
所编译出来的webassembly
代码与js
进行bindgen(连接)
时,这样所生成的js
代码被称为了胶水代码
为了满足好奇心,我细细品读了这个胶水代码后
我才深深体会到了函数式编程的麻烦魅力!
这个胶水代码具有大量的防呆设计,让人看得眼花缭乱,而把这些防呆设计全部剔除,并且规范好使用方式后,该胶水代码将大大减少!
胶水代码行数实在太多,本文将所对应的胶水代码已上传github,链接在文末~
各位同学可自行下载与本文讲解进行对应(自己写一个和mdn一样的也可以哟)
我们切割代码,分模块来讲解最为核心的几个函数的用途
记住如下函数调用栈
__wbg_init 初始化wasm模块
在前端初始化wasm模块时,我们往往会使用如下代码进行初始化代码
javascript
import init, { greet } from './pkg/hello_wasm.js';
init().then((wasm) => {
console.log(wasm)
greet('World');
});
其中导出的init
函数,在胶水代码中所对应的方法是__wbg_init
,该函数接受一个module_or_path
文件地址参数,而这个文件地址可传可不传。
当不传时就会使用默认的文件地址,而该默认文件地址就是使用wasm_bindgen
生成的wasm文件的地址
其余的if
判断全是防呆设计
下面是__wbg_init
函数具体的实现与解释
javascript
/**
* @namme 异步初始化Wasm模块的函数声明
* @param {*} module_or_path 可选参数 可根据传入的url地址实例化对应的Wasm模块
* @returns
* @description 简而言之,传module_or_path就用传入的路径进行wasm实例化,不传就用默认路径进行wasm实例化
*/
async function __wbg_init(module_or_path) {
// 当wasm已经实例化返回该实例
if (wasm !== undefined) return wasm;
// 当module_or_path不为undefined时,进行参数解析
if (typeof module_or_path !== 'undefined') {
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
({ module_or_path } = module_or_path)
} else {
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
}
}
// 当module_or_path为undefined时,进行默认路径的参数解析
if (typeof module_or_path === 'undefined') {
module_or_path = new URL('hello_wasm_bg.wasm', import.meta.url);
}
// 获取 WebAssembly 模块所需的导入对象
const imports = __wbg_get_imports();
// 判断 module_or_path 是否为字符串、Request 实例或 URL 实例,如果是,则使用 fetch 函数将其转换为一个请求对象
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
module_or_path = fetch(module_or_path);
}
// 初始化内存
__wbg_init_memory(imports);
// 加载并实例化 WebAssembly 模块
const { instance, module } = await __wbg_load(await module_or_path, imports);
// 完成最终的初始化并返回 wasm 实例
return __wbg_finalize_init(instance, module);
}
上述代码充满了条件判断与函数调用,其中引用了如下四个函数
javascript
// 负责把js方法导入到rust中
__wbg_get_imports()
// 初始化内存(在本文中是一个空函数,因此不探讨)
__wbg_init_memory(imports);
// 加载wasm模块
__wbg_load(await module_or_path, imports);
// 给wasm实例添加一个函数
__wbg_finalize_init(instance, module)
现在我们着重讲述上述这四个函数的具体用途
__wbg_get_imports 把js方法导入到rust中
该函数其实就是在构造WebAssembly.Instance
函数中的第二个参数,并且把wasm模块需要的js代码导入到wasm模块中去使用。
javascript
/**
* @name 获取WebAssembly模块所需的导入对象
* @returns
* @description 简而言之,就是把js代码导入到wasm模块中,然后返回一个对象,这个对象包含了模块的实例,以及模块本身。
*/
function __wbg_get_imports() {
// 创建导入对象
const imports = {};
// wbg 是 wasm-bindgen 的缩写,用于存储所有导入的函数
imports.wbg = {};
// 导入alert方法,添加了一大坨容错
imports.wbg.__wbg_alert_151c8d49e07bfbc4 = function () {
return logError(function (arg0, arg1) {
// 导入的alert方法
// getStringFromWasm0方法用于解析字符串到wasm中
// 第一个参数是字符串的指针,第二个参数是字符串的长度
alert(getStringFromWasm0(arg0, arg1));
}, arguments)
};
// 导入错误处理函数
imports.wbg.__wbindgen_throw = function (arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
// WebAssemblyTable的JavaScript包装对象存储js与wasm模块需要交互的函数
imports.wbg.__wbindgen_init_externref_table = function () {
const table = wasm.__wbindgen_export_0;
const offset = table.grow(4);
// 初始化表项
table.set(0, undefined);
table.set(offset + 0, undefined);
table.set(offset + 1, null);
table.set(offset + 2, true);
table.set(offset + 3, false);
};
return imports;
}
更底层一点的解读就是:
- wasm只支持四种数据类型:
i32
、i64
、f32
和f64
- Wasm 模块的内存是一个线性内存的数组,在这个数组,wasm和js都可以对这个内存进行读取或者修改,但是这个数组,只能是
number
类型。要变成string
类型,就需要转换。转换的函数就是getStringFromWasm0
函数。下文讲解getStringFromWasm0
函数 - Wasm 模块的内存由 Wasm 实例与js所共享,两者都可以读取和修改它,
- 需要注意的是Wasm 模块最多只能请求 4GB 的内存,因此每个可能的内存地址都可以编码为
i32
,因此该数据类型也常用作内存地址指针。
关于这个WebAssemblyTable
解释起来相对复杂,先贴上部分解读,笔者在详细研究后再撰文分享~
getStringFromWasm0
关于这个函数,就是把字符串转换为wasm模块可以接受的数据类型。这个函数具体实现我将在下文阐述。
__wbg_load 加载wasm模块
然后是这个加载wasm模块的函数,这个函数里面一大推车轱辘子话,一大推的防呆设计,生怕我传了不应该传的参数。
其实核心函数就是await WebAssembly.instantiateStreaming(module, imports)
调用这个函数实例化加载了我们需要的wasm模块
javascript
/**
* @name 初始化Wasm模块的函数声明
* @param {*} module 一个 Response 对象或一个会兑现为 Response 的 promise,其表示你想要传输、编译和实例化的 Wasm 模块的底层源
* @param {*} imports 简而言之,把js中的方法导入到rust中去使用
* @returns {
* instance, // WASM 实例,包含导出的函数
* module // 编译好的 WASM 模块
* }
* instance: WebAssembly.Instance 对象,包含所有导出的 WebAssembly 方法。
* module: WebAssembly.Module 对象,表示编译完成的 WebAssembly 模块。这个 Module 能够再次被实例化或通过 postMessage() 共享。
* @description 简而言之就是调用WebAssembly.instantiateStreaming方法实例化wasm模块,然后加上了各种兜底的判断.
*/
async function __wbg_load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
// 分支1: 处理 Response 对象(通常来自 fetch)
// 尝试使用现代浏览器支持的流式加载方式
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
// 处理 MIME 类型错误
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("WebAssembly.instantiateStreaming failed...");
} else {
throw e;
}
}
}
// 如果流式加载失败,回退到传统方式
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
// 分支2: 直接处理 WebAssembly 模块进行实例化
const instance = await WebAssembly.instantiate(module, imports);
return instance instanceof WebAssembly.Instance ?
{ instance, module } : instance;
}
}
简而言之:
- 优先使用现代的流式加载(更快)
- 不能用流式加载就用传统的加载方式
__wbg_finalize_init 返回wasm实例
这个函数就没有太多需要阐述的了,把await WebAssembly.instantiateStreaming(module, imports)
函数产生的wasm
实例解构一层返回出去
javascript
/**
* @name 返回wasm实例
* @returns
* @description 简而言之,就是把instance和module都封装到这个wasm对象里面去,然后在返回出去给then方法。
*/
function __wbg_finalize_init(instance, module) {
// 将 WebAssembly 实例的导出内容赋值给全局 wasm 变量
// instance.exports 包含了所有从 WASM 模块导出的函数和内存
wasm = instance.exports;
// 保存 WebAssembly 模块的引用
// 这允许在需要时重新实例化模块
__wbg_init.__wbindgen_wasm_module = module;
// 重置内存视图缓存
// 这确保下次访问 WASM 内存时会重新创建新的视图
cachedUint8ArrayMemory0 = null;
// 调用 WASM 模块的初始化函数
// __wbindgen_start 是由 wasm-bindgen 自动生成的初始化函数
wasm.__wbindgen_start();
// 返回配置完成的 wasm 实例
return wasm;
}
passStringToWasm0 生成str类型
将字符串传入wasm模块中,wasm只能接收整数的方式来传递字符串,所以需要把字符串转换为Uint8Array,然后再把指针传递给wasm模块
javascript
/**
* @name 将字符串传入wasm模块中,wasm只能接收整数的方式来传递字符串,所以需要把字符串转换为Uint8Array,然后再把指针传递给wasm模块。
* @param {string} name
*/
function passStringToWasm0(arg, malloc, realloc) {
// 防呆设计类型检查
if (typeof (arg) !== 'string')
throw new Error(`expected a string argument, found ${typeof (arg)}`);
// 简单情况:不需要重新分配内存
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
// 复杂情况:可能需要重新分配内存
let len = arg.length;
let ptr = malloc(len, 1) >>> 0;
const mem = getUint8ArrayMemory0();
let offset = 0;
// 尝试直接复制 ASCII 字符
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
// 处理非 ASCII 字符
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view);
if (ret.read !== arg.length)
throw new Error('failed to pass whole string');
offset += ret.written;
ptr = realloc(ptr, len, offset, 1) >>> 0;
}
WASM_VECTOR_LEN = offset;
// 返回指针
return ptr;
}
看上去读了很多东西,但是感觉脑瓜子里面没有知识???
API解读
首先脑瓜子记住一个知识点,然后我们尝试自己手写一个简单来看看。
需要知道的是我们都是围绕WebAssembly.Instance
以及WebAssembly.Module
api来完成这些工作的
WebAssembly.Module
:将wasm文件模块化WebAssembly.Instance
:将模块化后的 WebAssembly 文件实例化
没有防呆设计与类型转换后,简化后的代码:
javascript
// 步骤 1:获取 WebAssembly 模块(二进制格式)
const wasmBuffer = ...; // 通过 fetch、文件读取等方式获取
// 步骤 2:编译模块
const module = new WebAssembly.Module(wasmBuffer);
// 步骤 3:实例化模块 (importsObject:需要导入给wasm的数据)
const instance = new WebAssembly.Instance(module, importsObject);
实例化后,可通过 <font style="color:rgba(0, 0, 0, 0.85);">instance.exports</font>
访问 WebAssembly 模块导出的内容:
javascript
// 调用导出的函数
instance.exports.greet(2, 3); // 假设 WebAssembly 导出了 greet 函数
// 访问导出的变量
console.log(instance.exports.version);
我们可通过 instance.exports.memory
获取模块的内存实例
javascript
const memory = new Uint8Array(instance.exports.memory.buffer);
模块与实例的区别:
WebAssembly.Module
是编译后的二进制代码,不包含运行时状态。WebAssembly.Instance
是模块的运行实例,包含内存和状态。
手写一份我们自己的胶水代码
javascript
// 获取并初始化 WebAssembly 模块
const wasmResponse = await fetch('./pkg/hello_wasm_bg.wasm');
const wasmBuffer = await wasmResponse.arrayBuffer();
const wasmModule = new WebAssembly.Module(wasmBuffer);
// 创建编解码器
const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
const textEncoder = new TextEncoder('utf-8');
// 创建导入对象
const importObject = {
wbg: {
// 导入 alert 方法
__wbg_alert_151c8d49e07bfbc4: function (ptr, len) {
ptr = ptr >>> 0;
len = len >>> 0;
const view = new Uint8Array(wasmInstance.exports.memory.buffer, ptr, len);
alert(textDecoder.decode(view));
},
// 导入错误处理函数
__wbindgen_throw: function (ptr, len) {
ptr = ptr >>> 0;
len = len >>> 0;
const view = new Uint8Array(wasmInstance.exports.memory.buffer, ptr, len);
throw new Error(textDecoder.decode(view));
},
// 初始化外部引用表
__wbindgen_init_externref_table: function () {
const table = wasmInstance.exports.__wbindgen_export_0;
const offset = table.grow(4);
table.set(0, undefined);
table.set(offset + 0, undefined);
table.set(offset + 1, null);
table.set(offset + 2, true);
table.set(offset + 3, false);
}
}
};
// 实例化 WASM 模块
const wasmInstance = new WebAssembly.Instance(wasmModule, importObject);
// 导出 greet 函数
export function greet(name) {
// 将字符串编码并写入 WASM 内存
const buf = textEncoder.encode(name);
const ptr = wasmInstance.exports.__wbindgen_malloc(buf.length, 1) >>> 0;
new Uint8Array(wasmInstance.exports.memory.buffer).set(buf, ptr);
// 调用 WASM 的 greet 函数
wasmInstance.exports.greet(ptr, buf.length);
}
其实很简单不是吗?只用51行代码就完成了原本的工作~~
参考:
WebAssembly.instantiate() - WebAssembly | MDN
WebAssembly.Instance - WebAssembly | MDN
导出的 WebAssembly 函数 - WebAssembly | MDN
特别鸣谢:
trea这个工具,整个胶水代码的解读以及手写的胶水代码,api的解读均由它双重验证~
github链接: