百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

wasm-pack构建的wasm包如何用于微信小程序

haoteby 2025-03-07 19:06 38 浏览

微信小程序对于WebAssembly的支持

微信小程序基础库版本从2.13.0开始,通过WXWebAssembly对象对集成的wasm包进行支持。

WXWebAssembly

WXWebAssembly 类似于 Web 标准 WebAssembly,能够在一定程度上提高小程序的性能。

从基础库 v2.13.0 开始,小程序可以在全局访问并使用 WXWebAssembly 对象。

从基础库 v2.15.0 开始,小程序支持在 Worker 内使用 WXWebAssembly。

WXWebAssembly.instantiate(path, imports)

和标准 WebAssembly.instantiate 类似,差别是第一个参数只接受一个字符串类型的代码包路径,指向代码包内 .wasm 文件

与 WebAssembly 的异同

  1. WXWebAssembly.instantiate(path, imports) 方法,path为代码包内路径(支持.wasm和.wasm.br后缀)
  2. 支持 WXWebAssembly.Memory
  3. 支持 WXWebAssembly.Table
  4. 支持 WXWebAssembly.Global
  5. export 支持函数、Memory、Table,iOS 平台暂不支持 Global

微信官方仅提供了WXWebAssebly对象作为载入wasm文件的接口,我们的wasm包是通过wasm-pack编译打包而来,通常类似于wasm-pack或者emcc等工具打包的wasm package。除了wasm文件之外,还会提供用于前端代码与wasm后端进行交互的胶水代码,用于转变数据格式,通过内存地址进行通信初始化wasm文件。因此,我们按照wasm-pack官方文档进行引用时,由于微信提供的初始化接口与MDN不一致,我们需要对胶水文件做一些修改

wasm-pack web端引入方式

当我们使用

wasm-pack build --target web

命令进行编译和打包时,会产生一个如下图的输出文件结构:

  • 其中两个 .d.ts 文件我们都比较熟悉,就是ts的类型声明文件
  • .js 文件是前端应用与wasm文件交互的胶水文件
  • .wasm 文件就是wasm二进制文件

wasm-pack 文档中描述如下代码,对其模块进行引入

import init, { add } from './pkg/without_a_bundler.js';
async function run() {  
  await init(); 
  const result = add(1, 2); 
  console.log(`1 + 2 = ${result}`); 
  if (result !== 3)   
    throw new Error("wasm addition doesn't work!");
}
run();

可见胶水js文件向外暴露了一个模块,其中含有一个init方法用于初始化wasm模块,其他则为wasm模块向外暴露的方法

如果我们直接使用同样的方法在小程序中载入wasm模块,会出现下面的异常

SyntaxError: Cannot use 'import.meta' outside a module
Unhandled promise rejection Error: module "XXX" is not defined

修改WebAssembly引入方式

上一节最后提到的异常中,第一条比较常见,我们看到wasm-pack生成的胶水文件中,init函数中有用到import.meta属性

if (typeof input === 'undefined') {
  input = new URL('XXX.wasm', import.meta.url);
}

...

if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
  input = fetch(input);
}

报错信息表示import.meta 元属性只能在模块内部调用。这段代码在浏览器环境中是没有问题的,但是在小程序环境中就会报错,不知道是不是由于小程序环境中对ESM的支持度还不够。

总而言之,我们可以看到这段代码的意义是接下来使用fetch将远端的wasm文件下载下来,然后再调用其他方法对wasm文件进行初始化。

而小程序的文档描述中清楚的说到:

WXWebAssembly.instantiate(path, imports)

和标准 WebAssembly.instantiate 类似,差别是第一个参数只接受一个字符串类型的代码包路径,指向代码包内 .wasm 文件

因此可以理解为,使用小程序的初始化函数时,由于wasm文件会打包在小程序应用包中,因此也不需要考虑下载wasm文件的情况。

因此我们在init函数中删掉相关代码,修改之后的init函数变为:

