Source: WASMJSAPI.js

import TextConverter from "./TextConverter";

const DEBUG = false;

/**
 * WASMJSAPI class.
 * Creates a JS API from a WASM instance by demangling C++ function names.
 */
class WASMJSAPI {
    /**
     * WASMJSAPI constructor.
     * @param {Object} wasmInstance An instantiated WASM.
     * @param {Object} helperFunctions An object that provides these helper functions: malloc, free, memset, demangle.
     */
    constructor(wasmInstance, helperFunctions) {
        this.wasm = wasmInstance.exports;
        this.memory = this.wasm.memory.buffer;
        this.classes = {};
        this.wasm_malloc = helperFunctions.malloc;
        this.wasm_free = helperFunctions.free;
        this.wasm_memset = helperFunctions.memset;
        this.wasm_demangle = helperFunctions.demangle;

        let demangledCppFunctions = this._demangleCppFunctions(this.wasm);
        this._createJSClasses(demangledCppFunctions);
    }

    _createJSClasses(cppFunctions) {
        for (const [key, value] of Object.entries(cppFunctions)) {
            if (DEBUG) console.log("[SB] [WASMJSAPI] Processing " + key + "...");
            let indexOfFirstOpeningBracket = key.indexOf('(');
            if (indexOfFirstOpeningBracket == 0) continue;
            var str = key.substring(0, indexOfFirstOpeningBracket);
            var indexOfLastDoubleColon = str.lastIndexOf('::');
            if (indexOfLastDoubleColon == 0) continue;
            let methodName = str.substring(indexOfLastDoubleColon + 2);
            str = str.substring(0, indexOfLastDoubleColon);
            indexOfLastDoubleColon = str.lastIndexOf('::');
            if (indexOfLastDoubleColon == 0) continue;
            var className = str.substring(indexOfLastDoubleColon + 2);
            var template = "";
            var jsClassName = className;
            if (className.includes("<")) {
                let indexOfOpeningAngleBracket = className.indexOf('<');
                let indexOfClosingAngleBracket = className.indexOf('>');
                let classNameWoTemplate = className.substring(0, indexOfOpeningAngleBracket);
                template = className.substring(indexOfOpeningAngleBracket + 1, indexOfClosingAngleBracket);
                template = template[0].toUpperCase() + template.substring(1);
                className = classNameWoTemplate;
                jsClassName = classNameWoTemplate + template;
            }
            this._createJSClass(jsClassName);
            this._createMethod(className, template, jsClassName, methodName, value);
        }
    }

    _createJSClass(className) {
        if (this.classes[className] != null) {
            return;
        }
        if (DEBUG) console.log("[SB] [WASMJSAPI] Creating class " + className);
        let module = this;
        this.classes[className] = class {
            constructor() {
                if (DEBUG) console.log("[SB] [WASMJSAPI] " + className + " constructor called");
                let defaultObjectSize = 1024;
                var objectSize = 0;
                if (this.getObjectSize === undefined) {
                    objectSize = defaultObjectSize;
                    if (DEBUG) console.warn("[SB] [WASMJSAPI] Could not determine object size for " + className + ". Using default size (" + defaultObjectSize + ")");
                } else {
                    objectSize = this.getObjectSize();
                }
                let ptr = module.wasm_malloc(objectSize);
                if (DEBUG) console.log("[SB] [WASMJSAPI] memory allocated at " + ptr + " (" + objectSize + " bytes)")
                this.wasmMemAddress = ptr;
                let args = Array.from(arguments);
                this.init(args);
            }
            destruct() {
                if (DEBUG) console.log("[SB] [WASMJSAPI] " + className + " destructor called");
                if (this.deinit !== undefined) {
                    this.deinit();
                }
                module.wasm_free(this.wasmMemAddress);
            }
            static createWithPtr(memoryAddress) {
                let obj = Object.create(this.prototype);
                obj.wasmMemAddress = memoryAddress;
                return obj;
            }
        };
    }

    _convertArguments(args) {
        return args.map(arg => {
            if (typeof arg === "object") {
                if (arg.hasOwnProperty('wasmMemAddress')) {
                    return arg.wasmMemAddress;
                }
            }
            if (typeof arg === "string") {
                if (DEBUG) console.warn("[SB] [WASMJSAPI] string parameter needs to be converted to char*", arg);
            }
            return arg;
        })
    }

