Od czasu, gdy internet stał się platformą nie tylko dla dokumentów, ale też dla aplikacji, niektóre z najbardziej zaawansowanych aplikacji zaczęły wykorzystywać przeglądarki internetowe do granic ich możliwości. Podejście polegające na „bliższym kontakcie z hardwarem” przez interfejsy z językami niższego poziomu w celu poprawy wydajności jest spotykane w wielu językach wyższego poziomu. Na przykład Java ma Java Native Interface. 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. Następnie poznasz historię powstania WebAssembly, które zostało stworzone na podstawie tymczasowego rozwiązania asm.js.
Język asemblera
Czy kiedykolwiek programowałeś(-aś) w języku asemblera? W programowaniu asembler, często nazywany po prostu asemblerem i powszechnie skracany do ASM lub asm, to dowolny język programowania niskiego poziomu, w którym istnieje bardzo silna zależność między instrukcjami w języku a instrukcjami kodu maszynowego architektury.
Na przykład w architekturach Intel® 64 i IA-32 (PDF) instrukcja MUL
(dla mnożenia) wykonuje mnożenie bez znaku pierwszego argumentu (argumentu docelowego) i drugiego argumentu (argumentu źródłowego) oraz zapisuje wynik w argumencie docelowym. W uproszczeniu operand docelowy jest operandem niejawnym znajdującym 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
. Przyjrzyj się temu przykładowi 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 Twoim zadaniem byłoby pomnożenie 5 i 10, prawdopodobnie napisałbyś(-abyś) w JavaScript kod podobny do tego:
const factor1 = 5;
const factor2 = 10;
const result = factor1 * factor2;
Zaletą tego podejścia jest to, że kod niskiego poziomu zoptymalizowany pod kątem maszyn jest znacznie wydajniejszy niż kod wysokiego poziomu zoptymalizowany pod kątem ludzi. W tym przypadku nie ma to znaczenia, ale w przypadku bardziej złożonych operacji różnica może być znaczna.
Jak sama nazwa wskazuje, kod x86 jest zależny od architektury x86. A gdyby istniał sposób pisania kodu asemblera, który nie byłby zależny od konkretnej architektury, ale zachowywałby zalety asemblera w zakresie wydajności?
asm.js
Pierwszym krokiem w kierunku pisania kodu asemblera bez zależności od architektury był asm.js, czyli ś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 opisywał maszynę wirtualną w piaskownicy dla języków niebezpiecznych pod względem zarządzania pamięcią, takich jak C czy C++. Połączenie statycznej i dynamicznej weryfikacji umożliwiało silnikom JavaScript stosowanie strategii optymalizującej kompilacji AOT (ahead-of-time) dla prawidłowego kodu asm.js. Kod napisany w językach o statycznym typowaniu i ręcznym zarządzaniu pamięcią (np. C) był tłumaczony przez kompilator źródło-źródło, taki jak wczesna wersja Emscripten (oparta na LLVM).
Wydajność została poprawiona przez ograniczenie funkcji językowych do tych, które można skompilować z wyprzedzeniem. Firefox 22 był pierwszą przeglądarką, która obsługiwała asm.js. Została wydana pod nazwą OdinMonkey. W wersji 61 Chrome dodano obsługę asm.js. Chociaż asm.js nadal działa w przeglądarkach, został zastąpiony przez WebAssembly. W tym momencie asm.js można używać jako alternatywy dla przeglądarek, które nie obsługują WebAssembly.
WebAssembly
WebAssembly to język niskiego poziomu podobny do asemblera, który ma kompaktowy format binarny i działa z wydajnością zbliżoną do natywnej. Umożliwia on kompilowanie języków takich jak C/C++, Rust i wielu innych, dzięki czemu mogą one działać w internecie. Pracujemy nad obsługą języków z zarządzaniem pamięcią, takich jak Java i Dart. Powinna ona być dostępna wkrótce. W przypadku Kotlin/Wasm jest już dostępna. WebAssembly został zaprojektowany do działania obok JavaScriptu, dzięki czemu oba te języki mogą ze sobą współpracować.
Oprócz przeglądarki programy WebAssembly działają też w innych środowiskach wykonawczych dzięki WASI, czyli interfejsowi systemowemu WebAssembly, który jest modułowym interfejsem systemowym dla WebAssembly. WASI ma być przenośny między systemami operacyjnymi, bezpieczny i możliwy do uruchomienia w środowisku piaskownicy.
Kod WebAssembly (kod binarny, czyli kod bajtowy) jest przeznaczony do uruchamiania na przenośnej wirtualnej maszynie stosowej (VM). Kod bajtowy jest zaprojektowany tak, aby jego analizowanie i wykonywanie było szybsze niż w przypadku JavaScriptu, a jego reprezentacja była bardziej zwarta.
Koncepcyjne wykonywanie instrukcji odbywa się za pomocą tradycyjnego licznika programu, 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 sterujące, które tworzą konstrukcje sterujące i zdejmują ze stosu wartości argumentów, mogą zmieniać licznik programu i umieszczać na stosie wartości wyników.
- Proste instrukcje, które pobierają wartości argumentów ze stosu, stosują do nich operatora, a następnie umieszczają wartości wyników na stosie, po czym następuje niejawne zwiększenie 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.
asm.js jest w całości zaimplementowany w oprogramowaniu, co oznacza, że jego kod może działać w dowolnym silniku JavaScript (nawet jeśli nie jest zoptymalizowany). WebAssembly wymagał nowych funkcji, na które zgodzili się wszyscy dostawcy przeglądarek. Ogłoszony w 2015 r. i po raz pierwszy udostępniony w marcu 2017 r. WebAssembly stał się rekomendacją W3C 5 grudnia 2019 r. W3C utrzymuje standard dzięki wkładowi wszystkich głównych dostawców przeglądarek i innych zainteresowanych stron. Od 2017 r. obsługa przeglądarek jest uniwersalna.
WebAssembly ma 2 reprezentacje: tekstową i binarną. Powyżej widzisz reprezentację tekstową.
Reprezentacja tekstowa
Reprezentacja tekstowa jest oparta na wyrażeniach S i zwykle ma rozszerzenie .wat
(w przypadku WebAssembly text format). Jeśli chcesz, możesz napisać go ręcznie. Biorąc pod uwagę przykład mnożenia z powyższego akapitu i upraszczając go przez usunięcie stałych czynników, możesz zrozumieć poniższy 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 odczytu ani tworzenia przez ludzi. Za pomocą narzędzia takiego jak wat2wasm możesz przekonwertować powyższy kod na tę reprezentację binarną. (Komentarze zwykle nie są częścią reprezentacji binarnej, ale są dodawane przez narzędzie wat2wasm, aby zwiększyć czytelność).
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
Kompilowanie do WebAssembly
Jak widzisz, ani .wat
, ani .wasm
nie są zbyt przyjazne dla użytkowników. W takiej sytuacji 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. Rozważmy ten kod w języku C:
#include <stdio.h>
int main() {
printf("Hello World\n");
return 0;
}
Zwykle program w języku C kompiluje się za pomocą kompilatora gcc
.
$ gcc hello.c -o hello
Po zainstalowaniu Emscripten skompiluj go 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 otoki HTML hello.html
. Gdy udostępnisz plik hello.html
z serwera internetowego, w konsoli Narzędzi deweloperskich zobaczysz wydrukowany tekst "Hello World"
.
Istnieje też sposób na skompilowanie do WebAssembly bez otoczki HTML:
$ emcc hello.c -o hello.js
Podobnie jak wcześniej, spowoduje to utworzenie pliku hello.wasm
, ale tym razem będzie to plik hello.js
zamiast otoczki HTML. Aby przetestować działanie, uruchom uzyskany plik JavaScript hello.js
, na przykład w Node.js:
$ node hello.js
Hello World
Więcej informacji
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. 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 kompiluje się kod, takie jak C. Na szczęście istnieją kanały takie jak tag webassembly
na StackOverflow, gdzie eksperci chętnie pomogą, jeśli poprosisz ich o to w uprzejmy sposób.
Podziękowania
Ten artykuł został sprawdzony przez Jakoba Kummerowa, Dereka Schuffa i Rachel Andrew.