The Art of WebAssembly
There are two primary styles of WAT coding to choose from. One style is the linear instruction style list. The other coding style is called S-Expressions.
Unserstanding WAT: MDN Docs
Stack Machines
For example, local.get is defined to push the value of the local it read onto the stack, and i32.add pops two i32 values (it implicitly grabs the previous two values pushed onto the stack), computes their sum and pushes the resulting i32 value
When a function is called, it starts with an empty stack which is gradually filled up and emptied as the body’s instructions are executed. So for example, after executing the following function:
(func (param $p i32)
(result i32)
local.get $p
local.get $p
i32.add
)
The stack contains exactly one i32 value – the result of the expression ($p + $p), which is handled by i32.add. The return value of a function is just the final value left on the stack
LISP expressions are called symbolic expressions or s-expressions. The s-expressions are composed of three valid objects, atoms, lists, and strings
JavaScript treats all numbers as 64-bit floating-point numbers.
When you call a JavaScript function from WebAssembly, the JavaScript engine will perform an implicit conversion to a 64-bit float, no matter what data type you pass
Addition, subtraction, and multiplication typically perform three to five times faster with integers, Dividing by powers of two is also several times faster. However division by anything but a power of two can be faster with floating-point numbers.
The value stored in local variables and parameters are pushed onto the stack with the local.get expression.
WAT doesn’t require you to name your variables and functions. Instead, you can use index numbers to reference functions and variables that you haven’t yet named.
Code that calls local.get followed by a number is retrieving a local variable based on the order it appears in the WebAssembly code. A convenient part of this code style is that you can declare multiple parameters in a single expression by adding more types.
(module
(func (export "AddInt")
(param i32 i32)
(result i32)
local.get 0
local.get 1
i32.add
)
)
if/else Conditional Logic
One way that WAT differs from an assembly language is that it contains some higher-level control flow statements, such as if and else. WebAssembly doesn’t have a boolean type; instead, it uses i32 values to represent booleans.
All floating-point nubmers are signed and have a dedicated sing bit.
Loops and Blocks
The branching expressions in WAT are different than branching statements you might find in an assembly language. The differences prevent the spaghetti code that comes about as the result of jumps to arbitrary locations. If you want your code to jump backward, you must put your code inside a loop. If you want your code to jump to forward, you must put it inside a block. For the kind of functionality you would see in a high-level programming language, you must use the loop and block statements together.
Declaring globals in WebAssembly
WebAssembly has the ability to create global instances, accessible from both JavaScript and importable/exportable across one or more WebAssembly.Module instances. This is very useful, as it allows dynamic linking of multiple modules.
(module
(global $g (import "js" "global") (mut i32)
(func (export "getGlobal") (result i32)
(global.get $g)
)
(func (export "inGlobal")
(global.set $g
(i32.add (global.get $g) (i32.const 1))
)
)
)
To create an equivalent value using JavaScript, you’d use the WebAssembly.Global() constructor:
const global = new WebAssembly.Global({ value: 'i32', mutable: true }, 0);
Functions and Tables
Function calls will always result in some lost computing cycles. But it’s necessary to know that a WebAssembly module will lose more cycles when calling an imported JavaScript function than when calling a function defined inside your WebAssembly module.
Unfortunately, only numbers can be passed as parameters to JavaScript functions from WebAssembly.
WebAssembly can pass three of the four main data types back to functions imported from JavaScript
- 32-bit integers
- 32-bit floating-point numbers
- 64-bit floating-point numbers
If you pass a 32-bit integer or floating-point number to JavaScript, JavaScript converts it to a 64-bit float, which is the native JavaScript number type.
When you call a JavaScript function in WAT, you lose some cycles to overhead. This number isn’t extremely large, but if you execute an external JavaScript function in a loop that iterates 4,000,000 times, it can add up.
Function Tables
JavaScript can set variables to function, allowing an application to dynamically swap functions at runtime. WebAssembly doesn’t have this feature, but it does have tables, which can hold functions. For that reasons, we’ll refer to them as function tables. Function tables allow WebAssembly to dynamically swap functions at runtime, which allows compilers to support features such as function pointers and OOP virtual functions. Currently, tables only support the anyfunc type(anyfunc is a genetic WebAssembly function type), but int the future they might support JavaScript objects and DOM elements as well.
Tables are basically resizable arrays of references that can be access by index from WebAssembly code.
Low-Level Bit Manipulation
You can store integers and floating-point numbers in variables and in linear memory. Linear-memory is like an typed, unsigned-integer array.
The i64 data type in WAT doesn’t specify whether it’s signed or unsigned when the variable is declared. Instead, WAT has to choose operations to perform on that data based on whether the user wants to treat the number as signed or unsigned.
One problem with i64 data types is that you cannot directly move 64bit integers back and forth between WebAssembly and JavaScript
Linear Memory
Linear memory acts as one giant array of data that can be shared between WebAssembly and JavaScript.
The stack-allocated local variables are released from memory as soon as the function finishes executing.
The stack works great for local variables. However, one limitation in WAT is that local vairables that use the stack can only be one of four bytes, all of which are numeric. Sometimes, you might require more sophisticated data structures, such as strings, structures, arrays, and objects.
WebAssembly linear memory is allocated in large chunks called pages, which, once allocated, cannot be deallocated. WebAssembly memory is also a bit more like assembly language memory management: once you’ve allocated your chosen number of pages to your WebAssembly module, you, as the programmer, must keep track of what you’re using memory for and where it is.
The maximum number of pages that an application can allocate at the timeof this writing(v1.0) is 32,767, an overall maximum memory size of 2GB(32,767 * 64K).
If you attempt to grow your linear memory to more than the maximum value you pass in, the application will throw an error.
WebAssembly pointers behave differently from those you might be familiar with in C or C++ that can point to local variables or variables on the heap
When you represent a pointer in WAT, you must put the data in the linear memory; the pointer is then an i32 index to that data.
Unlike in C, WAT cannot create a pointer to a local or global variable. To get the C kind of pointer functionality in WebAssembly, you can set a global variable to set or retrieve the value stored in WebAssembly linear memory.
WebAssembly works well with the webgl and webgl2 canvas context, which render 3D models to the canvas