Obsah
1. Pohled pod kapotu formátu WebAssembly
2. Alternativa či nahrazení JavaScriptu?
3. Způsob využití různých programovacích jazyků na webových stránkách
4. Svět transpřekladačů do JavaScriptu
7. Instrukční sada definovaná pro WebAssembly
8. Instalace nástrojů pro manipulaci s formátem WebAssembly
9. Základní nástroje dostupné v balíčku wabt
10. Překlad do WebAssembly s využitím základních nástrojů Clangu
11. První demonstrační příklad – překlad funkce pro součet dvou celých čísel do WebAssembly
12. Analýza obsahu souboru add.wasm
13. Zpětný překlad z WebAssembly do vysokoúrovňového jazyka
14. Vygenerování optimalizovaného kódu ve WebAssembly
15. Bližší pohled na sekvenci instrukcí v přeložené funkci add
16. Druhý demonstrační příklad – překlad funkce pro součet dvou čísel typu float/single
17. Třetí demonstrační příklad – překlad funkce pro výpočet největšího společného dělitele
18. Analýza obsahu souboru gcd.wasm
19. Bližší pohled na sekvenci instrukcí v přeložené funkci gcd
1. Pohled pod kapotu formátu WebAssembly
V dnešním článku se seznámíme s technologiemi, na kterých je založen formát WebAssembly. Tento formát umožňuje spouštění aplikací v rámci webových prohlížečů popř. například na serverech nebo jako součást složitějších aplikací uvnitř takzvaného sandboxu (což můžeme považovat za jednu z forem virtualizace).
Na WebAssembly se můžeme dívat i jako na popis formátu uložení strojových instrukcí virtuálního stroje (bajtkódu); jedná se tedy o obdobu souborů .class ze světa virtuálního stroje Javy nebo souborů .pyc používaných virtuálním strojem Pythonu. WebAssembly popisuje jak samotný formát uložení strojových instrukcí (virtuálního stroje), tak i vlastní instrukce, které jsou tímto virtuálním strojem podporovány. Samotné WebAssembly se postupně vyvíjí, takže například ve WebAssembly 2.0 byla přidána podpora pro další SIMD operace atd.
Ukážeme si i způsob překladu funkcí z jazyka C do WebAssembly (a to bez použití Emscriptenu) a pro analýzu výsledných souborů použijeme nástroje dostupné v balíčku wabt. Pracovat tedy budeme na poměrně nízké úrovni, bez dalších přidaných vrstev abstrakce (například Emscripten je sám o sobě dosti komplikovaný, takže nám do značné míry ztěžuje pochopení interních vlastností WebAssembly).
2. Alternativa či nahrazení JavaScriptu?
JavaScript is an assembly language. The JavaScript + HTML generate is like a .NET assembly. The browser can execute it, but no human should really care what's there.
Erik Meijer
Na WebAssembly se můžeme dívat jako na další (tentokrát úspěšný) pokus o nabídnutí alternativní technologie k JavaScriptu, což je ve většině prohlížečů jediný podporovaný programovací jazyk. Podobných pokusů už v minulosti existovala celá řada, například VBScript, applety v jazyce Java (resp. přeložené do bajtkódu JVM), Flash, Silverlight, Adobe Air, Dart atd. atd. Ovšem každá z těchto technologií měla různé problémy (závislost na jednom prohlížeči, problém s instalací JVM, bezpečnost atd.), takže se sice používala, ale postupně byla nahrazena – buď řešením postaveným na JavaScriptu (asm.js apod.) nebo právě WebAssembly (to se ovšem z pohledu použité technologie do značné míry podobá právě JVM, ovšem bez nutnosti instalovat desítky megabajtů potenciálně problematických balíčků).
3. Způsob využití různých programovacích jazyků na webových stránkách
Pravděpodobně nejjednodušší a (alespoň teoreticky) nejpřímější cestou podpory nového programovacího jazyka ve webových prohlížečích je integrace jeho interpretru přímo do prohlížeče popř. použití přídavného modulu (pluginu) s tímto interpretrem. Ovšem i přes snahy některých vývojářů a softwarových společností o začlenění pluginů určených pro podporu dalších skriptovacích jazyků do webových prohlížečů (z historického pohledu se jednalo minimálně o jazyk Tcl, dále VBScript firmy Microsoft, Dart v Dartiu apod.) je patrné, že v současnosti je jediným široce akceptovaným skriptovacím jazykem na straně webového prohlížeče pouze JavaScript se všemi přednostmi a zápory, které tato monokultura do světa IT přináší.
To však v žádném případě neznamená, že by se ty části aplikace, které mají být spouštěny na straně klienta (v jeho webovém prohlížeči), musely psát pouze v JavaScriptu. Tento jazyk totiž nemusí zdaleka všem vývojářům vyhovovat, ať již z objektivních či ze subjektivních příčin (například kvůli dosti zvláštně navrženému typovému systému, který ovšem umožnil realizovat například JSF*ck).
V relativně nedávné minulosti proto vzniklo a stále ještě vzniká mnoho projektů, jejichž cílem je umožnit tvorbu webových aplikací pro prohlížeč napsaných v jiných programovacích jazycích, než je JavaScript. Zdrojové kódy těchto aplikací je posléze nutné nějakým vhodným způsobem zpracovat (transpřeložit, přeložit, …) takovým způsobem, aby je bylo možné ve webovém prohlížeči spustit. Možností, které máme v současnosti k dispozici, je hned několik – lze použít výše zmíněný plugin (velmi problematické a dnes značně nepopulární řešení), transpřekladač do JavaScriptu či virtuální stroj popř. interpret daného jazyka implementovaný opět v JavaScriptu. Právě posledními dvěma zmíněnými možnostmi se budeme zabývat v navazujících kapitolách; posléze se zaměříme na využití virtuálního stroje běžícího ve webovém prohlížeči – protože právě zde se setkáme s WebAssembly.
4. Svět transpřekladačů do JavaScriptu
Jednu z dnes velmi populárních technik umožňujících použití prakticky libovolného programovacího jazyka pro tvorbu aplikací běžících na straně webového prohlížeče, představuje použití takzvaných transcompilerů (source-to-source compiler) zajišťujících překlad programu napsaného ve zdrojovém programovacím jazyce do funkčně identického programu napsaného v JavaScriptu (někdy se setkáme i s označením transpiler). Transpřekladač se většinou spouští pouze jedenkrát na vývojářském počítači, samotní klienti již mají k dispozici „univerzální“ JavaScriptový kód spustitelný v prakticky jakémkoli moderním webovém prohlížeči.
Existuje však i druhá možnost, kdy je samotný transpřekladač naprogramován v JavaScriptu a spouštěn přímo ve webovém prohlížeči klientů. Oba přístupy mají své přednosti, ale pochopitelně i nějaké zápory (například tvůrci uzavřených aplikací pravděpodobně budou upřednostňovat první možnost, protože výstupy transcompilerů jsou většinou dosti nečitelné; dokonce by mohla snaha o prozkoumání kódu spadat pod reverse engineering). Druhá možnost je relativně elegantní v tom ohledu, že se z pohledu programátora webové aplikace skutečně jedná o nový programovací jazyk, který je jakoby přímo zpracováván prohlížečem na stejné úrovni jako JavaScript. Příkladem může být kombinace JavaScriptu a jazyka WISP:
<html>
<head>
<title>Jazyk WISP na webové stránce</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<script src="wisp.min.js" type="application/javascript">
</script>
<script type="application/wisp">
(print "část naprogramovaná ve WISPu")
</script>
<script type="application/javascript">
console.log("část naprogramovaná v JavaScriptu")
</script>
</head>
<body>
</body>
</html>
Z praxe můžeme uvést například následující projekty založené na transpřekladači. Některé z nich je možné použít přímo prostředí webového prohlížeče, jiné provádí překlad do JavaScriptu na příkazovém řádku a existují i kombinace obou způsobů (opět viz jazyk WISP podporující oba režimy):
:| # | Jazyk | Poznámka |
|---|---|---|
| 1 | CoffeeScript | přidání syntaktického cukru do JavaScriptu |
| 2 | JSweet | překlad programů z Javy do JavaScriptu popř. do TypeScriptu |
| 3 | Transcrypt | překlad Pythonu do JavaScriptu (tomuto nástroji se budeme věnovat v dalším článku) |
| 4 | ClojureScript | překlad aplikací psaných v Clojure do JavaScriptu |
| 5 | Kaffeine | rozšíření JavaScriptu o nové vlastnosti |
| 6 | RedScript | jazyk inspirovaný Ruby |
| 7 | GorillaScript | další rozšíření JavaScriptu |
| 8 | ghcjs | transpřekladač pro fanoušky programovacího jazyka Haskell |
| 9 | wisp | zjednodušená a dnes již nevyvíjená varianta ClojureScriptu |
| 10 | Babel | překlad novějších variant JavaScript (ES2015) a TypeScriptu do zvolené (starší) verze JavaScriptu, stále populární, i přesto, že nové prohlížeče ES2015 podporují |
| 11 | GopherJS | překladač programů naprogramovaných v jazyce Go do JavaScriptu |
5. Nástroj Emscripten
Další alternativní technologii, která mi osobně přijde velmi zajímavá a v budoucnu možná i přelomová, představují transpřekladače provádějící překlad z bajtkódu či mezikódu do JavaScriptu (vstupem zde tedy není zdrojový kód v nějakém lidsky čitelném programovacím jazyku, ale většinou binárně reprezentovaný výsledek předchozího překladu). Příkladem tohoto typu transpřekladače je především nástroj Emscripten [1] umožňující překlad kódu z libovolného jazyka podporovaného LLVM (Rust, C, C++, Objective C, D, Ada, Fortran atd.) do JavaScriptu. Podívejme se nyní ve stručnosti na kroky, které je zapotřebí provést proto, aby se původní zdrojový kód napsaný například v Céčku, mohl nějakým způsobem spustit ve webovém prohlížeči:
- Na vstupu celého procesu je program napsaný v céčku
- Nejprve je proveden překlad pomocí clang do mezikódu LLVM (LLVM Intermediate Representation)
- Následně je zavolán Fastcomp (jádro překladače Emscriptenu) pro překlad mezikódu z předchozího kroku do JavaScriptu
- Výsledný JavaScriptový zdrojový kód je možné využít různými způsoby (node.js na serveru, na WWW stránce atd.)
Právě projekt Emscripten do značné míry usnadnil další způsob zajištění běhu programů napsaných v různých programovacích jazycích ve webovém prohlížeči. Pokud je totiž možné přeložit jakýkoli program napsaný v jazycích C či C++ do JavaScriptu (samozřejmě za předpokladu, že se vhodným způsobem budou emulovat použité knihovní funkce), proč by nebylo možné do JavaScriptu rovnou přeložit celý virtuální stroj používaný daným programovacím jazykem? Samozřejmě to možné je, a to zejména v těch případech, kdy je překládaný virtuální stroj (alespoň z dnešního pohledu) malý, což je příklad VM pro jazyk Lua, tak i například poněkud většího virtuálního stroje Pythonu (.NET resp. CLR či Java VM už je pochopitelně mnohem těžší oříšek).
Překladem VM do JavaScriptu získáme poměrně mnoho výhod, zejména pak možnost mít přímo v HTML stránkách původní zdrojové kódy (Lua, Python atd.) a nikoli nečitelný výstup z transpřekladačů. Za tento postup však také zaplatíme, zejména pomalejším během aplikací v porovnání s nativní VM. V praxi se může jednat o výkonnostní propad zhruba na polovinu, což ovšem v mnoha aplikacích vůbec není tak špatný výsledek.
Příkladem takového typu virtuálního stroje je LuaJS.
6. WebAssembly
Konečně se dostáváme k technologii nazvané WebAssembly. Již v úvodní kapitole jsme si řekli, že se v první řadě jedná o specifikaci virtuálního stroje, především jeho struktury (mimochodem: je založen na zásobníku operandů, podobně jako například virtuální stroj Javy) a taktéž ze specifikace jeho instrukčního souboru.
Důležité přitom je, že současně používaná varianta WebAssembly je skutečně dosti nízkoúrovňová, takže neobsahuje například ani podporu pro automatickou správu paměti a i specifikace runtime je dosti minimalistická. To je ovšem v mnoha ohledech výhoda, protože u programovacích jazyků typu C, C++ či Rust není automatická správa paměti relevantní a jejich runtime je malý a naopak u jazyků typu Go je správce paměti přímo součástí runtime (zjednodušeně řečeno knihoven, které jsou slinkovány a tvoří výsledný bajtkód předávaný WebAssembly – ten ovšem dosti „nabobtná“). Správa paměti řízená přímo virtuálním strojem WebAssembly je prozatím ve fázi vývoje a v dnešním článku ji nebudeme potřebovat.
Již v předchozím odstavci jsme se zmínili o problematice runtime. Virtuální stroj WebAssembly akceptuje soubory s MIME typem application/wasm, které by měly obsahovat jak vlastní kód aplikace přeložený do bajtkódu, tak i veškerý podpůrný kód. V případě jazyka Go to konkrétně znamená, že soubory s přeloženou aplikací jsou poměrně velké. I ta nejjednodušší aplikace přeložená do WebAssembly má velikost cca 1300 kB, protože je ve výsledku obsažený celý potřebný runtime i automatický správce paměti. Pokud je použit Rust, je výsledná velikost souborů menší, protože tento jazyk má (obecně) menší runtime.
Velikost výsledného souboru se zvětšujícím se zdrojovým kódem aplikace dále již roste jen pomalu, ovšem i přesto je nutné počítat s tím, že první načtení a inicializace bajtkódu může být pomalá (mobilní připojení atd.) a může se tedy jednat o jeden z důvodů, proč například WebAssembly a Go v praxi spíše nepoužívat. Na druhou stranu si představme například aplikaci typu „webové IDE“, CAD nebo Google Docs – zde se doba nutná pro přenos cca jednoho či dvou megabajtů runtime pravděpodobně ztratí mezi stovkami kilobajtů dalších souborů (navíc se vlastně mnohdy mohou odstranit všechny JavaScriptové knihovny); u podobných aplikací se navíc očekává, že budou spuštěny delší dobu, na rozdíl od běžných webových prezentací.
Dnes již existuje relativně velké množství překladačů, které dokážou generovat kód určený pro WebAssembly:
| Programovací jazyk | Překlad do WebAssembly |
|---|---|
| C | Emscripten nebo Clang |
| C++ | Emscripten nebo Clang |
| Rust | rustc |
| Go | go build |
| C# | dotnet (přes workload wasm) |
| F# | Bolero |
| Python | Pyodide atd. |
| Java | CheerpJ, JWebAssembly, TeaVM |
| Kotlin | přímý překlad |
| Ruby | MRuby |
7. Instrukční sada definovaná pro WebAssembly
Většina instrukcí WebAssembly pracuje s operandy uloženými na takzvaném zásobníku operandů (operand stack). V tomto případě se již jedná o skutečný zásobník typu LIFO – Last In, First Out). Virtuální stroj typicky již při načítání souborů .wasm kontroluje typy operandů uložených na zásobník operandů a zajišťuje, že se nad těmito operandy budou provádět pouze typově bezpečné operace. V praxi to například znamená, že není možné na zásobník uložit dvě hodnoty typu float (resp.f32) a následně provést instrukci i32.add, protože tato instrukce vyžaduje, aby na zásobníku byly uloženy dvě hodnoty typu int (i když f32 i i32 mají v tomto případě shodnou bitovou šířku).
Instrukce jsou rozděleny do několika oblastí:
- Řízení toku programu
- Operace typu Load a Store (práce s operační pamětí)
- Práce s proměnnými a parametry
- Aritmetické instrukce pro celočíselné operandy
- Aritmetické instrukce pro operandy s plovoucí řádovou čárkou
- Instrukce pro porovnání celočíselných operandů
- Instrukce pro porovnání operandů s plovoucí řádovou čárkou
- Konverzní operace
Aritmetické, logické a relační instrukce typicky pracují s operandy těchto typů:
| Označení typu | Stručný popis |
|---|---|
| i8 | osmibitová celá čísla |
| i16 | 16bitová celá čísla |
| i32 | 32bitová celá čísla |
| i64 | 64bitová celá čísla |
| f32 | odpovídá typu single podle IEEE 754 |
| f64 | odpovídá typu double podle IEEE 754 |
Nověji je taktéž k dispozici datový typ v128, což je 128bitový vektor využívaný pro uložení operandů pro SIMD (vektorové) operace.
Jednotlivé instrukce si ukážeme v demonstračních příkladech, které jsou součástí praktické části dnešního článku.
8. Instalace nástrojů pro manipulaci s formátem WebAssembly
V praktické části dnešního článku budeme zkoumat obsah souborů s koncovkou .wasm. Jedná se o binární soubory, takže pro manipulaci s nimi použijeme specializované nástroje (i když by teoreticky mohl postačovat i nějaký hexadecimální prohlížeč nebo editor). Pro formát WebAssembly je dostupný balíček nazvaný wabt, který poskytuje základní nástroje pro manipulaci s .wasm.
wabt je v některých distribucích Linuxu dostupný ve formě standardního balíčku, takže jeho instalace je jednoduchá. Příkladem jsou distribuce založené na RPM:
$ sudo dnf install wabt Last metadata expiration check: 2:13:33 ago on Fri 07 Nov 2025 01:28:29 PM CET. Dependencies resolved. ======================================================================================================================================== Package Architecture Version Repository Size ======================================================================================================================================== Installing: wabt x86_64 1.0.33-3.fc40 fedora 2.2 M Transaction Summary ======================================================================================================================================== Install 1 Package Total download size: 2.2 M Installed size: 18 M Is this ok [y/N]: y Downloading Packages: wabt-1.0.33-3.fc40.x86_64.rpm 2.1 MB/s | 2.2 MB 00:01 ---------------------------------------------------------------------------------------------------------------------------------------- Total 1.3 MB/s | 2.2 MB 00:01 Running transaction check Transaction check succeeded. Running transaction test Transaction test succeeded. Running transaction Preparing : 1/1 Installing : wabt-1.0.33-3.fc40.x86_64 1/1 Running scriptlet: wabt-1.0.33-3.fc40.x86_64 1/1 Installed: wabt-1.0.33-3.fc40.x86_64 Complete!
9. Základní nástroje dostupné v balíčku wabt
V balíčku wabt se nachází celkem dvanáct nástrojů určených pro manipulaci s binárními soubory, které mají koncovku .wasm popř. s jejich textovými variantami s koncovkou .wat. Získání seznamu nainstalovaných nástrojů je jednoduché:
$ sudo dnf repoquery -l wabt | grep "/usr/bin" /usr/bin/spectest-interp /usr/bin/wasm-decompile /usr/bin/wasm-interp /usr/bin/wasm-objdump /usr/bin/wasm-opcodecnt /usr/bin/wasm-strip /usr/bin/wasm-validate /usr/bin/wasm2c /usr/bin/wasm2wat /usr/bin/wast2json /usr/bin/wat-desugar /usr/bin/wat2wasm
Pro potřeby dnešního článku využijeme pouze dva z těchto nástrojů. Konkrétně se jedná o wasm2wat, který dokáže převést binární soubor ve formátu .wasm na jeho textovou (čitelnou) variantu:
$ wasm2wat --help usage: wasm2wat [options] filename Read a file in the WebAssembly binary format, and convert it to the WebAssembly text format. examples: # parse binary file test.wasm and write text file test.wast $ wasm2wat test.wasm -o test.wat # parse test.wasm, write test.wat, but ignore the debug names, if any $ wasm2wat test.wasm --no-debug-names -o test.wat
Druhým nástrojem, který dnes využijeme, je nástroj nazvaný wasm-objdump, který do jisté míry odpovídá standardnímu „céčkovému“ nástroji objdump, s nímž jsme se seznámili v článku Nástroj objdump: švýcarský nožík pro vývojáře:
$ wasm-objdump --help
usage: wasm-objdump [options] filename+
Print information about the contents of wasm binaries.
examples:
$ wasm-objdump test.wasm
options:
--help Print this help message
--version Print version information
-h, --headers Print headers
-j, --section=SECTION Select just one section
-s, --full-contents Print raw section contents
-d, --disassemble Disassemble function bodies
--debug Print extra debug information
-x, --details Show section details
-r, --reloc Show relocations inline with disassembly
--section-offsets Print section offsets instead of file offsets in code disassembly
10. Překlad do WebAssembly s využitím základních nástrojů Clangu
Pro překlad rozsáhlejších aplikací naprogramovaných v jazyku C nebo C++ do WebAssembly se většinou využívá překladač Clang kombinovaný s Emscriptenem, což umožňuje zařazení i všech potřebných knihoven do výsledné (binární) distribuce aplikace spustitelné ve webovém prohlížeči. Nás ovšem bude nyní zajímat především WebAssembly, takže se pokusíme Emscripten z celé „pipeline“ vynechat. Ve skutečnosti to je v jednoduchých případech možné, protože si vystačíme s dvojicí nástrojů, tvořících jednoduchou pipeline:
- Samotného překladače clang, který dokáže provést překlad do mezikódu
- Druhého překladače llc, který mezikód přeloží do WebAssembly
Popř. můžeme do pipeline zařadit i explicitní volání nástroje opt, který dokáže provádět transformaci mezijazyk->mezijazyk s aplikací optimalizací:
- Překladač clang, který dokáže provést překlad do mezikódu
- Nástroj opt transformující mezikód do jiného mezikódu s aplikací optimalizací
- Druhý překladač llc, který mezikód přeloží do WebAssembly
11. První demonstrační příklad – překlad funkce pro součet dvou celých čísel do WebAssembly
V dnešním prvním demonstračním příkladu se pokusíme o překlad následující (triviální) funkce do WebAssembly:
int add(int a, int b) {
return a + b;
}
V prvním kroku provedeme překlad do mezikódu (prozatím bez optimalizací). Musíme uvést přepínač -S zapínající výstup do mezikódu a nastavíme i výslednou architekturu na WebAssembly:
$ clang --target=wasm32 -emit-llvm -c -S add.c
Výsledkem překladu by měl být soubor nazvaný add.ll, ve kterém je uložen zápis funkce add v mezikódu LLVM. Jedná se o obdobu klasického assembleru, ovšem určenou především pro zpracování automatizovanými nástroji, nikoli pro zápis lidmi. K tomuto strojovému kódu jsou přidána i další metadata:
; ModuleID = 'add.c'
source_filename = "add.c"
target datalayout = "e-m:e-p:32:32-p10:8:8-p20:8:8-i64:64-n32:64-S128-ni:1:10:20"
target triple = "wasm32"
; Function Attrs: noinline nounwind optnone
define hidden i32 @add(i32 noundef %0, i32 noundef %1) #0 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
store i32 %0, ptr %3, align 4
store i32 %1, ptr %4, align 4
%5 = load i32, ptr %3, align 4
%6 = load i32, ptr %4, align 4
%7 = add nsw i32 %5, %6
ret i32 %7
}
attributes #0 = { noinline nounwind optnone "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="generic" "target-features"="+mutable-globals,+sign-ext" }
!llvm.module.flags = !{!0}
!llvm.ident = !{!1}
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"clang version 18.1.8 (Fedora 18.1.8-2.fc40)"}
Ve druhém kroku zavoláme druhý (backend) překladač llc, který již vygeneruje WebAssembly:
$ llc -march=wasm32 -filetype=obj add.ll -o add.wasm
Výsledkem je soubor add.wasm, který je poměrně velký – má 299 bajtů:
$ ls -l * -rw-r--r--. 1 ptisnovs ptisnovs 44 Nov 7 15:56 add.c -rw-r--r--. 1 ptisnovs ptisnovs 801 Nov 9 10:47 add.ll -rw-r--r--. 1 ptisnovs ptisnovs 299 Nov 9 10:49 add.wasm
12. Analýza obsahu souboru add.wasm
Pro prohlédnutí obsahu binárního souboru add.wasm použijeme výše zmíněný nástroj wasm-objdump s přepínačem -d:
$ wasm-objdump add.wasm -d
Výstupem by měl být „disassemblovaný“ obsah funkce add, tj. sekvence instrukcí WebAssembly:
add.wasm: file format wasm 0x1 Code Disassembly: 00005a func[0] <add>: 00005b: 01 7f | local[2] type=i32 00005d: 23 80 80 80 80 00 | global.get 0 <env.__stack_pointer> 000063: 41 10 | i32.const 16 000065: 6b | i32.sub 000066: 22 02 | local.tee 2 000068: 20 00 | local.get 0 00006a: 36 02 0c | i32.store 2 12 00006d: 20 02 | local.get 2 00006f: 20 01 | local.get 1 000071: 36 02 08 | i32.store 2 8 000074: 20 02 | local.get 2 000076: 28 02 0c | i32.load 2 12 000079: 20 02 | local.get 2 00007b: 28 02 08 | i32.load 2 8 00007e: 6a | i32.add 00007f: 0b | end
Po zadání přepínače -x si můžeme zobrazit metadata uložená společně s kódem funkce add:
$ wasm-objdump add.wasm -x
Výsledek by měl vypadat následovně (obsahuje i typovou signaturu funkce add):
add.wasm: file format wasm 0x1 Section Details: Type[1]: - type[0] (i32, i32) -> i32 Import[2]: - memory[0] pages: initial=0 <- env.__linear_memory - global[0] i32 mutable=1 <- env.__stack_pointer Function[1]: - func[0] sig=0 <add> Code[1]: - func[0] size=38 <add> Custom: - name: "linking" - symbol table [count=2] - 0: F <add> func=0 [ binding=global vis=hidden ] - 1: G <env.__stack_pointer> global=0 [ undefined binding=global vis=default ] Custom: - name: "reloc.CODE" - relocations for section: 3 (Code) [1] - R_WASM_GLOBAL_INDEX_LEB offset=0x000006(file=0x00005e) symbol=1 <env.__stack_pointer> Custom: - name: "producers" Custom: - name: "target_features" - [+] mutable-globals - [+] sign-ext
Taktéž si můžeme nechat převést binární soubor .wasm na jeho textovou variantu .wat (což je sémantický ekvivalent, ovšem mnohem delší):
$ wasm2wat add.wasm -o add.wat
Takto by měl vypadat výsledek:
(module
(type (;0;) (func (param i32 i32) (result i32)))
(import "env" "__linear_memory" (memory (;0;) 0))
(import "env" "__stack_pointer" (global (;0;) (mut i32)))
(func $add (type 0) (param i32 i32) (result i32)
(local i32)
global.get 0
i32.const 16
i32.sub
local.tee 2
local.get 0
i32.store offset=12
local.get 2
local.get 1
i32.store offset=8
local.get 2
i32.load offset=12
local.get 2
i32.load offset=8
i32.add))
13. Zpětný překlad z WebAssembly do vysokoúrovňového jazyka
Potenciálně užitečný je i nástroj nazvaný wasm-decompile. Tento nástroj dokáže z binárního souboru .wasm vygenerovat zápis funkcí nikoli ve formě sekvence instrukcí, ale ve vysokoúrovňovém jazyku, který se do určité míry podobá céčku. Můžeme si to snadno otestovat:
$ wasm-decompile add.wasm
Výsledkem bude tento zápis, který by skutečně měl připomínat céčko (i když se například hlavička funkce podobá jazyku Go nebo spíše Pascalu):
import memory env_linear_memory;
import global env_stack_pointer:int;
function add(a:int, b:int):int {
var c:int_ptr = env_stack_pointer - 16;
c[3] = a;
c[2] = b;
return c[3] + c[2];
}
14. Vygenerování optimalizovaného kódu ve WebAssembly
Za povšimnutí stojí, že se ve vysokoúrovňovém kódu, který jsme si ukázali v předchozí kapitole, přistupuje do zdánlivě neexistujícího pole c, jehož začátek byl vypočítán na základě ukazatele na vrchol zásobníku. Tímto zápisem se vlastně simulují lokální proměnné.
Ovšem funkce add by žádné pomocné lokální proměnné nemusela potřebovat. Skutečně je tomu tak, ovšem nejprve musíme vygenerovat optimalizovaný kód ve WebAssembly. Nejjednodušší je provést tyto optimalizace přímo na úrovni clangu, například následovně:
Překlad do WebAssembly:
$ clang -O3 --target=wasm32 -emit-llvm -c -S add.c $ llc -march=wasm32 -filetype=obj add.ll -o add.wasm
Výsledkem bude soubor add.wasm, který bude mít délku jen 220 bajtů.
Prohlédnutí bajtkódu/strojového kódu:
$ wasm-objdump -d add.wasm add.wasm: file format wasm 0x1 Code Disassembly: 000043 func[0] <add>: 000044: 20 01 | local.get 1 000046: 20 00 | local.get 0 000048: 6a | i32.add 000049: 0b | end
A zpětný překlad do vysokúrovňového jazyka:
$ wasm-decompile add.wasm
import memory env_linear_memory;
function add(a:int, b:int):int {
return b + a
}
15. Bližší pohled na sekvenci instrukcí v přeložené funkci add
Ve výsledném bajtkódu funkce add přeložené do WebAssembly nalezneme pouze čtyři instrukce:
000044: 20 01 | local.get 1 000046: 20 00 | local.get 0 000048: 6a | i32.add 000049: 0b | end
Význam jednotlivých instrukcí je následující:
- Instrukce local.get načte obsah lokální proměnné určené svým indexem a uloží obsah této proměnné na zásobník operandů. Po provedení prvních dvou instrukcí tedy bude zásobník operandů obsahovat dvě hodnoty, přičemž na nejvyšším místě bude obsah první proměnné (jde o zásobník LIFO).
- Instrukce i32.add přečte ze zásobníku operandů obě hodnoty (více jich tam uloženo stejně není), provede jejich součet a výsledek součtu uloží zpět na zásobník operandů. Tato instrukce existuje ve čtyřech základních variantách: i32, i64, f32 a f64.
- Poslední instrukce end ukončuje tělo bloku. V tomto konkrétním případě se jedná o ukončení celé funkce (což je taktéž blok) a návrat do volajícího kódu. Tento kód si sám zpracuje obsah zásobníku operandů, kde je uložen výsledek součtu.
16. Druhý demonstrační příklad – překlad funkce pro součet dvou čísel typu float/single
V dnešním druhém demonstračním příkladu se pokusíme o překlad funkce, která opět vrátí součet dvou čísel, nyní ovšem typu float/single:
float fadd(float a, float b) {
return a + b;
}
Překlad do WebAssembly provedeme známým způsobem:
$ clang --target=wasm32 -emit-llvm -c -S fadd.c $ llc -march=wasm32 -filetype=obj fadd.ll -o fadd.wasm
Analýza výsledného bajtkódu:
$ wasm-objdump fadd.wasm -d fadd.wasm: file format wasm 0x1 Code Disassembly: 00005a func[0] <fadd>: 00005b: 01 7f | local[2] type=i32 00005d: 23 80 80 80 80 00 | global.get 0 <env.__stack_pointer> 000063: 41 10 | i32.const 16 000065: 6b | i32.sub 000066: 22 02 | local.tee 2 000068: 20 00 | local.get 0 00006a: 38 02 0c | f32.store 2 12 00006d: 20 02 | local.get 2 00006f: 20 01 | local.get 1 000071: 38 02 08 | f32.store 2 8 000074: 20 02 | local.get 2 000076: 2a 02 0c | f32.load 2 12 000079: 20 02 | local.get 2 00007b: 2a 02 08 | f32.load 2 8 00007e: 92 | f32.add 00007f: 0b | end
Již zde je vidět, že sekvence instrukcí je příliš dlouhá na tak jednoduchou funkci. Proto donutíme clang k optimalizacím:
$ clang -O3 --target=wasm32 -emit-llvm -c -S fadd.c $ llc -march=wasm32 -filetype=obj fadd.ll -o fadd.wasm $ wasm-objdump -d fadd.wasm
Celá funkce je přeložena do čtyř instrukcí:
fadd.wasm: file format wasm 0x1 Code Disassembly: 000043 func[0] <fadd>: 000044: 20 00 | local.get 0 000046: 20 01 | local.get 1 000048: 92 | f32.add 000049: 0b | end
Výsledek pokusu o zpětný překlad dopadne velmi dobře:
import memory env_linear_memory;
function fadd(a:float, b:float):float {
return a + b
}
000044: 20 01 | local.get 1 000044: 20 00 | local.get 0 000046: 20 00 | local.get 0 000046: 20 01 | local.get 1 000048: 6a | i32.add 000048: 92 | f32.add 000049: 0b | end 000049: 0b | end
17. Třetí demonstrační příklad – překlad funkce pro výpočet největšího společného dělitele
V dnešním posledním demonstračním příkladu si ukážeme překlad funkce pro výpočet největšího společného dělitele dvou celých (kladných) čísel. Jedna z možných implementací tohoto algoritmu vypadá následovně (samozřejmě je možné použít různé triky, aby se zamezilo použití pomocné proměnné t atd.):
int gcd(int u, int v) {
while (v) {
int t = u;
u = v;
v = t % v;
}
return u;
}
Překlad do WebAssembly provedeme nám již známým způsobem:
$ clang --target=wasm32 -emit-llvm -c -S gcd.c $ llc -march=wasm32 -filetype=obj gcd.ll -o gcd.wasm
18. Analýza obsahu souboru gcd.wasm
Výsledek překladu funkce gcd bez povolení optimalizací vede k poměrně složitému výslednému kódu (zde je zobrazen obsah textové varianty WebAssembly i se všemi dalšími metainformacemi):
(module
(type (;0;) (func (param i32 i32) (result i32)))
(import "env" "__linear_memory" (memory (;0;) 0))
(import "env" "__stack_pointer" (global (;0;) (mut i32)))
(func $gcd (type 0) (param i32 i32) (result i32)
(local i32)
global.get 0
i32.const 16
i32.sub
local.tee 2
local.get 0
i32.store offset=12
local.get 2
local.get 1
i32.store offset=8
block ;; label = @1
loop ;; label = @2
local.get 2
i32.load offset=8
i32.eqz
br_if 1 (;@1;)
local.get 2
local.get 2
i32.load offset=12
i32.store offset=4
local.get 2
local.get 2
i32.load offset=8
i32.store offset=12
local.get 2
local.get 2
i32.load offset=4
local.get 2
i32.load offset=8
i32.rem_s
i32.store offset=8
br 0 (;@2;)
end
end
local.get 2
i32.load offset=12))
Na tentýž kód se můžeme podívat i nástrojem wasm-objdump:
gcd.wasm: file format wasm 0x1 Code Disassembly: 00005a func[0] <gcd>: 00005b: 01 7f | local[2] type=i32 00005d: 23 80 80 80 80 00 | global.get 0 <env.__stack_pointer> 000063: 41 10 | i32.const 16 000065: 6b | i32.sub 000066: 22 02 | local.tee 2 000068: 20 00 | local.get 0 00006a: 36 02 0c | i32.store 2 12 00006d: 20 02 | local.get 2 00006f: 20 01 | local.get 1 000071: 36 02 08 | i32.store 2 8 000074: 02 40 | block 000076: 03 40 | loop 000078: 20 02 | local.get 2 00007a: 28 02 08 | i32.load 2 8 00007d: 45 | i32.eqz 00007e: 0d 01 | br_if 1 000080: 20 02 | local.get 2 000082: 20 02 | local.get 2 000084: 28 02 0c | i32.load 2 12 000087: 36 02 04 | i32.store 2 4 00008a: 20 02 | local.get 2 00008c: 20 02 | local.get 2 00008e: 28 02 08 | i32.load 2 8 000091: 36 02 0c | i32.store 2 12 000094: 20 02 | local.get 2 000096: 20 02 | local.get 2 000098: 28 02 04 | i32.load 2 4 00009b: 20 02 | local.get 2 00009d: 28 02 08 | i32.load 2 8 0000a0: 6f | i32.rem_s 0000a1: 36 02 08 | i32.store 2 8 0000a4: 0c 00 | br 0 0000a6: 0b | end 0000a7: 0b | end 0000a8: 20 02 | local.get 2 0000aa: 28 02 0c | i32.load 2 12 0000ad: 0b | end
Zajímavější ovšem bude provedení překladu s povolením optimalizací:
$ clang -O3 --target=wasm32 -emit-llvm -c -S gcd.c $ llc -march=wasm32 -filetype=obj gcd.ll -o gcd.wasm
Zajímavé je, že i po provedení optimalizací je stále možné „zrekonstruovat“ původní algoritmus nástrojem wasm-decompile, přičemž výsledný pseudokód bude velmi dobře čitelný:
$ wasm-decompile gcd.wasm
import memory env_linear_memory;
function gcd(a:int, b:int):int {
var c:int;
if (b) goto B_a;
return a;
label B_a:
loop L_b {
b = a % (c = b);
a = c;
if (b) continue L_b;
}
return c;
}
19. Bližší pohled na sekvenci instrukcí v přeložené funkci gcd
Nyní se pokusme analyzovat instrukce, které vznikly překladem funkce gcd do WebAssembly s povolením optimalizací. Použijeme nám již dobře známý příkaz wasm-objdump:
$ wasm-objdump -d gcd.wasm
Výsledek by měl vypadat následovně:
gcd.wasm: file format wasm 0x1 Code Disassembly: 000043 func[0] <gcd>: 000044: 01 7f | local[2] type=i32 000046: 02 40 | block 000048: 20 01 | local.get 1 00004a: 0d 00 | br_if 0 00004c: 20 00 | local.get 0 00004e: 0f | return 00004f: 0b | end 000050: 03 40 | loop 000052: 20 00 | local.get 0 000054: 20 01 | local.get 1 000056: 22 02 | local.tee 2 000058: 6f | i32.rem_s 000059: 21 01 | local.set 1 00005b: 20 02 | local.get 2 00005d: 21 00 | local.set 0 00005f: 20 01 | local.get 1 000061: 0d 00 | br_if 0 000063: 0b | end 000064: 20 02 | local.get 2 000066: 0b | end
V tomto kódu se objevují nové typy instrukcí. V první řadě můžeme vidět rozdělení instrukcí do bloků. První blok začíná instrukcí block a končí instrukcí end. Druhý blok tvoří dvojice loop a end, je tedy zřejmé, že se jedná o implementaci smyčky.
Instrukce local.get již známe. Jejich opakem je local.set, které naopak uloží do lokální proměnné definované svým indexem hodnotu ze zásobníku operandů (tato hodnota se ze zásobníku odstraní). Nová je funkce local.tee, která se od local.set odlišuje tím, že neodstraňuje hodnotu ze zásobníku operandů.
Další novou instrukcí je i32.rem_s provádějící výpočet zbytku po dělení (klasicky se získáním operandů ze zásobníku a uložením výsledku zpět na zásobník). Tato instrukce existuje jen v celočíselné variantě pro typy i32 a i64. Navíc se postfixem určuje její varianta bez znaménka _u nebo se znaménkem _s.
Předposlední novou instrukcí je instrukce return, která ukončí provádění aktuální funkce a vrací řízení kódu, ze kterého byla tato instrukce volána. Návratové hodnoty jsou určeny obsahem zásobníku operandů a taktéž signaturou funkce. Pokud zásobník operandů obsahuje více hodnot, než je specifikováno v signatuře funkce, jsou tyto nadbytečné hodnoty ignorovány.
Prozatím nejsložitější instrukcí je br_if, což je zkratka sousloví branch if. Tato instrukce přečte hodnotu ze zásobníku operandů, kterou považuje za pravdivostní hodnotu. Na základě této hodnoty se buď provede nebo naopak neprovede skok na konec bloku nebo na začátek smyčky (nová iterace smyčky). Povšimněte si, jak se tato instrukce liší od klasického podmíněného skoku, který pracuje s absolutními nebo relativními adresami. Typicky se před touto instrukcí nachází výpočet vracející 0 nebo 1 na zásobník, ovšem v našem konkrétním algoritmu je výsledek získán přímo z počítané hodnoty (while(v) ….
20. Odkazy na Internetu
- Compiling C to WebAssembly without Emscripten
https://surma.dev/things/c-to-webassembly/ - Web Assemply: Text Format
https://webassembly.github.io/spec/core/text/index.html - Web Assembly: Binary Format
https://webassembly.github.io/spec/core/binary/index.html - WebAssembly
https://webassembly.org/ - WebAssembly na Wiki Golangu
https://github.com/golang/go/wiki/WebAssembly - The future of WebAssembly – A look at upcoming features and proposals
https://blog.scottlogic.com/2018/07/20/wasm-future.html - WebAssembly Design
https://github.com/WebAssembly/design - Využití WebAssembly z programovacího jazyka Go
https://www.root.cz/clanky/vyuziti-webassembly-z-programovaciho-jazyka-go/ - WebAssembly slibuje podstatné zrychlení webů, konec JavaScriptu se ale nekoná
https://www.lupa.cz/clanky/webassembly-slibuje-podstatne-zrychleni-webu-konec-javascriptu-se-ale-nekona/ - List of languages that compile to JS
https://github.com/jashkenas/coffeescript/wiki/List-of-languages-that-compile-to-JS - asm.js
http://asmjs.org/ - Top 23 WASM Open-Source Projects
https://www.libhunt.com/topic/wasm - Made with WebAssembly
https://madewithwebassembly.com/ - The Top 1,790 Wasm Open Source Projects on Github
https://awesomeopensource.com/projects/wasm - Sanspiel
https://sandspiel.club/ - Painting on HTML5 Canvas with Rust WASM
https://www.subarctic.org/painting_on_html5_canvas_with_rust_wasm.html - Writing WebAssembly By Hand
https://blog.scottlogic.com/2018/04/26/webassembly-by-hand.html - WebAssembly Specification
https://webassembly.github.io/spec/core/index.html - Index of Instructions
https://webassembly.github.io/spec/core/appendix/index-instructions.html - The WebAssembly Binary Toolkit
https://github.com/WebAssembly/wabt - Will WebAssembly replace JavaScript? Or Will WASM Make JavaScript More Valuable in Future?
https://dev.to/vaibhavshah/will-webassembly-replace-javascript-or-will-wasm-make-javascript-more-valuable-in-future-5c6e - Webassembly as 32bit and 64bit
https://stackoverflow.com/questions/78580226/webassembly-as-32bit-and-64bit - Portability
https://webassembly.org/docs/portability/ - Hexadecimální prohlížeče a editory s textovým uživatelským rozhraním
https://www.root.cz/clanky/hexadecimalni-prohlizece-a-editory-s-textovym-uzivatelskym-rozhranim/ - Nástroj objdump: švýcarský nožík pro vývojáře
https://www.root.cz/clanky/nastroj-objdump-svycarsky-nozik-pro-vyvojare/ - Getting Started: Building and Running Clang
https://clang.llvm.org/get_started.html - Clang: a C language family frontend for LLVM
https://clang.llvm.org/ - Scheduling LLVM Passes with the New Pass Manager
https://stephenverderame.github.io/blog/scheduling_llvm/
