vak: (Default)
[personal profile] vak
На наших глазах родилась новая компьютерная архитектура: WebAssembly. Я уже упоминал как-то. Спецификацию можно скачать по ссылке:

webassembly.github.io/spec/core/_download/WebAssembly.pdf

Фактически это самая массовая виртуальная машина нынче. WebAssembly реализована во всех современных веб-браузерах:
  • Google Chrome: полная поддержка с версии 57 (март 2017 г.).
  • Mozilla Firefox: полная поддержка с версии 52 (март 2017 г.).
  • Microsoft Edge: полная поддержка с версии 16 (октябрь 2017 г.).
  • Safari: полная поддержка с версии 11 (сентябрь 2017 г.).
  • Opera: полная поддержка с версии 44 (март 2017 г.).
  • Brave: поддерживает WebAssembly, так как он основан на Chromium.
  • Samsung Internet: полная поддержка в последних версиях.
  • UC Browser: поддерживает WebAssembly в современных версиях.
  • Tor Browser: поддерживает WebAssembly, так как он основан на Firefox.
Кроме того, WebAssembly работает в Node.js (на стороне сервера) и других средах с движками JavaScript, такими как V8, SpiderMonkey или JavaScriptCore.

Чтобы программы, написанные на WebAssembly (в простонародии Wasm), могли общаться с внешним миром, разработан стандарт WASI. По аналогии с юниксом можно считать это набором системных вызовов.

Чтобы программировать на самом близком к "железу" уровне, для традиционных компьютеров используют язык ассемблера. Но мы же не дикари какие в 21-м веке. 😀 Для Wasm сделан высокоуровневый язык AssemblyScript. Он напоминает TypeScript, но со статической типизацией, и транслируется непосредственно в машинные команды Wasm.

В качестве примера попросим Грок реализовать на AssemblyScript традиционную юниксную команду /bin/echo.
// WASI bindings for command-line arguments and output
@external("wasi_snapshot_preview1", "args_sizes_get")
export declare function args_sizes_get(argc: i32, argv_buf_size: i32): i32;

@external("wasi_snapshot_preview1", "args_get")
export declare function args_get(argv: i32, argv_buf: i32): i32;

@external("wasi_snapshot_preview1", "fd_write")
export declare function fd_write(fd: i32, iovs: i32, iovs_len: i32, nwritten: i32): i32;

// Function to write an ASCII string to stdout
function writeAscii(str: string): void
{
    let buffer = new ArrayBuffer(str.length);
    let ptr = changetype<i32>(buffer);
    let offset = 0;

    for (let i = 0; i < str.length; i++) {
        let ch = str.charCodeAt(i);

        // Copy only ASCII characters.
        if (ch <= 0x7F) {
            store<u8>(ptr + offset, ch);
            offset++;
        }
    }

    // Allocate iovec structure (ptr, len)
    let iovec_ptr = changetype<i32>(new ArrayBuffer(8));
    store<i32>(iovec_ptr, changetype<i32>(buffer));
    store<i32>(iovec_ptr + 4, offset);

    // Write to stdout (fd=1)
    let nwritten_ptr = changetype<i32>(new ArrayBuffer(4));
    fd_write(1, iovec_ptr, 1, nwritten_ptr);
}

// Function to write UTF-8 bytes directly to stdout
function writeUtf8(arg_ptr: i32): void
{
    // Calculate length of null-terminated UTF-8 string
    let length = 0;
    while (load<u8>(arg_ptr + length) != 0) {
        length++;
    }

    // Allocate iovec structure (ptr, len)
    let iovec_ptr = changetype<i32>(new ArrayBuffer(8));
    store<i32>(iovec_ptr, arg_ptr);
    store<i32>(iovec_ptr + 4, length);

    // Write to stdout (fd=1)
    let nwritten_ptr = changetype<i32>(new ArrayBuffer(4));
    fd_write(1, iovec_ptr, 1, nwritten_ptr);
}

