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

Kiedy internet stał się platformą nie tylko dla dokumentów, ale też aplikacji, niektóre z najbardziej zaawansowanych aplikacji wyniosły ograniczenia w przeglądarkach. W wielu językach najwyższego poziomu dochodzi do zbliżenia się do sedna sprawy przez łączenie się z językami niższego poziomu w celu poprawy wydajności. Na przykład w środowisku Java jest natywny interfejs Java. W przypadku JavaScriptu tym 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ę też, jak utworzono WebAssembly za pomocą tymczasowego rozwiązania opartego na asm.js.

Język asemblera

Czy programujesz kiedyś w języku asemblera? W programowaniu komputerowym język asemblera, nazywany po prostu Asembly, i powszechnie znany w skrócie ASM lub asm, to dowolny język programowania niskopoziomowego z bardzo silną korespondencją między instrukcjami w języku a instrukcjami w języku architektonicznym.

Na przykład w architekturze Intel® 64 i IA-32 Architectures (PDF) instrukcja MUL (do mulacji) wykonuje niepodpisany mnożenie pierwszego operandu (operandu docelowego) i drugiego (operandu źródłowego) i zapisuje wynik w operandze docelowym. W bardzo uproszczeniu: operand docelowy to domniemany operand znajdujący się w rejestrze AX, a operand źródłowy znajduje się w rejestrze ogólnego przeznaczenia, takim jak CX. Wynik jest ponownie przechowywany w rejestrze AX. Przeanalizuj 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 – gdy chcesz mnożyć 5 i 10, prawdopodobnie napiszesz kod podobny do tego w JavaScript:

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

Zaletą procesu montażu jest to, że taki kod niskopoziomowy i zoptymalizowany pod kątem maszyn jest znacznie efektywniejszy niż kod wysokiego poziomu i kod zoptymalizowany pod kątem człowieka. W poprzednim 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 sama nazwa wskazuje, kod x86 zależy od architektury x86. Co by było, gdyby istniał sposób pisania kodu asemblera, który nie był zależny od konkretnej architektury, ale przejmowałby korzyści związane z wydajnością zapewnianą przez asemblera?

asm.js

Pierwszym krokiem do napisania kodu asemblera bez zależności architektury był asm.js – rygorystyczny podzbiór JavaScriptu, którego można używać jako niskopoziomowego, wydajnego języka docelowego kompilatorów. Ten podjęzyk skutecznie opisał maszynę wirtualną działającą w piaskownicy na potrzeby języków niezabezpieczonych w pamięci, takich jak C lub C++. Połączenie statycznej i dynamicznej weryfikacji pozwoliło mechanizmom JavaScriptu zastosować wyprzedzającą strategię optymalizowania strategii kompilacji 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 source-source, taki jak wczesna wersja Emscripten (oparta na modelu LLVM).

Działanie aplikacji zostało poprawione przez ograniczenie funkcji językowych do osób, które obsługują AOT. Firefox 22 był pierwszą przeglądarką obsługującą asm.js, która została wydana pod nazwą OdinMonkey. Chrome dodał obsługę asm.js w wersji 61. Chociaż plik asm.js nadal działa w przeglądarkach, został zastąpiony przez WebAssembly. Zamiast tego użyj asm.js jako alternatywy dla przeglądarek, które nie obsługują WebAssembly.

WebAssembly

WebAssembly to niskopoziomowy język asemblera z kompaktowym formatem binarnym, który działa niemal w środowisku natywnym i udostępnia języki takie jak C/C++ czy Rust oraz wiele innych dzięki miejscu docelowej kompilacji, na którym można działać w internecie. Obecnie pracujemy nad obsługą języków zarządzanych w pamięci, takich jak Java i Dart, i powinny być dostępne wkrótce lub można już z niego korzystać, tak jak w przypadku języka Kotlin/Wasm. Komponent WebAssembly został zaprojektowany do działania równolegle z JavaScriptem, dzięki czemu mogą one ze sobą współpracować.

Dzięki WASI (WebAssembly System Interface) modułowi interfejsu systemu WebAssembly, poza przeglądarką, programy WebAssembly działają również w innych środowiskach wykonawczych. Metoda WASI została stworzona z myślą o przenośności w różnych systemach operacyjnych. Jest bezpieczna i możliwa do działania w środowisku piaskownicy.

