Co to jest WebAssembly i skąd się wzięła?

Odkąd internet stał się platformą nie tylko do obsługi dokumentów, ale także aplikacji, niektóre z najbardziej zaawansowanych aplikacji zbliżyły się do granic wytrzymałości przeglądarek. W wielu językach wyższego poziomu stosuje się podejście „bliżej metalu” przez połączenie z językami niższego poziomu w celu poprawy wydajności. Przykładowo Java ma interfejs natywny Java. W przypadku JavaScriptu językiem niższego poziomu jest WebAssembly. Z tego artykułu dowiesz się, czym jest język asemblera i dlaczego może być przydatny w internecie. Dowiesz się z niego, jak powstaje WebAssembly przy użyciu tymczasowego rozwiązania asm.js.

Język asemblera

Czy programowałeś(-aś) kiedykolwiek w języku asemblera? W programowaniu komputerowym język asemblera, często nazywany po prostu asemblerem i powszechnie skracany jako ASM lub asm, to każdy język programowania niskiego poziomu, w którym mocno korespondują instrukcje w tym języku z instrukcjami w kodzie maszynowym architektury.

Na przykład w architekturze Intel® 64 i IA-32 (PDF) instrukcja MUL (dla multiplication) wykonuje nieoznaczone mnożenie pierwszego operandu (operandu docelowego) i drugiego (operandu źródłowego) oraz zapisuje wynik w operandie miejsca docelowego. Bardzo uproszczony operand miejsca docelowego jest domniemanym operandem znajdującym się w rejestrze AX, a operand źródłowy znajduje się w rejestrze ogólnego przeznaczenia, takim jak CX. Wynik jest zapisywany ponownie w rejestrze AX. Zobacz ten przykładowy kod x86:

mov ax, 5  ; Set the value of register AX to 5.
mov cx, 10 ; Set the value of register CX to 10.
mul cx     ; Multiply the value of register AX (5)
           ; and the value of register CX (10), and
           ; store the result in register AX.

Dla porównania, jeśli celem jest mnożenie 5 i 10, należałoby napisać w JavaScripcie kod podobny do tego:

const factor1 = 5;
const factor2 = 10;
const result = factor1 * factor2;

Zaletą ścieżki montażowej jest to, że taki niskopoziomowy, zoptymalizowany pod kątem maszyn kod jest znacznie skuteczniejszy niż kod wysokiego poziomu i zoptymalizowany przez człowieka. W poprzednim przypadku nie ma to znaczenia, ale można sobie wyobrazić, że przy bardziej złożonych operacjach różnica może być znaczna.

Jak sama nazwa wskazuje, kod x86 jest zależny od architektury x86. Co by było, gdyby istniał sposób pisania kodu asemblera, który nie był zależny od konkretnej architektury, ale przeniósłby korzyści w zakresie wydajności z montażu?

asm.js

Pierwszym krokiem do pisania kodu asemblera bez zależności od architektury był asm.js – rygorystyczny podzbiór kodu JavaScript, który można wykorzystać jako niskopoziomowy, wydajny język docelowy kompilatorów. Ten podjęzyk skutecznie opisał maszynę wirtualną w piaskownicy na potrzeby języków, które nie są bezpieczne w pamięci, takich jak C czy C++. Połączenie statycznej i dynamicznej walidacji pozwoliło mechanizmom JavaScript zastosować strategię z wyprzedzeniem (AOT) optymalizującą kompilację w przypadku prawidłowego kodu asm.js. Kod napisany statycznie w językach z ręcznym zarządzaniem pamięcią (np. C) został przetłumaczony przez kompilator typu źródło-to-źródło, np. wczesny Emscripten (oparty na LLVM).

Wydajność poprawiła się przez ograniczenie funkcji językowych do tych, którym podoba się AOT. Firefox 22 był pierwszą przeglądarką obsługującą plik asm.js wydaną pod nazwą OdinMonkey. W Chrome 61 dodaliśmy obsługę asm.js. Choć kod asm.js nadal działa w przeglądarkach, został on zastąpiony przez WebAssembly. Używanie w tym momencie kodu asm.js byłoby alternatywą dla przeglądarek, które nie obsługują WebAssembly.

WebAssembly

