How to use WASM in JavaScript

What is WebAssembly (Wasm)?

Wasm, an abbreviation for WebAssembly, serves as the binary instruction format utilized by compilers to convert high-level programming languages such as C, Rust, and C++ into low-level machine code that can be executed by the browser or any target environment you are compiling for. By allowing developers to execute code written in these languages within the web browser at speeds approaching that of native applications, it broadens the accessibility of web application development. In contrast to JavaScript, which undergoes interpretation or just-in-time (JIT) compilation, Wasm is precompiled into a compact binary format. This advance facilitates rapid loading and execution within the JavaScript engine of contemporary browsers.

Wasm serves as a portable, secure, and efficient binary instruction set for a compact virtual stack machine. Its primary objective is to deliver a platform-agnostic, adaptable, and high-performance runtime environment suitable for various host contexts, including web browsers, Node.js, and even embedded systems. Consequently, Wasm represents an exceptional advancement for web developers seeking performance improvements, especially in resource-intensive operations such as gaming, video processing, machine learning, and cryptographic applications.

Why was WebAssembly Created?

Historically, JavaScript was the sole language utilized for client-side scripting in web browsers. While the performance and functionality of JavaScript have significantly improved, it was not originally designed to handle certain resource-intensive tasks, particularly those that require rapid execution. Solutions such as asm.js emerged to address this shortcoming by providing a means to enhance JavaScript code optimization; however, they came with their own set of constraints:

JavaScript Overhead: As a dynamic and interpreted language, JavaScript incurs a certain level of performance overhead. Although its runtime characteristics, including just-in-time (JIT) compilation and dynamic typing, can enhance productivity in numerous scenarios, they can also result in inefficiencies, especially when dealing with CPU-intensive operations like intricate mathematical calculations or graphics rendering. This overhead restricts its effectiveness in managing applications that are critical to performance.

Restricted Language Compatibility: Additionally, JavaScript is presently the sole language that enjoys support from web browsers. Consequently, developers find themselves constrained when it comes to utilizing lower-level programming languages such as C, C++, and Rust. These languages offer superior control over memory and system resources, which provides them with a competitive advantage in scenarios where optimal performance is essential.

Code Portability: Both challenges are addressed by WebAssembly (Wasm), a versatile, low-level bytecode format that is executable on any platform equipped with a compatible runtime. This includes web browsers and Node.js environments, where it can operate at speeds close to that of native code, executed directly from the binary produced after compiling languages such as C/C++ or Rust.

How Does WebAssembly Work?

WebAssembly (Wasm) serves as a compiled code format designed for execution within a web context, delivering high-performance results as defined by an extension of the ECMAScript Language specifications. It offers rapid execution capabilities and can operate in any environment that supports a compatible runtime, such as web browsers and Node.js.

The process involves three key steps:

Compilation: Subsequently, by utilizing tools like Emscripten (for C/C++) or wasm_pack (for Rust), we translate high-level source code into WebAssembly bytecode. The result of this process is a .wasm file, which is more compact than JavaScript, making it quicker to parse for scenarios where performance is crucial.

Instantiation: After the .wasm file is loaded, it is transferred to the browser or a host environment. The WebAssembly module is then instantiated, linking any necessary imports (such as JavaScript functions and browser APIs). At this point, we are situated in the environment for which the Wasm module was designed to operate.

Execution: Once the WebAssembly functions have been instantiated, they can be invoked directly from JavaScript. This capability allows WebAssembly to manage various tasks within the browser, including gaming, image processing, and machine learning.