Kod WebAssembly (kod binarny, czyli bytecode) powinien być uruchamiany na przenośnej maszynie wirtualnej. Kod bajtowy jest zaprojektowany tak, aby był szybciej analizowany i wykonywany niż JavaScript oraz miał kompaktową reprezentację.

Koncepcyjne instrukcje są realizowane za pomocą tradycyjnego licznika programów, który postępuje zgodnie z instrukcjami. W praktyce większość wyszukiwarek Wasm kompiluje kod bajtowy Wasm na kod maszynowy, a potem go wykonuje. Instrukcje dzielą się na 2 kategorie:

  • Instrukcje kontrolne, które konstruują elementy sterujące i wydzielają ich wartości argumentów ze stosu, mogą zmieniać licznik programu i umieszczać wartości wyników na stosie.
  • Proste instrukcje, które wydzielają wartości argumentów ze stosu, stosują operator do wartości, a następnie wypychają wartości wynikowe na stos, a następnie pośredniczy postęp licznika programu.

Wracając do wcześniejszego przykładu, następujący 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.

Choć kod asm.js jest zaimplementowany w oprogramowaniu, to jego kod może działać w dowolnym silniku JavaScript (nawet jeśli nie jest zoptymalizowany), jednak WebAssembly wymagał nowych funkcji, które zaakceptowały wszyscy dostawcy przeglądarek. Została ogłoszona w 2015 roku, a po raz pierwszy ukazała się w marcu 2017 r., i 5 grudnia 2019 r. została rekomendacją W3C. W3C dba o utrzymanie standardu, udostępniając je wszystkim głównym dostawcom przeglądarek i innym zainteresowanym stronom. Od 2017 roku przeglądarki są obsługiwane na całym świecie.

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

Reprezentacja tekstowa

Reprezentacja tekstowa opiera się na wyrażeniach S i zwykle używa rozszerzenia pliku .wat (format WbAsembly text). Jeśli naprawdę chcesz, możesz napisać to odręcznie. Biorąc pod uwagę powyższy przykład mnożenia i zwiększając jego przydatność, eliminując czynniki, które nie są już zakodowane na stałe, możesz prawdopodobnie opracować 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 użytku przez ludzi, a w szczególności do tworzenia przez człowieka. Korzystając z narzędzia takiego jak wat2wasm, możesz przekonwertować powyższy kod na podaną niżej postać binarną. (Komentarze nie są zwykle częścią reprezentacji binarnej, ale są dodawane przez narzędzie wat2wasm dla lepszego zrozumienia).

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 tym przypadku do akcji wkracza kompilator taki jak Emscripten. Umożliwia on kompilację z języków wyższego poziomu, takich jak C i C++. Istnieją też inne kompilatory dla innych języków, takich jak Rust i wiele innych. Weźmy na przykład ten kod C:

#include <stdio.h>

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

Zwykle ten program w języku C jest kompilowany za pomocą kompilatora gcc.

$ gcc hello.c -o hello

Gdy zainstalujesz Emscripten, kompilujesz go do WebAssembly za pomocą polecenia emcc i prawie takich samych argumentów:

$ emcc hello.c -o hello.html

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

Możesz też skompilować kod do interfejsu WebAssembly bez użycia otoki HTML:

$ emcc hello.c -o hello.js

Tak jak wcześniej, spowoduje to utworzenie pliku hello.wasm, ale tym razem z plikiem hello.js zamiast opakowania HTML. Aby przeprowadzić test, uruchom wynikowy plik JavaScript hello.js, na przykład z Node.js:

$ node hello.js
Hello World

Więcej

To krótkie wprowadzenie do WebAssembly to tylko wierzchołek góry lodowej. Więcej informacji o WebAssembly znajdziesz w dokumentacji WebAssembly w MDN oraz w dokumentacji Emscripten. Praca z WebAssembly może być podobna do zadania Jak narysować mema sowa, zwłaszcza że programiści stron internetowych zaznajomieni z językiem HTML, CSS i JavaScript nie muszą znać się na językach do kompilacji, takich jak C. Na szczęście istnieją takie kanały jak tag webassembly w StackOverflow, na których eksperci chętnie Ci pomogą, jeśli miło Cię o to poprosisz.

Podziękowania

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