Поділитися сторінкою

Вивчіть X за Y хвилин

Де X=WebAssembly

;; learnwasm-ua.wast

(module
  ;; У WebAssembly весь код знаходиться в модулях. Будь-яка операція
  ;; може бути записана за допомогою s-виразу. Також існує синтаксис "стек машини",
  ;; втім, він не сумісний з проміжним бінарним представленням коду.

  ;; Формат бінарного проміжного представлення майже повністю сумісний 
  ;; з текстовим форматом WebAssembly.
  ;; Деякі відмінності:
  ;; local_set -> local.set
  ;; local_get -> local.get

  ;; Код розміщується у функціях

  ;; Типи даних
  (func $data_types
    ;; WebAssembly має чотири типи даних:
    ;; i32 - ціле число, 32 біти
    ;; i64 - ціле число, 64 біти (не підтримується у JavaScript)
    ;; f32 - число з плаваючою комою, 32 біти
    ;; f64 - число з плаваючою комою, 64 біти

    ;; Створити локальну змінну можна за допомогою ключового слова "local".
    ;; Змінні потрібно оголошувати на початку функції.

    (local $int_32 i32)
    (local $int_64 i64)
    (local $float_32 f32)
    (local $float_64 f64)

    ;; Змінні, оголошені вище, ще не ініціалізовані, себто, не мають значення.
    ;; Давайте присвоїмо їм значення за допомогою <тип даних>.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))
  )

  ;; Базові операції
  (func $basic_operations

    ;; Нагадаємо, у WebAssembly будь-що є s-виразом, включно
    ;; з математичними виразами або зчитуванням значень змінних

    (local $add_result i32)
    (local $mult_result f64)

    (local.set $add_result (i32.add (i32.const 2) (i32.const 4)))
    ;; тепер add_result дорівнює 6!

    ;; Для кожної операції потрібно використовувати правильний тип:
    ;; (local.set $mult_result (f32.mul (f32.const 2.0) (f32.const 4.0))) ;; Ніт! mult_result має тип f64!
    (local.set $mult_result (f64.mul (f64.const 2.0) (f64.const 4.0))) ;; Ніт! mult_result має тип f64!

    ;; У WebAssembly є вбудовані функції накшталт математики та побітових операцій.
    ;; Варто зазначити, що тут відсутні вбудовані тригонометричні функції.
    ;; Тож нам потрібно:
    ;; - написати їх самостійно (не найкраща ідея)
    ;; - звідкись їх імпортувати (як саме - побачимо згодом)
  )

  ;; Функції
  ;; Параметри вказуються ключовим словом `param`, значення, що повертається - `result`
  ;; Поточне значення стеку і є значенням функції, що повертається

  ;; Ми можемо викликати інші функції за допомогою `call`

  (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))
  )

  ;; Досі ми не могли що-небудь вивести на консоль і не мали доступу
  ;; до високорівневої математики (степеневі функції, обрахунок експоненти або тригонометрія).
  ;; Більше того, ми навіть не могли викликати WASM функції у JavaScript!
  ;; Виклик цих функцій у WebAssembly залежить від того,
  ;; де ми знаходимось - чи це Node.js, чи середовище браузера.

  ;; Якщо ми у Node.js, то потрібно виконати два кроки. По-перше, ми маємо сконвертувати
  ;; текстове представлення WASM у справжній код webassembly.
  ;; Наприклад, ось так (Binaryen):

  ;; wasm-as learn-wasm.wast -o learn-wasm.wasm

  ;; Давай також застосуємо оптимізації:

  ;; wasm-opt learn-wasm.wasm -o learn-wasm.opt.wasm -O3 --rse

  ;; Тепер наш скомпільований WebAssembly можна завантажити у 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)
  ;; }

  ;; Цей код зчитує функції з importObject
  ;; (вказано у асинхронній JavaScript функції instantiate), а потім експортує функцію
  ;; "print_args", яку ми викликаємо у 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))

  ;; Завантаження даних з пам'яті WebAssembly.
  ;; Наприклад, ми хочемо порахувати cos для елементів JavaScript масиву.
  ;; Нам потрібно отримати доступ до масиву і можливість ітерувати по ньому.
  ;; У прикладі нижче ми змінимо існуючий масив.
  ;; f64.load і f64.store приймають адресу числа у пам'яті *у байтах*.
  ;; Для того, щоб отримати доступ до 3-го елементу масиву, ми маємо передати щось
  ;; накшталт (i32.mul (i32.const 8) (i32.const 2)) у функцію f64.store.

  ;; У JavaScript ми викличемо `apply_cos64` таким чином
  ;; (використаємо функцію instantiate з попереднього прикладу):
  ;;
  ;; 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)
  ;; }
  ;;
  ;; Ця функція не буде працювати, якщо ми виділимо пам'ять для (створимо) Float32Array у JavaScript.

  (memory (export "memory") 100)

  (func $apply_cos64 (param $array_length i32)
    ;; визначаємо змінну циклу або лічильник
    (local $idx i32)
    ;; визначаємо змінну для доступу до пам'яті
    (local $idx_bytes i32)
    ;; константа - кількість байтів у числі типу f64.
    (local $bytes_per_double i32)

    ;; визначаємо змінну, яка зберігатиме значення з пам'яті
    (local $temp_f64 f64)

    (local.set $idx (i32.const 0))
    (local.set $idx_bytes (i32.const 0)) ;; не обов'язково
    (local.set $bytes_per_double (i32.const 8))

    (block
      (loop
        ;; записуємо у idx_bytes необхідне зміщення в пам'яті - для поточного числа.
        (local.set $idx_bytes (i32.mul (local.get $idx) (local.get $bytes_per_double)))

        ;; отримуємо число з пам'яті (за зміщенням):
        (local.set $temp_f64 (f64.load (local.get $idx_bytes)))

        ;; рахуємо cos:
        (local.set $temp_64 (call $cos (local.get $temp_64)))

        ;; тепер зберігаємо результат обчислень у пам'ять:
        (f64.store
          (local.get $idx_bytes)
          (local.get $temp_64))

        ;; або робимо все за один крок (альтернативний код)
        (f64.store
          (local.get $idx_bytes)
          (call $cos
            (f64.load
              (local.get $idx_bytes))))

        ;; збільшуємо лічильник на одиницю (інкремент)
        (local.set $idx (i32.add (local.get $idx) (i32.const 1)))

        ;; якщо лічильник дорівнює довжині масиву, то завершуємо цикл
        (br_if 1 (i32.eq (local.get $idx) (local.get $array_length)))
        (br 0)
      )
    )
  )
  (export "apply_cos64" (func $apply_cos64))
)

Маєте пораду? А може, виправлення? Відкрийте Issue у GitHub-репозиторії або зробіть pull request самостійно!

Автор початкової версії Dean Shaff, оновлено 2 авторами.