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

Odkąd internet stał się platformą nie tylko do obsługi dokumentów, ale również 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 polegające na „zbliżaniu się do metalu” przez tworzenie interfejsów z językami niskiego poziomu w celu poprawy wydajności. Przykładowo Java ma interfejs natywny Java. W przypadku JavaScriptu jest to 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 powstał WebAssembly za pomocą tymczasowego rozwiązania asm.js.

Czy kiedykolwiek programowałeś/programowałaś w języku asemblera? W programowaniu komputerowym język asemblera, często nazywany po prostu asemblerem i skrótem ASM lub asm, to dowolny język programowania niskiego poziomu, w którym instrukcje są bardzo podobne do instrukcji kodu maszynowego danej architektury.

Na przykład w przypadku architektury Intel® 64 i IA-32 (PDF) instrukcja MUL (mulplikacja) wykonuje nieoznaczone mnożenie pierwszego operanda (operanda docelowego) przez drugiego operanda (operanda źródłowego) i przechowuje wynik w operandzie docelowym. Upraszczając, operand docelowy to domyślny operand znajdujący się w rejestrze AX, a operand źródłowy znajduje się w rejestrze ogólnego przeznaczenia, np. CX. Wynik jest ponownie zapisywany w rejestrze AX. Rozważ ten przykład kodu 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 tym przypadku nie ma to znaczenia, ale możesz sobie wyobrazić, że w przypadku bardziej złożonych operacji różnica może być znaczna.

Jak sugeruje nazwa, 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 napisania kodu asemblera bez zależności od architektury był asm.js, ścisły podzbiór JavaScriptu, który można było wykorzystać jako wydajny język docelowy niskiego poziomu dla 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 w językach ze statycznymi typami danych z ręcznym zarządzaniem pamięcią (np. C) został przetłumaczony przez kompilator źródło-źródło, taki jak wczesna wersja Emscripten (opracowana na podstawie LLVM).

Wydajność została poprawiona dzięki ograniczeniu funkcji językowych do tych, które można zastosować w ramach kompilacji AOT. Firefox 22 była pierwszą przeglądarką obsługującą plik asm.js wydaną pod nazwą OdinMonkey. W wersji 61 dodano obsługę asm.js. Biblioteka asm.js nadal działa w przeglądarkach, ale została zastąpiona przez WebAssembly. W tej chwili asm.js można używać jako alternatywy 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 powinno zostać udostępnione, ale jest już dostępne w przypadku języków Kotlin/Wasm. WebAssembly jest przeznaczony do działania razem z JavaScriptem, co pozwala im współpracować.

Oprócz przeglądarki programy WebAssembly działają też w innych środowiskach uruchomieniowych dzięki WASI, czyli interfejsowi systemowemu WebAssembly, który jest modułowym interfejsem systemowym dla WebAssembly. WASI została stworzona z myślą o przenośności różnych systemów operacyjnych. Zależy nam na zapewnieniu bezpieczeństwa i możliwości działania w środowisku piaskownicy.

Kod WebAssembly (kod binarny, czyli kod bajtowy) jest przeznaczony do uruchamiania na przenośnej maszynie wirtualnej. Bytekod jest zaprojektowany tak, aby jego analizowanie i wykonywanie było szybsze niż w przypadku JavaScriptu, a jego reprezentacja kodu była zwarta.

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

  • Instrukcje sterujące, które tworzą konstrukcje sterujące i wyrzucają wartości argumentów ze stosu, mogą zmieniać licznik programu i wrzucać wartości wyników na stos.
  • Proste instrukcje, które wyjmują wartości argumentów ze stosu, stosują do nich operator, a potem umieszczają wyniki na stosie, po czym następuje domyślne przesunię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.

Podczas gdy asm.js jest implementowany w oprogramowaniu, czyli jego kod może działać w dowolnym silniku JavaScript (nawet nieoptymalizowanym), WebAssembly wymagało nowych funkcji, które zostały uzgodnione 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ą. Powyżej widzisz reprezentację tekstową.

Tekstowa reprezentacja

Reprezentacja tekstowa opiera się na wyrażeniach S i zazwyczaj używa rozszerzenia pliku .wat (w formacie WebAssembly text). Jeśli naprawdę chcesz, możesz napisać go odręcznie. Powyższy przykład mnożenia można ulepszyć, usuwając z niego zakodowane na stałe czynniki, dzięki czemu kod będzie bardziej użyteczny. Prawdopodobnie zrozumiesz 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, który używa rozszerzenia pliku .wasm, nie jest przeznaczony do użytku przez ludzi, a tym bardziej do tworzenia przez nich. Za pomocą narzędzia takiego jak wat2wasm możesz przekonwertować powyższy kod na reprezentację binarną. (Komentarze zwykle nie są częścią reprezentacji binarnej, ale są dodawane przez narzędzie wat2wasm, aby ułatwić zrozumienie kodu).

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. Właśnie w takich sytuacjach przydaje się kompilator, taki jak Emscripten. Umożliwia kompilowanie z języków wyższego poziomu, takich jak C i C++. Istnieją też inne kompilatory dla innych języków, np. Rust. Weź pod uwagę ten kod C:

#include <stdio.h>

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

Zwykle kompilujesz program C za pomocą kompilatora gcc.

$ gcc hello.c -o hello

Po zainstalowaniu Emscripten możesz skompilować kod do WebAssembly za pomocą polecenia emcc i prawie tych samych argumentów:

$ emcc hello.c -o hello.html

Spowoduje to utworzenie pliku hello.wasm i pliku otulającego HTML hello.html. Gdy serwujesz plik hello.html z serwera internetowego, w konsoli Narzędzi deweloperskich zobaczysz wydruk "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ć, uruchom uzyskany plik JavaScript hello.js, na przykład za pomocą 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ć mem „Jak narysować sowę”, zwłaszcza że programiści stron internetowych znający HTML, CSS i JavaScript niekoniecznie znają języki, z których można kompilować, takie jak C. Na szczęście istnieją kanały takie jak tag webassembly na StackOverflow, gdzie eksperci chętnie udzielają pomocy, jeśli grzecznie o nią poprosisz.

Podziękowania

Ten artykuł został sprawdzony przez Jakob Kummerow, Derek Schuff i Rachel Andrew.