mirror of
https://github.com/adambard/learnxinyminutes-docs.git
synced 2024-12-23 17:41:41 +00:00
3406234ef1
The second comment probably got to the actual expression by mistake. Commented expression is wrong as it should be, but the other one is fine
9.6 KiB
9.6 KiB
language | filename | contributors | |||
---|---|---|---|---|---|
WebAssembly | learn-wasm.wast |
|
;; learn-wasm.wast
(module
;; In WebAssembly, everything is included in a module. Moreover, everything
;; can be expressed as an s-expression. Alternatively, there is the
;; "stack machine" syntax, but that is not compatible with Binaryen
;; intermediate representation (IR) syntax.
;; The Binaryen IR format is *mostly* compatible with WebAssembly text format.
;; There are some small differences:
;; local_set -> local.set
;; local_get -> local.get
;; We have to enclose code in functions
;; Data Types
(func $data_types
;; WebAssembly has only four types:
;; i32 - 32 bit integer
;; i64 - 64 bit integer (not supported in JavaScript)
;; f32 - 32 bit floating point
;; f64 - 64 bit floating point
;; We can declare local variables with the "local" keyword
;; We have to declare all variables before we start doing anything
;; inside the function
(local $int_32 i32)
(local $int_64 i64)
(local $float_32 f32)
(local $float_64 f64)
;; These values remain uninitialized.
;; To set them to a value, we can use <type>.const:
(local.set $int_32 (i32.const 16))
(local.set $int_32 (i64.const 128))
(local.set $float_32 (f32.const 3.14))
(local.set $float_64 (f64.const 1.28))
)
;; Basic operations
(func $basic_operations
;; In WebAssembly, everything is an s-expression, including
;; doing math, or getting the value of some variable
(local $add_result i32)
(local $mult_result f64)
(local.set $add_result (i32.add (i32.const 2) (i32.const 4)))
;; the value of add_result is now 6!
;; We have to use the right data type for each operation:
;; (local.set $mult_result (f32.mul (f32.const 2.0) (f32.const 4.0))) ;; WRONG! mult_result is f64!
(local.set $mult_result (f64.mul (f64.const 2.0) (f64.const 4.0)))
;; WebAssembly has some builtin operations, like basic math and bitshifting.
;; Notably, it does not have built in trigonometric functions.
;; In order to get access to these functions, we have to either
;; - implement them ourselves (not recommended)
;; - import them from elsewhere (later on)
)
;; Functions
;; We specify arguments with the `param` keyword, and specify return values
;; with the `result` keyword
;; The current value on the stack is the return value of a function
;; We can call other functions we've defined with the `call` keyword
(func $get_16 (result i32)
(i32.const 16)
)
(func $add (param $param0 i32) (param $param1 i32) (result i32)
(i32.add
(local.get $param0)
(local.get $param1)
)
)
(func $double_16 (result i32)
(i32.mul
(i32.const 2)
(call $get_16))
)
;; Up until now, we haven't be able to print anything out, nor do we have
;; access to higher level math functions (pow, exp, or trig functions).
;; Moreover, we haven't been able to use any of the WASM functions in Javascript!
;; The way we get those functions into WebAssembly
;; looks different whether we're in a Node.js or browser environment.
;; If we're in Node.js we have to do two steps. First we have to convert the
;; WASM text representation into actual webassembly. If we're using Binyaren,
;; we can do that with a command like the following:
;; wasm-as learn-wasm.wast -o learn-wasm.wasm
;; We can apply Binaryen optimizations to that file with a command like the
;; following:
;; wasm-opt learn-wasm.wasm -o learn-wasm.opt.wasm -O3 --rse
;; With our compiled WebAssembly, we can now load it into Node.js:
;; const fs = require('fs')
;; const instantiate = async function (inFilePath, _importObject) {
;; var importObject = {
;; console: {
;; log: (x) => console.log(x),
;; },
;; math: {
;; cos: (x) => Math.cos(x),
;; }
;; }
;; importObject = Object.assign(importObject, _importObject)
;;
;; var buffer = fs.readFileSync(inFilePath)
;; var module = await WebAssembly.compile(buffer)
;; var instance = await WebAssembly.instantiate(module, importObject)
;; return instance.exports
;; }
;;
;; const main = function () {
;; var wasmExports = await instantiate('learn-wasm.wasm')
;; wasmExports.print_args(1, 0)
;; }
;; The following snippet gets the functions from the importObject we defined
;; in the JavaScript instantiate async function, and then exports a function
;; "print_args" that we can call from Node.js
(import "console" "log" (func $print_i32 (param i32)))
(import "math" "cos" (func $cos (param f64) (result f64)))
(func $print_args (param $arg0 i32) (param $arg1 i32)
(call $print_i32 (local.get $arg0))
(call $print_i32 (local.get $arg1))
)
(export "print_args" (func $print_args))
;; Loading in data from WebAssembly memory.
;; Say that we want to apply the cosine function to a Javascript array.
;; We need to be able to access the allocated array, and iterate through it.
;; This example will modify the input array inplace.
;; f64.load and f64.store expect the location of a number in memory *in bytes*.
;; If we want to access the 3rd element of an array, we have to pass something
;; like (i32.mul (i32.const 8) (i32.const 2)) to the f64.store function.
;; In JavaScript, we would call `apply_cos64` as follows
;; (using the instantiate function from earlier):
;;
;; const main = function () {
;; var wasm = await instantiate('learn-wasm.wasm')
;; var n = 100
;; const memory = new Float64Array(wasm.memory.buffer, 0, n)
;; for (var i=0; i<n; i++) {
;; memory[i] = i;
;; }
;; wasm.apply_cos64(n)
;; }
;;
;; This function will not work if we allocate a Float32Array on the JavaScript
;; side.
(memory (export "memory") 100)
(func $apply_cos64 (param $array_length i32)
;; declare the loop counter
(local $idx i32)
;; declare the counter that will allow us to access memory
(local $idx_bytes i32)
;; constant expressing the number of bytes in a f64 number.
(local $bytes_per_double i32)
;; declare a variable for storing the value loaded from memory
(local $temp_f64 f64)
(local.set $idx (i32.const 0))
(local.set $idx_bytes (i32.const 0)) ;; not entirely necessary
(local.set $bytes_per_double (i32.const 8))
(block
(loop
;; this sets idx_bytes to bytes offset of the value we're interested in.
(local.set $idx_bytes (i32.mul (local.get $idx) (local.get $bytes_per_double)))
;; get the value of the array from memory:
(local.set $temp_f64 (f64.load (local.get $idx_bytes)))
;; now apply the cosine function:
(local.set $temp_64 (call $cos (local.get $temp_64)))
;; now store the result at the same location in memory:
(f64.store
(local.get $idx_bytes)
(local.get $temp_64))
;; do it all in one step instead
(f64.store
(local.get $idx_bytes)
(call $cos
(f64.load
(local.get $idx_bytes))))
;; increment the loop counter
(local.set $idx (i32.add (local.get $idx) (i32.const 1)))
;; stop the loop if the loop counter is equal the array length
(br_if 1 (i32.eq (local.get $idx) (local.get $array_length)))
(br 0)
)
)
)
(export "apply_cos64" (func $apply_cos64))
;; Wasm is a stack-based language, but for returning values more complicated
;; than an int/float, a separate memory stack has to be manually managed. One
;; approach is to use a mutable global to store the stack_ptr. We give
;; ourselves 1MiB of memstack and grow it downwards.
;;
;; Below is a demonstration of how this C code **might** be written by hand
;;
;; typedef struct {
;; int a;
;; int b;
;; } sum_struct_t;
;;
;; sum_struct_t sum_struct_create(int a, int b) {
;; return (sum_struct_t){a, b};
;; }
;;
;; int sum_local() {
;; sum_struct_t s = sum_struct_create(40, 2);
;; return s.a + s.b;
;; }
;; Unlike C, we must manage our own memory stack. We reserve 1MiB
(global $memstack_ptr (mut i32) (i32.const 65536))
;; Structs can only be returned by reference
(func $sum_struct_create
(param $sum_struct_ptr i32)
(param $var$a i32)
(param $var$b i32)
;; c// sum_struct_ptr->a = a;
(i32.store
(get_local $sum_struct_ptr)
(get_local $var$a)
)
;; c// sum_struct_ptr->b = b;
(i32.store offset=4
(get_local $sum_struct_ptr)
(get_local $var$b)
)
)
(func $sum_local (result i32)
(local $var$sum_struct$a i32)
(local $var$sum_struct$b i32)
(local $local_memstack_ptr i32)
;; reserve memstack space
(i32.sub
(get_global $memstack_ptr)
(i32.const 8)
)
tee_local $local_memstack_ptr ;; tee both stores and returns given value
set_global $memstack_ptr
;; call the function, storing the result in the memstack
(call $sum_struct_create
((;$sum_struct_ptr=;) get_local $local_memstack_ptr)
((;$var$a=;) i32.const 40)
((;$var$b=;) i32.const 2)
)
;; retrieve values from struct
(set_local $var$sum_struct$a
(i32.load offset=0 (get_local $local_memstack_ptr))
)
(set_local $var$sum_struct$b
(i32.load offset=4 (get_local $local_memstack_ptr))
)
;; unreserve memstack space
(set_global $memstack_ptr
(i32.add
(get_local $local_memstack_ptr)
(i32.const 8)
)
)
(i32.add
(get_local $var$sum_struct$a)
(get_local $var$sum_struct$b)
)
)
(export "sum_local" (func $sum_local))
)