Key features of WebAssembly

  • Portability The point of Wasm is that it's all completely platform independent. Wasm binary file can run on any device or operating system that has a compatible runtime. It's for modern web browsers ( Chrome , Firefox , Edge , Safari ) and environments like Node.js. Not only does this abstract away the hardware and platform-specific details, but it allows compiled code to run on almost any structure, which makes web assembly great for cross-platform apps.
  • Performance WebAssembly is binary, by design, for speed. They are smaller than equivalent JavaScript files, which means faster download times. Additionally, the browser's engine can parse and execute the low level, statically typed instructions of Wasm rapidly. Wasm code is precompiled, and it's much faster than JavaScript's just-in-time (JIT) compilation. Running it makes it an excellent workload for CPU -demanding jobs such as gaming, machine learning, video processing, and all types of performance-critical applications, delivering almost native performance.
  • Security Just like JavaScript, WebAssembly runs in a sandboxed, secure environment. This design guarantees that Wasm code can't directly interact with the host system, and you can't let them do any malicious things. Using a linear memory model and boundary checks to prevent buffer overflows and memory corruption, it has strict memory safety guarantees. As a default, Wasm modules have little access to system resources, which reduces the attack surface and thus makes Wasm safe to use in web applications.
  • Interoperability While calling WebAssembly functions from JavaScript is possible in Wasm, it is not transactional (so it doesn't work the other way around). This bi-directional interaction allows developers to use JavaScript for UI, DOM manipulation, and events while taking advantage of Wasm's performance in the performance-critical parts of an application. This makes Wasm easy to adopt incrementally because it can work with existing JavaScript code and remains easy for projects that already depend on JavaScript.
  • Approach-1: Using Fetch API and WebAssembly.instantiate

We utilize the Fetch API in conjunction with WebAssembly.instantiate to enable us to access the compiled code for extraction purposes within our web browser.

Loading and executing a WebAssembly (.wasm) module in JavaScript is quite straightforward through the use of the Fetch API in conjunction with WebAssembly.instantiate. I believe this method is particularly beneficial for newcomers and minor projects, as it is easy to understand and allows you to directly supply your data.

The Fetch API is a contemporary JavaScript capability that allows you to perform HTTP requests to retrieve resources (such as .wasm files) in an asynchronous manner. Its primary application is to access resources from a server without the need to reload the web page.

In JavaScript, the WebAssembly API is available, and among its features is the WebAssembly.instantiate method. This method takes a .wasm binary code provided as an ArrayBuffer and an optional import object. It compiles and instantiates the code, ultimately returning an instance that includes exported functions, which can be invoked directly from JavaScript.

C/C++ Code (Wasm Module)

Initially, let's consider that you possess the subsequent C/C++ code, which you intend to compile into WebAssembly (module.wasm):

Example

// math_operations.c (to be compiled to WebAssembly)
int add(int a, int b) {
    return a + b;
}
int subtract(int a, int b) {
    return a - b;
}
int multiply(int a, int b) {
    return a * b;
}
int divide(int a, int b) {
    if (b == 0) return 0; // Prevent division by zero
    return a / b;
}

Compile this code using a tool like Emscripten:

Example

emcc math_operations.c -s WASM=1 -o module.wasm

JavaScript code:

Example

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Wasm Calculator</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            margin-top: 50px;
        }
        input {
            width: 200px;
            margin: 5px;
            padding: 10px;
            font-size: 16px;
        }
        button {
            width: 100px;
            padding: 10px;
            margin: 5px;
            font-size: 16px;
        }
        .result {
            margin-top: 20px;
            font-size: 20px;
        }
    </style>
</head>
<body>
    <h1>Wasm Calculator</h1>
    <input type="number" id="num1" placeholder="Enter first number">
    <input type="number" id="num2" placeholder="Enter second number">
    <div>
        <button id="addBtn">Add</button>
        <button id="subBtn">Subtract</button>
        <button id="mulBtn">Multiply</button>
        <button id="divBtn">Divide</button>
    </div>
    <div class="result" id="result">Result: </div>
    <script>
        // Load and instantiate the WebAssembly module
        let wasmInstance;

        async function loadWasm() {
            try {
                // Step 1: Fetch the .wasm file
                const response = await fetch('module.wasm');
                if (!response.ok) throw new Error(`Failed to fetch module.wasm: ${response.statusText}`);
                // Step 2: Convert the response to an ArrayBuffer
                const bytes = await response.arrayBuffer();
                // Step 3: Instantiate the WebAssembly module
                const { instance } = await WebAssembly.instantiate(bytes);
                wasmInstance = instance;
                console.log('WebAssembly module loaded successfully.');
                // Enable buttons once Wasm is loaded
                document.querySelectorAll('button').forEach(btn => btn.disabled = false);
            } catch (error) {
                console.error('Error loading WebAssembly:', error);
                document.getElementById('result').textContent = 'Error loading WebAssembly module.';
            }
        }
        // Helper function to get input values
        function getInputValues() {
            const num1 = parseInt(document.getElementById('num1').value, 10);
            const num2 = parseInt(document.getElementById('num2').value, 10);
            return [num1, num2];
        }
        // Event handlers for calculator operations
        document.getElementById('addBtn').addEventListener('click', () => {
            const [num1, num2] = getInputValues();
            const result = wasmInstance.exports.add(num1, num2);
            document.getElementById('result').textContent = `Result: ${result}`;
        });
        document.getElementById('subBtn').addEventListener('click', () => {
            const [num1, num2] = getInputValues();
            const result = wasmInstance.exports.subtract(num1, num2);
            document.getElementById('result').textContent = `Result: ${result}`;
        });
        document.getElementById('mulBtn').addEventListener('click', () => {
            const [num1, num2] = getInputValues();
            const result = wasmInstance.exports.multiply(num1, num2);
            document.getElementById('result').textContent = `Result: ${result}`;
        });
        document.getElementById('divBtn').addEventListener('click', () => {
            const [num1, num2] = getInputValues();
            const result = wasmInstance.exports.divide(num1, num2);
            document.getElementById('result').textContent = num2 === 0 ? 'Error: Division by zero' : `Result: ${result}`;
        });
        // Disable buttons initially until Wasm is loaded
        document.querySelectorAll('button').forEach(btn => btn.disabled = true);
        // Load the WebAssembly module
        loadWasm();
    </script>