WebAssembly to niskopoziomowy język w stylu asemblera o kompaktowym formacie binarnym, który działa z niesamowitą wydajnością oraz udostępnia takie języki jak C/C++ czy Rust oraz wiele innych z celem kompilacji, które umożliwia ich działanie w internecie. Obecnie pracujemy nad obsługą języków zarządzanych przez pamięć, takich jak Java i Dart, i wkrótce powinna zostać udostępniona. Rozwiązanie jest dostępne już w przypadku języków Kotlin/Wasm. Projekt WebAssembly został zaprojektowany do działania równolegle z JavaScriptem, co umożliwia współdziałanie obu tych elementów.

Programy WebAssembly działają nie tylko w przeglądarce, ale również w innych środowiskach wykonawczych dzięki zastosowaniu WASI – interfejsu systemowego WebAssembly – modułowego interfejsu systemowego WebAssembly. WASI została stworzona z myślą o przenośności między systemami operacyjnymi. Jej celem jest zapewnienie bezpieczeństwa i możliwość działania w środowisku piaskownicy.

Kod WebAssembly (kod binarny, czyli kod bajtowy) jest przeznaczony do uruchamiania na przenośnej maszynie wirtualnej. Kod bajtowy został zaprojektowany pod kątem szybszego analizowania i wykonywania niż w przypadku JavaScriptu, a jego reprezentacji jest kompaktowy.

Koncepcyjne wykonywanie instrukcji odbywa się za pomocą tradycyjnego licznika programów, który przechodzi przez instrukcje. W praktyce większość silników Wasm kompiluje kod bajtowy Wasm do kodu maszynowego, a następnie go wykonuje. Instrukcje dzielą się na 2 kategorie:

  • Instrukcje kontrolne, które tworzą konstrukcje sterujące i wypychają ich wartości argumentów ze stosu, mogą zmieniać licznik programu i umieszczać wartości wyników na stosie.
  • Proste instrukcje, które wyodrębniają wartości argumentów ze stosu, stosują operator do wartości, a następnie umieszczają wartości wyników na stosie, co następnie powoduje przejście licznika programu.

Wracając do poprzedniego przykładu: poniższy kod WebAssembly byłby odpowiednikiem kodu x86 z początku artykułu:

i32.const 5  ; Push the integer value 5 onto the stack.
i32.const 10 ; Push the integer value 10 onto the stack.
i32.mul      ; Pop the two most recent items on the stack,
             ; multiply them, and push the result onto the stack.

Komponent asm.js jest zaimplementowany w całości w oprogramowaniu, co oznacza, że jego kod może działać w dowolnym silniku JavaScript (nawet niezoptymalizowanym), ale WebAssembly wymagał nowych funkcji uzgodnionych przez wszystkich dostawców przeglądarek. Ogłoszony w 2015 r., a po raz pierwszy opublikowany w marcu 2017 r. WebAssembly stał się rekomendacją W3C 5 grudnia 2019 r. W3C utrzymuje te standardy dzięki wskazówkom od wszystkich największych dostawców przeglądarek i innych zainteresowanych stron. Od 2017 roku przeglądarka jest uniwersalna.

WebAssembly ma 2 reprezentacje: tekstową i binarną. To, co widzisz powyżej, to reprezentacja tekstowa.

Przedstawienie tekstowe

Reprezentacja tekstowa opiera się na wyrażeniach S i często używa rozszerzenia pliku .wat (dla formatu brzetaj). Jeśli naprawdę chcesz, możesz to napisać odręcznie. Powołując się na powyższy przykład mnożenia i uczyniając z niego bardziej użyteczny przykład, nie kodując czynników na stałe, możesz pewnie zrozumieć ten kod:

(module
  (func $mul (param $factor1 i32) (param $factor2 i32) (result i32)
    local.get $factor1
    local.get $factor2
    i32.mul)
  (export "mul" (func $mul))
)

Reprezentacja binarna

Format binarny z rozszerzeniem .wasm nie jest przeznaczony do konsumpcji przez ludzi, a tym samym do tworzenia przez człowieka. Za pomocą narzędzia wat2wasm możesz przekonwertować powyższe fragmenty kodu do postaci binarnej opisanej poniżej. (Komentarze nie są zwykle częścią reprezentacji binarnej, ale są dodawane przez narzędzie wat2wasm, aby poprawić ich zrozumienie).