    _createMethod(className, template, jsClassName, methodName, wasmFunction) {
        let module = this;
        // console.log("[SB] [WASMJSAPI] Creating method " + jsClassName + "::" + methodName + " to call " + wasmFunction);
        var sbClass = this.classes[jsClassName];
        if (methodName == className) {
            if (sbClass.prototype.init !== undefined) {
                if (DEBUG) console.warn("[SB] [WASMJSAPI] Init method is already set for " + className);
            }
            sbClass.prototype.init = function (args) {
                if (DEBUG) console.log("[SB] [WASMJSAPI] Init called");
                args.unshift(this.wasmMemAddress);
                module.wasm[wasmFunction].apply(this, args);
            };
        } else if (methodName == "~" + className) {
            sbClass.prototype.deinit = function () {
                if (DEBUG) console.log("[SB] [WASMJSAPI] Deinit called");
                let args = Array.from(arguments);
                args.unshift(this.wasmMemAddress);
                module.wasm[wasmFunction].apply(this, args);
            }
        } else {
            if (sbClass.prototype[methodName] !== undefined) {
                if (DEBUG) console.warn("[SB] [WASMJSAPI] " + methodName + " method is already set for " + className);
            }
            let jsMethod = function () {
                // console.log("[SB] [WASMJSAPI] Called className " + className + " method " + jsClassName + " jsClassName" + methodName + " with arguments");
                let args = Array.from(arguments);
                let isStaticMethod = typeof this === "function";
                if (!isStaticMethod) {
                    args.unshift(this.wasmMemAddress);
                }
                args = module._convertArguments(args);
                let wasmResult = module.wasm[wasmFunction].apply(this, args);
                // console.log("[SB] [WASMJSAPI] WASM returned: " + wasmResult);
                return wasmResult;
            }
            sbClass.prototype[methodName] = jsMethod;
            sbClass[methodName] = jsMethod;
        }
    }

    _demangleCppFunctions(wasmInstanceExports) {
        let maxBytes = 1024;
        let inputBuffer = this.wasm_malloc(maxBytes);
        let outputBuffer = this.wasm_malloc(maxBytes);

        var demangledFunctions = {};
        for (const [key, value] of Object.entries(wasmInstanceExports)) {
            if (key.startsWith("_Z")) {
                // console.log("[SB] [WASMJSAPI] Demangling " + key + "...");
                this.wasm_memset(inputBuffer, 0, maxBytes);
                this.wasm_memset(outputBuffer, 0, maxBytes);
                TextConverter.toWASMString(this.memory, key, inputBuffer, maxBytes);
                let length = this.wasm_demangle(inputBuffer, outputBuffer);
                if (length == 0) {
                    if (DEBUG) console.warn("[SB] [WASMJSAPI] Could not demangle function: " + key);
                    continue;
                }
                let demangledFunction = TextConverter.toJSString(this.memory, outputBuffer, length);
                demangledFunctions[demangledFunction] = key;
                // console.log("[SB] [WASMJSAPI] Result: " + demangledFunction);
            }
        }
        this.wasm_free(inputBuffer);
        this.wasm_free(outputBuffer);
        return demangledFunctions;
    }

    /**
     * Converts a JS string to a WASM string.
     * @param {String} str The JS string.
     * @returns {Number} A pointer to the WASM string.
     */
    allocString(str) {
        let numberOfBytes = str.length + 1;
        let ptr = this.wasm_malloc(numberOfBytes);
        this.wasm_memset(ptr, 0, numberOfBytes);
        let result = TextConverter.toWASMString(this.memory, str, ptr, numberOfBytes);
        return ptr;
    }

    /**	
     * Writes bytes directly into the WASM memory and returns a pointer to its location.	
     * @param {Uint8Array} data The data to be written.	
     * @returns {Number} A pointer to the data.	
     */
    allocBytes(data, numberOfBytes) {
        let ptr = this.wasm_malloc(numberOfBytes);
        this.wasm_memset(ptr, 0, numberOfBytes);
        let wasmMemory = new Uint8Array(this.memory, ptr, numberOfBytes);
        wasmMemory.set(data);
        return ptr;
    }

    /**
     * Frees the memory at a given pointer.
     * @param {Number} ptr The pointer to the WASM memory.
     */
    freeMemory(ptr) {
        this.wasm_free(ptr);
    }
}

export default WASMJSAPI;