</body>
</html>

Explanation:

The WebAssembly binary is embedded directly within the JavaScript code as a Base64 encoded string. Consequently, there is no requirement for a separate .wasm file. The function base64ToArrayBuffer accepts the Base64 string and converts it into an ArrayBuffer, which can subsequently be utilized to instantiate this WebAssembly module. The loadWasm function processes the Base64 encoded Binary Wasm, invoking wasmInstance by transforming the data and saving it in wasmInstance.

There exist four WebAssembly functions named add, subtract, multiply, and divide. Each function is invoked from JavaScript when the respective buttons are selected.

Users have the capability to input numerical values and perform arithmetic operations using the HTML user interface. The outcomes are presented in a designated result division.

Approach-2: Using WebAssembly.instantiateStreaming

WebAssembly.instantiateStreaming is a contemporary API designed to simplify the process of loading and compiling WebAssembly (Wasm) modules. The method of instantiation through streaming differs from WebAssembly.instantiate, which necessitates the retrieval of the WebAssembly binary prior to the compilation of the program. In contrast, with instantiateStreaming, the compilation of the WebAssembly module occurs concurrently as it is being downloaded.

This becomes especially beneficial when dealing with extensive modules, as the module can begin its execution immediately once a sufficient amount has been retrieved and compiled, thereby reducing the total loading duration.

Steps to Use WebAssembly.instantiateStreaming

Retrieving the WebAssembly Binary: The initial step involves obtaining the WebAssembly module from a server, which is commonly done via a fetch request. This process downloads the binary file, which usually has a .wasm extension, from a specified URL.

Streaming WebAssembly Binaries: Utilizing instantiateStreaming, the .wasm file can be handled in segments rather than requiring complete download before processing. As the data streams in, it gets compiled immediately, thus accelerating the overall workflow. A significant advantage of this method is that the WebAssembly module becomes ready for execution more swiftly compared to the conventional download-then-compile method.

Creating an Instance of the Module: After a module has been both streamed and compiled, it is subsequently instantiated as a JavaScript object, allowing for interaction. When necessary, you can invoke the exported functions from the WebAssembly module that you have instantiated, using JavaScript.

WebAssembly.instantiateStreaming merges the processes of fetching and compiling, providing a swift and efficient method for utilizing WebAssembly modules, especially in the case of sizable files or applications that require rapid initialization. However, for this functionality to operate correctly, the .wasm file needs to be delivered with the appropriate MIME type (such as application/wasm). Given that contemporary browsers endorse this method, it has become a popular choice for enhancing performance compared to the traditional method.

Program:

Example

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Wasm with instantiateStreaming</title>
</head>
<body>
    <h1>Simple Wasm Example with instantiateStreaming</h1>
    <button id="loadBtn">Load Wasm</button>
    <div id="output">Result: </div>
    <script>
        // Step 1: Define a URL to the Wasm binary
        const wasmUrl = "module.wasm"; // Make sure this path is correct
        // Step 2: Function to load Wasm using instantiateStreaming()
        async function loadWasm() {
            try {
                // Fetch the Wasm module as a stream and instantiate it
                const response = await fetch(wasmUrl);
                const wasmModule = await WebAssembly.instantiateStreaming(response);
                // Access exported functions
                const { add, subtract } = wasmModule.instance.exports;
                // Example usage of exported functions
                const resultAdd = add(10, 20);
                const resultSubtract = subtract(30, 10);

                // Display the results in the DOM
                document.getElementById("output").textContent = `Add: ${resultAdd}, Subtract: ${resultSubtract}`;
            } catch (error) {
                console.error("Failed to load Wasm:", error);
            }
        }
        // Attach event listener to the button
        document.getElementById("loadBtn").addEventListener("click", loadWasm);
    </script>
</body>
</html>

Explanation:

instantiateStreaming: This method constructs the .wasm module by fetching and compiling it in real-time as the file is received from the server. This approach improves the loading efficiency in contrast to retrieving the entire file in a single download.