async function init(input) {
  /* 
  删掉下面注释的代码
  if (typeof input === 'undefined') {
    input = new URL('ron_weasley_bg.wasm', import.meta.url);
  }
  */
  const imports = {};
  imports.wbg = {};
  imports.wbg.__wbindgen_throw = function(arg0, arg1) {
    throw new Error(getStringFromWasm0(arg0, arg1));
  };

  /*
  input 参数我们将直接传入wasm文件的绝对路径,下面这些用于判断是否需要生成一个fetch对象的代码也没有用了
  删除下面注释的代码
  if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
    input = fetch(input);
  }
  */

  // const { instance, module } = await load(await input, imports); // 这里的 input 参数是字符串,await也可以删除了
  const { instance, module } = await load(input, imports);
  
  
  wasm = instance.exports;
  init.__wbindgen_wasm_module = module;

  return wasm;
}

接下来,我们在小程序的Page文件中尝试引用wasm模块的init方法:

onLoad: async function (options) {
  await init('/pages/main/pkg/ron_weasley_bg.wasm');
}

会出现报错

VM409 WAService.js:2 Unhandled promise rejection ReferenceError: WebAssembly is not defined

修改wasm初始化调用方式

上面一节最后出现的异常,就很清楚了,我们只需要在胶水文件中找到对于WebAssembly的引用,替换为WXWebAssembly即可。

经过查找可以看胶水文件中对于WebAssembly的引用全部出现在 async function load 函数中:

async function load(module, imports) {
    if (typeof Response === 'function' && module instanceof Response) {
        if (typeof WebAssembly.instantiateStreaming === 'function') {
            try {
                return await WebAssembly.instantiateStreaming(module, imports);

            } catch (e) {
                if (module.headers.get('Content-Type') != 'application/wasm') {
                    console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);

                } else {
                    throw e;
                }
            }
        }

        const bytes = await module.arrayBuffer();
        return await WebAssembly.instantiate(bytes, imports);

    } else {
        const instance = await WebAssembly.instantiate(module, imports);

        if (instance instanceof WebAssembly.Instance) {
            return { instance, module };

        } else {
            return instance;
        }
    }
}

由于我们传入的module参数为wasm文件的绝对路径,因此一定不是Response类型,所以我们不用管函数中if的正向分支,来仔细看看else分支

// 下面这行代码是初始化wasm模块的方法,就是我们需要替换的 WebAssembly
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
  return { instance, module };
} else {
  return instance;
}

修改之后的else分支是这个样子

const instance = await WXWebAssembly.instantiate(module, imports);
if (instance instanceof WXWebAssembly.Instance) {
  return { instance, module };
} else {
  return instance;
}

刷新小程序开发工具,不再报异常了。接下来我们调用wasm中的XXX方法。

import init, { xxx } from './pkg/ron_weasley'

Page({
  onLoad: async function (options) {
    await init('/pages/main/pkg/xxx.wasm');
    console.log(xxx('1111', '2222'))
  }
})

小程序开发工具正常执行了,也返回了正确的值。这非常好。于是我非常惬意的在真机上也来了一把测试,异常如下:

ReferenceError: Can't find variable: TextDecoder

小程序的TextEncoder & TextDecoder

搜一下胶水文件,发现其中使用了TextEncoder和TextDecoder用来进行UInt8Array与JS String的互相转换。

web标准中,所有现代浏览器都已经实现了这两个类,但是被阉割的小程序环境竟然没有实现这两个类。如果无法进行UInt8Array与JS String之间的互相转换,就意味着JS可以调用wasm模块的函数,但是无法传值,wasm模块执行之后的返回数值,也无法传递给JS使用。

  • 思路一:手撸一套转化代码。可行,但是是否能够覆盖所有case,以及健壮性都是令人担心的
  • 思路二:既然是现代浏览器才实现的能力,那么一定存在polyfill,网上找找

MDN推荐的polyfill是一个名字巨长的包,叫做:
FastestSmallestTextEncoderDecoder

github地址在这里:
https://github.com/anonyco/FastestSmallestTextEncoderDecoder

我们将其引入胶水文件,并赋值给模块内部的TextEncoder & TextDecoder

require('../../../utils/EncoderDecoderTogether.min')

const TextDecoder = global.TextDecoder;
const TextEncoder = global.TextEncoder;

再次执行,报异常:

TypeError: Cannot read property 'length' of undefined
    at p.decode (EncoderDecoderTogether.min.js? [sm]:61)
    at ron_weasley.js? [sm]:10
    at p (VM730 WAService.js:2)
    at n (VM730 WAService.js:2)
    at main.js? [sm]:2
    at p (VM730 WAService.js:2)
    at :1148:7
    at doWhenAllScriptLoaded (:1211:21)
    at Object.scriptLoaded (:1239:5)
    at Object. (:1264:22)(env: macOS,mp,1.05.2109131; lib: 2.19.4)

可以看到是EncoderDecoderTogether中对于TextDecoder.decode方法的调用引发了异常,观察一下胶水文件中有一行代码

let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });

cachedTextDecoder.decode();

下面这行代码,调用了decode方法,但是参数为空,引发了length of undefined异常。

删除之后继续报异常:

VM771 WAService.js:2 Unhandled promise rejection TypeError: Failed to execute 'decode' on 'TextDecoder': The provided value is not of type '(ArrayBuffer or ArrayBufferView)'
    at p.decode (EncoderDecoderTogether.min.js? [sm]:formatted:1)
    at getStringFromWasm0 (ron_weasley.js? [sm]:20)
    at ron_weasley_sign (ron_weasley.js? [sm]:100)
    at _callee$ (main.js? [sm]:18)
    at L (regenerator.js:1)
    at Generator._invoke (regenerator.js:1)
    at Generator.t. [as next] (regenerator.js:1)
    at asyncGeneratorStep (asyncToGenerator.js:1)
    at c (asyncToGenerator.js:1)
    at VM771 WAService.js:2(env: macOS,mp,1.05.2109131; lib: 2.19.4)

在github仓库的issue中搜索,发现有人反馈在调用decode时,对于Uint8Array的buffer进行slice的时候这个库会有offset不准的情况出现。问题找到了,解决就简单了,直接找找有没有办法将Uint8Array转为String类型即可。

var str = String.fromCharCode.apply(null, uint8Arr);

引用这个答案:
https://stackoverflow.com/a/19102224

这个问题中其他答案也讨论了通过读取blob数据再进行转换的方案

以及使用String.fromCharCode方法时,如果uint8arr数据量过大时会发生栈溢出的异常,可以通过对uint8arr进行分片逐步转化的方案进行优化

如有兴趣可以阅读这个问题:
https://stackoverflow.com/questions/8936984/uint8array-to-string-in-javascript

接下来我们把使用到
FastestSmallestTextEncoderDecoder中TextDecoder的部分进行替换:

let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); // 删除
function getStringFromWasm0(ptr, len) {
    return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); // 替换
}

修改之后的相关代码为

function getStringFromWasm0(ptr, len) {
    return String.fromCharCode.apply(null, getUint8Memory0().subarray(ptr, ptr + len))
}

再次运行小程序开发工具,已经没有问题了,再来看看真机,果然还是异常了:

MiniProgramError Right hand side of instanceof is not a object

WXWebAssembly的Instance属性

还记得前几节我们替换WebAssembly为WXWebAssembly吗?

这次的异常仍然出现在load函数的else分支中

const instance = await WXWebAssembly.instantiate(module, imports);

if (instance instanceof WXWebAssembly.Instance) { // 就是这里
  return { instance, module };
} else {
  return instance;
}

debug一下发现代码走的是else分支。看了下文档:

instance instances WebAssembly.Instance 是在通过Instance方法初始化wasm时为true

不知道理解的对不对,如果instantiate方法初始化时上面的判断为false的话,那么我们直接删除判断即可,直接返回instance。

修改之后,开发工具与真机都不报错了,算是大功告成。

完整代码

修改的diff列表如下:

1,3d0
< require('../../../utils/EncoderDecoderTogether.min')
<
< const textencoder='global.TextEncoder;' 6a46> let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
>
> cachedTextDecoder.decode();
17c17
< return string.fromcharcode.applynull getuint8memory0.subarrayptr ptr len --->     return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
124,125c124,131
<         const instance = await WXWebAssembly.instantiate(module, imports);
< return instance --->         const instance = await WebAssembly.instantiate(module, imports);
>
>         if (instance instanceof WebAssembly.Instance) {
>             return { instance, module };
>
>         } else {
>             return instance;
>         }
130c136,138
< --->     if (typeof input === 'undefined') {
>         input = new URL('ron_weasley_bg.wasm', import.meta.url);
>     }
136a145,149
>     if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
>         input = fetch(input);
>     }
>
>
138c151
< const instance module loadinput imports --->     const { instance, module } = await load(await input, imports);