0000000: 0061 736d                             ; WASM_BINARY_MAGIC
0000004: 0100 0000                             ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01                                    ; section code
0000009: 00                                    ; section size (guess)
000000a: 01                                    ; num types
; func type 0
000000b: 60                                    ; func
000000c: 02                                    ; num params
000000d: 7f                                    ; i32
000000e: 7f                                    ; i32
000000f: 01                                    ; num results
0000010: 7f                                    ; i32
0000009: 07                                    ; FIXUP section size
; section "Function" (3)
0000011: 03                                    ; section code
0000012: 00                                    ; section size (guess)
0000013: 01                                    ; num functions
0000014: 00                                    ; function 0 signature index
0000012: 02                                    ; FIXUP section size
; section "Export" (7)
0000015: 07                                    ; section code
0000016: 00                                    ; section size (guess)
0000017: 01                                    ; num exports
0000018: 03                                    ; string length
0000019: 6d75 6c                          mul  ; export name
000001c: 00                                    ; export kind
000001d: 00                                    ; export func index
0000016: 07                                    ; FIXUP section size
; section "Code" (10)
000001e: 0a                                    ; section code
000001f: 00                                    ; section size (guess)
0000020: 01                                    ; num functions
; function body 0
0000021: 00                                    ; func body size (guess)
0000022: 00                                    ; local decl count
0000023: 20                                    ; local.get
0000024: 00                                    ; local index
0000025: 20                                    ; local.get
0000026: 01                                    ; local index
0000027: 6c                                    ; i32.mul
0000028: 0b                                    ; end
0000021: 07                                    ; FIXUP func body size
000001f: 09                                    ; FIXUP section size
; section "name"
0000029: 00                                    ; section code
000002a: 00                                    ; section size (guess)
000002b: 04                                    ; string length
000002c: 6e61 6d65                       name  ; custom section name
0000030: 01                                    ; name subsection type
0000031: 00                                    ; subsection size (guess)
0000032: 01                                    ; num names
0000033: 00                                    ; elem index
0000034: 03                                    ; string length
0000035: 6d75 6c                          mul  ; elem name 0
0000031: 06                                    ; FIXUP subsection size
0000038: 02                                    ; local name type
0000039: 00                                    ; subsection size (guess)
000003a: 01                                    ; num functions
000003b: 00                                    ; function index
000003c: 02                                    ; num locals
000003d: 00                                    ; local index
000003e: 07                                    ; string length
000003f: 6661 6374 6f72 31            factor1  ; local name 0
0000046: 01                                    ; local index
0000047: 07                                    ; string length
0000048: 6661 6374 6f72 32            factor2  ; local name 1
0000039: 15                                    ; FIXUP subsection size
000002a: 24                                    ; FIXUP section size

Kompiluję do WebAssembly

Jak widać, ani .wat, ani .wasm nie są szczególnie przyjazne dla ludzi. Dlatego z pomocą przychodzi kompilator, taki jak Emscripten. Pozwala kompilować z języków wyższego poziomu, takich jak C czy C++. Istnieją też inne kompilatory dla innych języków, np. Rust. Przeanalizuj następujący kod C:

#include <stdio.h>

int main() {
  printf("Hello World\n");
  return 0;
}

Zwykle kompiluje się program w języku C z kompilatorem gcc.

$ gcc hello.c -o hello

Po zainstalowaniu skompilujesz go do WebAssembly za pomocą polecenia emcc i niemal tych samych argumentów:

$ emcc hello.c -o hello.html

Spowoduje to utworzenie pliku hello.wasm i pliku z kodem HTML hello.html. Gdy udostępniasz plik hello.html z serwera WWW, w konsoli Narzędzi deweloperskich zobaczysz wartość "Hello World".

Istnieje też sposób skompilowania kodu do WebAssembly bez użycia kodu HTML:

$ emcc hello.c -o hello.js

Tak jak poprzednio, utworzymy plik hello.wasm, ale tym razem zamiast kodu HTML będzie plik hello.js. Aby przetestować działanie, uruchom wynikowy plik JavaScript hello.js przy użyciu, np. Node.js:

$ node hello.js
Hello World

Więcej informacji

To krótkie wprowadzenie do WebAssembly to zaledwie wierzchołek góry lodowej. Więcej informacji o WebAssembly znajdziesz w dokumentacji WebAssembly w MDN. Możesz też zapoznać się z dokumentacją Emscripten. Prawdę mówiąc, praca z WebAssembly może przypominać artykuł Jak narysować mema o sowy, zwłaszcza że programiści stron internetowych zaznajomieni z HTML, CSS i JavaScriptem niekoniecznie są dobrze znani z języków do skompilowania, takich jak C. Na szczęście istnieją kanały takie jak tag webassembly w StackOverflow, na których eksperci chętnie pomogą, jeśli będziesz mieć jakieś uprzejme pytania.

Podziękowania

Ten artykuł napisali Jakob Kummerow, Derek Schuff i Rachel Andrew.