What Are Typed Arrays in WebAssembly?
Typed Arrays are arrays that allow you to work with binary data in a specific format, such as 8-bit integers or 32-bit floating point numbers. These arrays are important because WebAssembly works with raw binary data and memory and typed arrays help organize and manage that data efficiently.
- In WebAssembly: Typed arrays are not directly supported as data structures but are managed using linear memory. You can interact with WebAssembly memory using JavaScript’s TypedArray objects.
- In JavaScript: Typed arrays like Uint8Array, Int32Array and Float64Array allow you to access and manipulate memory buffers with specific byte-level precision.
Why Are Typed Arrays Important in WebAssembly?
- Efficiency:
- WebAssembly operates at a low level and data needs to be stored and retrieved from memory efficiently. Typed arrays provide this efficiency by directly mapping to fixed-size memory blocks.
- Memory Layout:
- In WebAssembly, memory is represented as a flat, contiguous block (known as linear memory). Typed arrays allow structured access to this memory, making it easier to manage data.
- Interaction with JavaScript:
- WebAssembly and JavaScript can exchange data using typed arrays. JavaScript typed arrays map directly to WebAssembly’s linear memory, allowing the two to share and manipulate data effectively.
Types of Typed Arrays in JavaScript
Before we jump into how WebAssembly interacts with these arrays, let’s review the common types of JavaScript typed arrays:
- Int8Array: 8-bit signed integer.
- Uint8Array: 8-bit unsigned integer.
- Int16Array: 16-bit signed integer.
- Uint16Array: 16-bit unsigned integer.
- Int32Array: 32-bit signed integer.
- Uint32Array: 32-bit unsigned integer.
- Float32Array: 32-bit floating point.
- Float64Array: 64-bit floating point.
Each typed array represents a specific kind of data, and each element in the array occupies a fixed number of bytes.
How Typed Arrays Work with WebAssembly
1. Creating WebAssembly Memory
WebAssembly requires linear memory, and we interact with this memory using typed arrays in JavaScript.
Here’s how to allocate and manage linear memory:
- Linear Memory: WebAssembly modules export a memory object, which is a contiguous block of memory. This memory can be shared and accessed by both JavaScript and WebAssembly code.
Example: Allocating WebAssembly Memory
In a WebAssembly module, you can define memory like this:
(module
(memory (export "memory") 1) ;; Defines 1 page (64 KiB) of memory
(export "getMemory" (func $getMemory))
)
The JavaScript code interacts with this memory through typed arrays:
// Fetch the WebAssembly module and instantiate it
(async () => {
const response = await fetch('module.wasm');
const buffer = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(buffer);
// Create a typed array that maps to WebAssembly memory
const memory = new Uint32Array(instance.exports.memory.buffer);
// Access and modify WebAssembly memory via the typed array
memory[0] = 42; // Set value at index 0 to 42
console.log(memory[0]); // Output: 42
})();
In the above example:
- A Uint32Array is created to map to WebAssembly’s linear memory.
- You access and manipulate memory directly using the memory object, which is backed by instance.exports.memory.
Example 1: Passing Data to WebAssembly Using Typed Arrays
Let’s walk through an example where we pass an array from JavaScript to WebAssembly, perform an operation in WebAssembly and then retrieve the result back in JavaScript.
WebAssembly Code (WAT):
(module
(memory (export "memory") 1) ;; Define 1 page of memory
(func $sumArray (param i32 i32) (result i32)
(local $i i32)
(local $sum i32)
(local.set $sum (i32.const 0))
(local.set $i (i32.const 0))
(loop $loop
local.get $i
local.get 1
i32.ge_u
if
local.get $sum
return
end
local.get 0
local.get $i
i32.add
i32.load
i32.add
local.set $sum
local.get $i
i32.const 1
i32.add
local.set $i
br $loop
)
)
(export "sumArray" (func $sumArray))
)
JavaScript Code:
(async () => {
const response = await fetch('module.wasm');
const buffer = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(buffer);
const memory = new Uint32Array(instance.exports.memory.buffer);
// Write an array into the WebAssembly memory
memory.set([10, 20, 30, 40, 50], 0); // Write at address 0
// Call WebAssembly function to sum the array
const sumArray = instance.exports.sumArray;
console.log("Sum of array:", sumArray(0, 5)); // Output: 150
})();
Explanation:
- The sumArray function in WebAssembly adds values from the array stored in WebAssembly memory.
- The array is passed to WebAssembly through the Uint32Array, which represents memory in JavaScript.
- The sum is calculated and returned back to JavaScript.
Example 2: WebAssembly Typed Arrays for Image Processing
Let’s consider a more practical use case—processing pixel data from an image in WebAssembly.
JavaScript Example: Manipulating Image Pixels
// Create an image object and load the image
const img = new Image();
img.src = 'image.jpg';
img.onload = async () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Draw image on canvas and get image data
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
// Get pixel data as Uint8Array
const imageData = ctx.getImageData(0, 0, img.width, img.height);
const pixels = new Uint8Array(imageData.data.buffer); // Extract pixel data buffer
// Pass pixel data to WebAssembly for processing
const response = await fetch('process_image.wasm');
const wasmBuffer = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(wasmBuffer);
// Create a typed array for WebAssembly to manipulate the pixel data
const memory = new Uint8Array(instance.exports.memory.buffer);
memory.set(pixels, 0); // Copy pixel data to WebAssembly memory
// Call WebAssembly function to manipulate image (invert colors, for example)
const processImage = instance.exports.processImage;
processImage(0, img.width, img.height);
// Retrieve processed pixel data back from WebAssembly memory
const processedPixels = memory.slice(0, pixels.length);
// Put processed image data back on the canvas
imageData.data.set(processedPixels);
ctx.putImageData(imageData, 0, 0);
};