全网不唯一,但最热心带读Wasm胶水代码!被面试官问到再也不怕啦~

学习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;
}

更底层一点的解读就是

  1. wasm只支持四种数据类型:i32i64f32f64
  2. Wasm 模块的内存是一个线性内存的数组,在这个数组,wasm和js都可以对这个内存进行读取或者修改,但是这个数组,只能是number类型。要变成string类型,就需要转换。转换的函数就是getStringFromWasm0函数。下文讲解getStringFromWasm0函数
  3. Wasm 模块的内存由 Wasm 实例与js所共享,两者都可以读取和修改它,
  4. 需要注意的是Wasm 模块最多只能请求 4GB 的内存,因此每个可能的内存地址都可以编码为 i32,因此该数据类型也常用作内存地址指针。

关于这个WebAssemblyTable解释起来相对复杂,先贴上部分解读,笔者在详细研究后再撰文分享~

WebAssembly.Table接口

导出的 WebAssembly 函数 - WebAssembly | MDN

WebAssembly.Table - WebAssembly | MDN

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;
    }
}

简而言之:

  1. 优先使用现代的流式加载(更快)
  2. 不能用流式加载就用传统的加载方式

__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.Moduleapi来完成这些工作的

  1. WebAssembly.Module:将wasm文件模块化
  2. 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行代码就完成了原本的工作~~

参考:

在非 Rust 服务器中逐步引入 Rust 以提高性能

WebAssembly.instantiate() - WebAssembly | MDN

WebAssembly.Instance - WebAssembly | MDN

导出的 WebAssembly 函数 - WebAssembly | MDN

特别鸣谢:

trea这个工具,整个胶水代码的解读以及手写的胶水代码,api的解读均由它双重验证~

github链接:

解读mdn上HelloWordWasm生成的胶水代码

相关推荐
Source.Liu7 小时前
【quantity】9 长度单位模块(length.rs)
rust
DonciSacer7 小时前
第一章-Rust入门
开发语言·后端·rust
景天科技苑3 天前
【Rust通用集合类型】Rust向量Vector、String、HashMap原理解析与应用实战
开发语言·后端·rust·vector·hashmap·string·rust通用集合类型
UestcXiye3 天前
Rust 学习笔记:枚举与模式匹配
rust
UestcXiye4 天前
Rust 学习笔记:修复所有权常见错误
rust
hrrrrb4 天前
【Rust】所有权
开发语言·后端·rust
Source.Liu4 天前
【quantity】1 创建 crates.io 账号并上传 Rust 库
rust
Dreamfine4 天前
Rust将结构导出到json如何处理小数点问题
rust·json·dolphindb·rust_decimal·serde_json
muyouking115 天前
Rust多线程性能优化:打破Arc+锁的瓶颈,效率提升10倍
开发语言·性能优化·rust
heroboyluck5 天前
rust 全栈应用框架dioxus
前端·rust·dioxus