网站首页 > 技术文章 正文
微信小程序对于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 的异同
- WXWebAssembly.instantiate(path, imports) 方法,path为代码包内路径(支持.wasm和.wasm.br后缀)
- 支持 WXWebAssembly.Memory
- 支持 WXWebAssembly.Table
- 支持 WXWebAssembly.Global
- 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 <anonymous>:1148:7
at doWhenAllScriptLoaded (<anonymous>:1211:21)
at Object.scriptLoaded (<anonymous>:1239:5)
at Object.<anonymous> (<anonymous>: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.<computed> [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;
6a4,6
> let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
>
> cachedTextDecoder.decode();
17c17
< return String.fromCharCode.apply(null, getUint8Memory0().subarray(ptr, 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 } = await load(input, 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;
- 上一篇: 15 张精美动图全面讲解 CORS 400种动图
- 下一篇: 涨知识:HTTP 规范中的暗坑知多少?
猜你喜欢
- 2024-11-14 每个JavaScript开发人员都应该了解的编程概念
- 2024-11-14 Next.js Api Router 数据处理 router next参数
- 2024-11-14 2023 黑马前端就业版最新线下课程(8月版本)
- 2024-11-14 23 个超实用 JS 技巧 js简明教程
- 2024-11-14 HTTP 规范中的那些暗坑 http+规范中的那些暗坑有哪些
- 2024-11-14 使用 JS 操作 HTML 元素 用js写html
- 2024-11-14 一面 2:JS-Web-API 知识点与高频考题解析
- 2024-11-14 MutationObserver 都被传烂了,你怎么到现在都还不会?
- 2024-11-14 无名前端库 无名前端库是什么
- 2024-11-14 可观测系统如何识别网站有多少文件命中了缓存?
- 标签列表
-
- content-disposition (47)
- nth-child (56)
- math.pow (44)
- 原型和原型链 (63)
- canvas mdn (36)
- css @media (49)
- promise mdn (39)
- readasdataurl (52)
- if-modified-since (49)
- css ::after (50)
- border-image-slice (40)
- flex mdn (37)
- .join (41)
- function.apply (60)
- input type number (64)
- weakmap (62)
- js arguments (45)
- js delete方法 (61)
- blob type (44)
- math.max.apply (51)
- js (44)
- firefox 3 (47)
- cssbox-sizing (52)
- js删除 (49)
- js for continue (56)
- 最新留言
-