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

  1. WXWebAssembly. Instantiate (path, imports) method and path for code package path (support. Wasm and wasm. Br suffix)
  2. Support WXWebAssembly. The Memory
  3. Support WXWebAssembly. Table
  4. Support WXWebAssembly. Global
  5. 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