Wechat applet support for WebAssembly
Wechat Applets base library version from 2.13.0, through WXWebAssembly objects to support the integrated WASM package.
WXWebAssembly
WXWebAssembly is similar to Web standard WebAssembly and can improve the performance of small programs to a certain extent.
Starting with the base library V2.13.0, applets can access and use WXWebAssembly objects globally.
Starting with the base library v2.15.0, applets support using WXWebAssembly within workers.
WXWebAssembly.instantiate(path, imports)
Similar to standard WebAssembly.instantiate, except that the first argument only takes a string path to the package, pointing to the.wasm file within the package
Similarities and differences with WebAssembly
- WXWebAssembly. Instantiate (path, imports) method and path for code package path (support. Wasm and wasm. Br suffix)
- Support WXWebAssembly. The Memory
- Support WXWebAssembly. Table
- Support WXWebAssembly. Global
- Export supports functions, Memory, and Table, while iOS does not support Global
Wechat official only provides the WXWebAssebly object as the interface to load wASM files. Our WASM package is packaged by wASM-pack compilation, which is usually similar to wASM package packaged by wASM-pack or EMCC tools. In addition to the WASM file, glue code is provided for the front-end code to interact with the WASM back end, to convert data formats, and to initialize the WASM file by communicating with memory addresses. Therefore, when we refer to the official wASM-Pack document, we need to make some modifications to the glue file because the initialization interface provided by wechat is inconsistent with MDN
Wasm-pack Import mode on the Web side
When we use
wasm-pack build --target web
Copy the code
When the command is compiled and packaged, an output file structure looks like this:
- Two of the.d.ts files we are familiar with are the ts type declaration files
- The.js file is the glue file that the front-end application interacts with the WASM file
- The. Wasm file is the WASM binary file
The following code is described in the WASM-pack documentation to introduce its modules
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();Copy the code
The glue js file exposes a module, which contains an init method to initialize the WASM module, and other methods to expose the WASM module
If we load the WASM module directly in a applet using the same method, the following exception occurs
SyntaxError: Cannot use 'import.meta' outside a module
Unhandled promise rejection Error: module "XXX" is not defined
Copy the code
Modify the WebAssembly import mode
Of the exceptions mentioned at the end of the previous section, the first is more common. We saw that the init function in the glue file generated by WASM-pack used the import.meta attribute
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); }Copy the code
The error message indicates that the import.meta meta attribute can only be called inside the module. This code is fine in the browser environment, but it will report an error in the applet environment, perhaps because the applet environment does not have enough ESM support.
In summary, we can see that what this code means is that the fetch is then used to download the remote WASM file and then initialize the WASM file by calling other methods.
The documentation of the applet clearly states:
WXWebAssembly.instantiate(path, imports)
Similar to standard WebAssembly.instantiate, except that the first argument only takes a string path to the package, pointing to the.wasm file within the package
Therefore, when using the applet initialization function, the WASM file is packaged in the applet application package, so there is no need to consider downloading the WASM file.
Therefore, we delete the relevant code in the init function and change the init function to:
Async function init(input) {if (typeof input === 'undefined') {input = new URL('ron_weasley_bg.wasm', '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 we pass the absolute path to the wASM file directly, The following is used to determine whether you want to generate a fetch the object code also was not used to delete the following comments of code 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); Const {instance, module} = await load(input, imports); wasm = instance.exports; init.__wbindgen_wasm_module = module; return wasm; }Copy the code
Next, we try to reference the WASM module init method in the applet’s Page file:
onLoad: async function (options) {
await init('/pages/main/pkg/ron_weasley_bg.wasm');
}
Copy the code
An error occurs
VM409 WAService.js:2 Unhandled promise rejection ReferenceError: WebAssembly is not defined
Copy the code
Modify the wASM initialization call method
The exception at the end of the previous section makes it clear that we just need to find the reference to WebAssembly in the glue file and replace it with WXWebAssembly.
All references to WebAssembly in the glue file appear in 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;
}
}
}
Copy the code
Since the module argument we pass is the absolute path to the WASM file, it must not be of Response type, so let’s ignore the forward branch of the if function and take a closer look at the else branch
Instantiate (module, imports); // Instantiate (module, imports); if (instance instanceof WebAssembly.Instance) { return { instance, module }; } else { return instance; }Copy the code
The modified else branch looks like this
const instance = await WXWebAssembly.instantiate(module, imports);
if (instance instanceof WXWebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
Copy the code
Refresh the small program development tool, no longer reported exceptions. Next we call the XXX method in WASM.
import init, { xxx } from './pkg/ron_weasley'
Page({
onLoad: async function (options) {
await init('/pages/main/pkg/xxx.wasm');
console.log(xxx('1111', '2222'))
}
})
Copy the code
The applets development tool executed correctly and returned the correct value. That’s very good. So I am very comfortable in the real machine also came to a test, the anomaly is as follows:
ReferenceError: Can't find variable: TextDecoder
Copy the code
Small program TextEncoder & TextDecoder
Search the glue file and find that TextEncoder and TextDecoder are used to convert the UInt8Array to JS String.
All modern browsers have implemented both classes in web standards, but the neutered applet environment doesn’t. If the UInt8Array and JS String cannot be converted to and from each other, it means that JS can call functions of the WASM module but cannot pass values, and the values returned by the WASM module after execution cannot be passed to JS for use.
- Idea 1: hand out a set of conversion code. Yes, but the ability to cover all cases, and robustness, is a concern
- Since it is the ability of modern browsers to achieve, then there must be polyfill, online search
MDN recommended polyfill giant packets is a name, called: FastestSmallestTextEncoderDecoder
Github is here: github.com/anonyco/Fas…
We import it into the glue file and assign it to the TextEncoder & TextDecoder inside the module
require('.. /.. /.. /utils/EncoderDecoderTogether.min') const TextDecoder = global.TextDecoder; const TextEncoder = global.TextEncoder;Copy the code
Run again and report an exception:
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)Copy the code
As you can see, the call to the textDecoder. decode method in EncoderDecoderTogether raised the exception. Look for a line of code in the glue file
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
Copy the code
The following line of code calls the decode method, but the argument is empty, raising the length of undefined exception.
Continue to report exceptions after deletion:
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)Copy the code
Searching in the issue of Github warehouse, I found that someone reported that when decode was called, there would be inaccurate offset in the library when slice of Uint8Array’s buffer. Uint8Array = Uint8Array = Uint8Array
var str = String.fromCharCode.apply(null, uint8Arr);
Copy the code
Refer to the answer: stackoverflow.com/a/19102224
Other answers to this question also discuss the option of reading bloB data and then transforming it
In addition, if the uint8arr data volume is too large, stack overflow will occur in the case of the String.fromCharCode method, we can optimize the scheme of piecewise transformation of the Uint8arr
If you are interested can read the question stackoverflow.com/questions/8…
Next, we use to replace a part of the FastestSmallestTextEncoderDecoder TextDecoder:
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); Function getStringFromWasm0(PTR, len) {return cachedTextdecoder.decode (getUint8Memory0().subarray(PTR, len); ptr + len)); / / replace}Copy the code
The relevant code after modification is
function getStringFromWasm0(ptr, len) {
return String.fromCharCode.apply(null, getUint8Memory0().subarray(ptr, ptr + len))
}
Copy the code
Run the small program development tool again, there is no problem, and then look at the real machine, sure enough, or abnormal:
MiniProgramError Right hand side of instanceof is not a object
Copy the code
Instance property of WXWebAssembly
Remember a few sections ago when we replaced WebAssembly with WXWebAssembly?
The exception still appears in the else branch of the load function
const instance = await WXWebAssembly.instantiate(module, imports); If (instance instanceof wxwebassembly.instance) {return {instance, module}; } else { return instance; }Copy the code
Debug the code to find the else branch. Take a look at the document:
Instance Instances Webassembly. instance is true when wASM is initialized using the instance method
If the instantiate method is instantiated with false, delete the instantiate method and return instance.
After modification, the development tool and the real machine do not report the error, is accomplished.
The complete code
The modified diff list is as follows:
1, 3 d0 < the 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)); 131 < 124125 c124, 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); >} 136 a145, 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);Copy the code
Modified glue file:
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;Copy the code