// Main function
export function _start(): void
{
    // Allocate memory for argument count and buffer size
    let argc_ptr = changetype<i32>(new ArrayBuffer(4));
    let argv_buf_size_ptr = changetype<i32>(new ArrayBuffer(4));

    // Get argument count and buffer size
    args_sizes_get(argc_ptr, argv_buf_size_ptr);
    let argc = load<i32>(argc_ptr);
    let argv_buf_size = load<i32>(argv_buf_size_ptr);

    // Allocate memory for argv pointers and buffer
    let argv = changetype<i32>(new ArrayBuffer(argc * 4));
    let argv_buf = changetype<i32>(new ArrayBuffer(argv_buf_size));

    // Get argument values
    args_get(argv, argv_buf);

    // Skip the first argument (program name)
    let firstArg = 1;

    // If no arguments (besides program name), print newline and exit
    if (argc <= 1) {
        writeAscii("\n");
        return;
    }

    // Iterate through arguments
    for (let i = firstArg; i < argc; i++) {
        // Get pointer to argument string
        let arg_ptr = load<i32>(argv + i * 4);

        // Write UTF-8 bytes directly
        writeUtf8(arg_ptr);

        // Add space between arguments, but not after the last one
        if (i < argc - 1) {
            writeAscii(" ");
        }
    }

    // Print final newline
    writeAscii("\n");
}
Как можно видеть, используются системные вызовы args_sizes_get, args_get, fd_write. Некоторую трудность вызывает преобразование строк из внутреннего формата UTF-16 в UTF-8 для fd_write. Компилируем, запускаем:
$ asc echo.ts --use abort= -O -o echo.wasm
$ wasmtime echo.wasm foobar йцукенг 12 3 456
foobar йцукенг 12 3 456
Работает! И даже русские буквы в кодировке UTF-8 правильно показывает. Глянем, как выглядят машинные команды. Дизассемблируем с помощью wasm2wat и отыщем функцию _start().
(func $echo/_start (type 0)
    (local i32 i32 i32 i32 i32 i32)
    global.get $~lib/memory/__stack_pointer
    i32.const 4
    i32.sub
    global.set $~lib/memory/__stack_pointer
    global.get $~lib/memory/__stack_pointer
    i32.const 1204
    i32.lt_s
    if  ;; label = @1
      unreachable
    end
    global.get $~lib/memory/__stack_pointer
    i32.const 0
    i32.store
    i32.const 4
    call $~lib/arraybuffer/ArrayBuffer#constructor
    local.tee 0
    i32.const 4
    call $~lib/arraybuffer/ArrayBuffer#constructor
    local.tee 1
    call $echo/args_sizes_get
    drop
    local.get 1
    i32.load
    local.set 1
    local.get 0
    i32.load
    local.tee 5
    i32.const 2
    i32.shl
    call $~lib/arraybuffer/ArrayBuffer#constructor
    local.tee 4
    local.get 1
    call $~lib/arraybuffer/ArrayBuffer#constructor
    call $echo/args_get
    drop
    i32.const 1
    local.set 0
    local.get 5
    i32.const 1
    i32.le_s
    if  ;; label = @1
      global.get $~lib/memory/__stack_pointer
      i32.const 1136
      i32.store
      i32.const 1136
      call $echo/writeAscii
      global.get $~lib/memory/__stack_pointer
      i32.const 4
      i32.add
      global.set $~lib/memory/__stack_pointer
      return
    end
    loop  ;; label = @1
      local.get 0
      local.get 5
      i32.lt_s
      if  ;; label = @2
        local.get 4
        local.get 0
        i32.const 2
        i32.shl
        i32.add
        i32.load
        local.set 2
        i32.const 0
        local.set 1
        loop  ;; label = @3
          local.get 1
          local.get 2
          i32.add
          i32.load8_u
          if  ;; label = @4
            local.get 1
            i32.const 1
            i32.add
            local.set 1
            br 1 (;@3;)
          end
        end
        i32.const 8
        call $~lib/arraybuffer/ArrayBuffer#constructor
        local.tee 3
        local.get 2
        i32.store
        local.get 3
        local.get 1
        i32.store offset=4
        i32.const 1
        local.get 3
        i32.const 1
        i32.const 4
        call $~lib/arraybuffer/ArrayBuffer#constructor
        call $echo/fd_write
        drop
        local.get 0
        local.get 5
        i32.const 1
        i32.sub
        i32.lt_s
        if  ;; label = @3
          global.get $~lib/memory/__stack_pointer
          i32.const 1168
          i32.store
          i32.const 1168
          call $echo/writeAscii
        end
        local.get 0
        i32.const 1
        i32.add
        local.set 0
        br 1 (;@1;)
      end
    end
    global.get $~lib/memory/__stack_pointer
    i32.const 1136
    i32.store
    i32.const 1136
    call $echo/writeAscii
    global.get $~lib/memory/__stack_pointer
    i32.const 4
    i32.add
    global.set $~lib/memory/__stack_pointer)
Вроде ничо так выглядит. На этом вполне можно программировать. А если учесть, что в последнем WASIX появились системные вызовы fork(), exec() и wait(), можно даже попробовать портонуть юниксный /bin/sh и весь прочий набор команд BSD.

Date: 2025-04-28 07:48 (UTC)
volynsky: (Default)
From: [personal profile] volynsky
лайк!

Date: 2025-04-28 12:33 (UTC)
chaource: (Default)
From: [personal profile] chaource
А почему можно память распредѣлять, но не нужно освобождать? Тамъ что, сборщикъ мусора въ этомъ ассемблерѣ встроенный? Не вижу нигдѣ dealloc() или нѣчто подобное ему.

Date: 2025-04-28 18:10 (UTC)
sab123: (Default)
From: [personal profile] sab123
Так оно же наверное все равно джаваскрипт внутри, со всем сбором мусора.

Date: 2025-04-28 18:29 (UTC)
chaource: (Default)
From: [personal profile] chaource
Ну это же не очень по-ассемблерному получается.

Date: 2025-04-28 13:36 (UTC)
From: [identity profile] ledernierheros.livejournal.com
> можно даже попробовать портонуть юниксный /bin/sh и весь прочий набор команд BSD

Всё уже украдено до нас: webvm.io Правда, там Linux, не BSD, но принцип тот же :-)

Date: 2025-04-28 18:22 (UTC)
ufm: (Default)
From: [personal profile] ufm
потому что x86-to-WebAssembly JIT compiler