全网不唯一,但最热心带读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生成的胶水代码

相关推荐
techdashen2 小时前
Rust主流框架性能比拼: Actix vs Axum vs Rocket
开发语言·后端·rust
Source.Liu3 小时前
【Raqote】 1.2 路径填充ShaderClipMaskBlitter结构体(blitter.rs)
rust·cad
汪子熙4 小时前
使用 Trae 开发一个演示勾股定理的动画演示
前端·人工智能·trae
pumpkin845145 小时前
理解 Rust 中的 String 分配机制
开发语言·rust
顾洋洋6 小时前
WASM与OPFS组合技系列二(魔改读操作)
前端·javascript·webassembly
维维酱8 小时前
Rust - Deref 强制转换
rust
XuanXu10 小时前
我使用Tauri deep-link插件踩过的坑
rust
无名之逆12 小时前
[特殊字符] 超轻高性能的 Rust HTTP 服务器 —— Hyperlane [特殊字符][特殊字符]
java·服务器·开发语言·前端·网络·http·rust
夕水12 小时前
学到了学到了,一个小小的demo里隐藏着一个有趣的算法
trae
小old弟13 小时前
🤔不会搭建技术博客,Trae+vitepress,😎3s搞定
前端·trae