learnxinyminutes-docs/wasm.html.markdown
2024-05-27 12:15:14 -06:00

9.6 KiB

language filename contributors
WebAssembly learn-wasm.wast
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_64 (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))
)