修改之后的胶水文件:

require('../../../utils/EncoderDecoderTogether.min')

const TextEncoder = global.TextEncoder;

let wasm;


let cachegetUint8Memory0 = null;
function getUint8Memory0() {
    if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) {
        cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer);
    }
    return cachegetUint8Memory0;
}

function getStringFromWasm0(ptr, len) {
    return String.fromCharCode.apply(null, getUint8Memory0().subarray(ptr, ptr + len))
}

let WASM_VECTOR_LEN = 0;

let cachedTextEncoder = new TextEncoder('utf-8');

const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
    ? function (arg, view) {
    return cachedTextEncoder.encodeInto(arg, view);
}
    : function (arg, view) {
    const buf = cachedTextEncoder.encode(arg);
    view.set(buf);
    return {
        read: arg.length,
        written: buf.length
    };
});

function passStringToWasm0(arg, malloc, realloc) {

    if (realloc === undefined) {
        const buf = cachedTextEncoder.encode(arg);
        const ptr = malloc(buf.length);
        getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
        WASM_VECTOR_LEN = buf.length;
        return ptr;
    }

    let len = arg.length;
    let ptr = malloc(len);

    const mem = getUint8Memory0();

    let offset = 0;

    for (; offset < len offset const code='arg.charCodeAt(offset);' if code> 0x7F) break;
        mem[ptr + offset] = code;
    }

    if (offset !== len) {
        if (offset !== 0) {
            arg = arg.slice(offset);
        }
        ptr = realloc(ptr, len, len = offset + arg.length * 3);
        const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
        const ret = encodeString(arg, view);

        offset += ret.written;
    }

    WASM_VECTOR_LEN = offset;
    return ptr;
}