Wasm Module: We create JavaScript code that subsequently invokes the exported functions (such as add and subtract) from the JavaScript code, following the processes of retrieving, instantiating, and utilizing the WebAssembly binary.

Approach-3: Using WebAssembly.compile and WebAssembly.instantiate Separately

Compile the WebAssembly Module: The initial step involves transforming the .wasm binary into an in-memory WebAssembly.Module object. This process is achieved through the WebAssembly.compile function, which accepts binary data (such as an ArrayBuffer containing the .wasm file) and translates it into a module format comprehensible to the WebAssembly runtime. However, at this stage, the compiled module remains non-executable.

Creating an Instance of the Module: Following the compilation of the module, the subsequent action is to create an instance through the WebAssembly.instantiate method. This procedure generates an instance of the module, which encompasses memory, imports, and functions. The instantiation process connects the compiled module to any essential imports (such as JavaScript functions or additional WebAssembly modules) and readies it for execution. During this instantiation phase, you also have the opportunity to manage particular configuration or setup tasks, which may include initializing memory or specifying imports.

This methodology provides enhanced flexibility by distinguishing between the steps of compilation and instantiation. For example, it allows for the compilation of a module to occur once while enabling multiple instantiations, or it permits further configuration prior to execution. Additionally, this approach facilitates more accurate error management and optimization techniques, particularly when dealing with large or intricate modules. This strategy proves beneficial in scenarios where you require control over the instantiation process or wish to precompile a module prior to its utilization, thereby ensuring efficient management of resources and enhanced performance.

Program:

Example

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Wasm with compile() and instantiate()</title>
</head>
<body>
    <h1>Wasm Example with compile() and instantiate()</h1>
    <button id="loadBtn">Load Wasm</button>
    <div id="output">Result: </div>
    <script>
        // URL to the Wasm binary
        const wasmUrl = 'module.wasm'; // Ensure correct path to Wasm file
        // Function to load Wasm using compile() and instantiate()
        async function loadWasm() {
            try {
                // Step 1: Fetch the Wasm binary
                const response = await fetch(wasmUrl);
                const wasmArrayBuffer = await response.arrayBuffer();
                // Step 2: Compile the Wasm binary
                const compiledModule = await WebAssembly.compile(wasmArrayBuffer);
                // Step 3: Instantiate the module
                const { instance } = await WebAssembly.instantiate(compiledModule);
                // Access the exported functions
                const { add, subtract } = instance.exports;
                // Example usage of exported functions
                const resultAdd = add(10, 20);
                const resultSubtract = subtract(30, 10);
                // Display the result in the DOM
                document.getElementById('output').textContent = `Add: ${resultAdd}, Subtract: ${resultSubtract}`;
            } catch (error) {
                console.error('Failed to load WebAssembly:', error);
            }
        }
        // Attach event listener to the button
        document.getElementById('loadBtn').addEventListener('click', loadWasm);
    </script>
</body>
</html>

Explanation:

Acquiring the WebAssembly Binary: The process begins by obtaining the WebAssembly binary from the server, which is commonly a .wasm file. This Wasm file is acquired as an ArrayBuffer from the designated URL (wasmUrl) through the use of the get API.

Compiling the WebAssembly Module: After the binary has been obtained, the ArrayBuffer is supplied as an argument to the WebAssembly.compile function. This method compiles the binary into an in-memory WebAssembly.Module object. However, at this stage, the module is not yet ready for execution.

Creating an Instance of the Module: Following the compilation of the module, it is necessary to instantiate it through the WebAssembly.instantiate method. During this process, a WebAssembly instance is generated, which includes memory, imports, and exported functions. The functions that are exported from the Wasm module can be accessed via the instance.exports object. In this context, arithmetic calculations are performed utilizing the add and subtract functions.

Utilizing WebAssembly Functions: Once the WebAssembly module has been instantiated, JavaScript invokes the add and subtract functions. The outcomes of these addition and subtraction operations are displayed in the HTML element with the ID output.

Benefits of Using WebAssembly.compile and WebAssembly.instantiate Separately

Separation of Compilation and Instantiation

This method allows for greater management of the two phases. To optimize resource utilization and reduce the need for repeated compilations, developers have the option to compile the WebAssembly module a single time and then instantiate it multiple times.

Better Error Handling

Developers can enhance their debugging and problem-solving capabilities by distinguishing between compilation and instantiation. This separation facilitates the identification of errors at each individual phase.

Resource Management Utilizing this approach enables more efficient management of memory and other resources. By compiling a module just a single time, you can leverage it across various sections of the application without the necessity of reloading or recompiling it, thereby improving performance in applications that require significant resources.

Input Required

This code uses input(). Please provide values below: