diff options
-rw-r--r-- | wasm.html.markdown | 227 |
1 files changed, 227 insertions, 0 deletions
diff --git a/wasm.html.markdown b/wasm.html.markdown new file mode 100644 index 00000000..98bfc000 --- /dev/null +++ b/wasm.html.markdown @@ -0,0 +1,227 @@ +--- +language: WebAssembly +filename: learn-wasm.wast +contributors: + - ["Dean Shaff", "http://dean-shaff.github.io"] +--- + +``` +;; 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))) ;; WRONG! mult_result is f64! + + ;; 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)) +) + +``` |