let cachegetInt32Memory0 = null;
function getInt32Memory0() {
    if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) {
        cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer);
    }
    return cachegetInt32Memory0;
}
/**
* @param {string} message
* @param {string} cnonce
* @returns {string}
*/
export function xxx(message, cnonce) {
    try {
        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
        var ptr0 = passStringToWasm0(message, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
        var len0 = WASM_VECTOR_LEN;
        var ptr1 = passStringToWasm0(cnonce, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
        var len1 = WASM_VECTOR_LEN;
        wasm.xxx(retptr, ptr0, len0, ptr1, len1);
        var r0 = getInt32Memory0()[retptr / 4 + 0];
        var r1 = getInt32Memory0()[retptr / 4 + 1];
        return getStringFromWasm0(r0, r1);
    } finally {
        wasm.__wbindgen_add_to_stack_pointer(16);
        wasm.__wbindgen_free(r0, r1);
    }
}

async function load(module, imports) {
    if (typeof Response === 'function' && module instanceof Response) {
        if (typeof WebAssembly.instantiateStreaming === 'function') {
            try {
                return await WebAssembly.instantiateStreaming(module, imports);

            } catch (e) {
                if (module.headers.get('Content-Type') != 'application/wasm') {
                    console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);

                } else {
                    throw e;
                }
            }
        }

        const bytes = await module.arrayBuffer();
        return await WebAssembly.instantiate(bytes, imports);

    } else {
        const instance = await WXWebAssembly.instantiate(module, imports);
        return instance;
    }
}

async function init(input) {
    
    const imports = {};
    imports.wbg = {};
    imports.wbg.__wbindgen_throw = function(arg0, arg1) {
        throw new Error(getStringFromWasm0(arg0, arg1));
    };


    const { instance, module } = await load(input, imports);

    wasm = instance.exports;
    init.__wbindgen_wasm_module = module;

    return wasm;
}

export default init;



相关推荐

蜗牛矿机改NAS后远程访问教程,花生壳盒子完美解决

今年不少烧友都在玩星际蜗牛的机器,,我也跟风入手一台,用星际蜗牛改装了一台NAS,在星际蜗牛上安装了多个版本的NAS系统,各方面都不错,就是远程访问功能实现不了。由于本地运营商不提供公网IP,就只能考...

不吹不黑,揭秘网工提升效率的7大神器!

作为一个网络工程师,在日常工作中肯定会使用许多方便的实用软件来提高效率,下面就简单介绍一下网络工程师常用的7种软件。01、FileZilla...

有线网间歇性断网?3个步骤帮你解决 90% 有线网中断问题

有线网络偶尔中断可能由硬件故障、网络设置、线路问题或外部干扰等多种因素导致。一、常见原因及验证方法1.硬件设备...

「这 25 年我被天气 PUA 的日常」

翻出手机相册里每年6月拍的天空,白到发亮的云层下总配着同一句文案:"热到裂开"。掐指一算被高温暴打的四分之一个世纪里,每个夏天都在上演《关于我和天气互相折磨的那些年》。2003年绝对...

数码爱好者必备工具:POWER-Z KM001C多功能测试仪

作为一名数码类爱好者,平时要测试手机、平板、充电器、充电宝等等电子产品,一款好用的测试工具尤为重要。近期,通过充电头网购入了一款POWER-ZKM001C多功能测试仪,主要用来平日里测试快充头和充电...

监控摄像头常用测试命令大全(摄像头测试项目)

以下是监控摄像头在Linux系统中常用的测试命令大全,涵盖设备检测、参数调整、视频录制、网络监控等方面,结合多个来源的信息整理而成:一、摄像头设备检测与调用1.查看摄像头设备①`ls/dev/v...

中级消防设施操作员考试-计算机基础知识学习笔记

消防设施操作员模块八计算机基础课程摘要消防设施操作员模块八主要介绍了计算机基础知识,包括计算机系统的组成和功能、硬件和软件、输入输出设备、外存储器、内存条、中央处理器、机箱等硬件部分,以及系统软件和应...

今日揭秘:上网行为监控软件是如何监控的?7个功能图文介绍

同事A:“哎,你们听说了吗?隔壁部门小王昨天上班刷短视频被领导抓包了!”同事B:“真的假的?公司不是没装摄像头吗?怎么知道的?”...

USB详细介绍(usb简介)

USB概念1.概念USB是通用串行总线(UniversalSerialBus),分为HOST/DEVICE两个角色,所有的数据传输都由主机主动发起,而设备只是被动的负责应答。例如,在读数据时,U...

程序员必备,Fiddler和spy-debugger的远程调试手机APP

背景笔者从事Web开发,不论是PC端还是APP端,调试抓包都是必不可少的环节,懂前端的人都知道,PC端调试非常方便,Chrome或者火狐等浏览器等都自带了非常方便且易于使用的开发者工具,便于我们抓包调...

通用无线网络破解抓包跑包教程(wifi抓包跑包教程)

由于很多的信号很强,但是后面都没有带WPS,怎么办呢,现在我给大家介绍一个简单的抓包跑字典的办法来解决这个难题,首先搜索信号,水滴,关注我的这个应该都会了吧!选择一个信号,点击启动,记住不是点...

抓包神器wireshark安装保姆级教程

简介当我们进行网络抓包时,我们通常需要借助其他的工具进行抓取,比如Charles,fiddler等,今天我们给大家介绍一款同样非常流行的抓包工具——wireshark,本文将介绍wireshark的安...

别让资料拖后腿!STM32开发‘作弊包’开源,工程师直呼内行!

一、开发环境与编译工具...

背完这套 Java 面试八股文,自动解锁面试牛逼症被动技能

前言国内的互联网面试,恐怕是现存的、最接近科举考试的制度。很多人对八股文都嗤之以鼻,认为无法衡量出一个程序员的真是水平。还有一部分人则是深恶痛绝,因为实在太难背了。但是国内大环境如此,互联网IT行...

混合云的多活架构指南(混合云架构图)

文/董晓聪吕亚霖在之前的《如何正确选择多云架构?》一文中介绍了混合云(广义的多云)的诸多架构以及各自的优势,本篇会重点来介绍下混合云下的多活架构。...