Pohled pod kapotu formátu WebAssembly: nízkoúrovňová náhrada JavaScriptu

11. 11. 2025
Doba čtení: 27 minut

Sdílet

Pracovníci rozebírají vnitřek PC
Autor: Shutterstock
Seznámíme se 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ř. na serverech nebo jako součást složitějších aplikací uvnitř takzvaného sandboxu.

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

5. Nástroj Emscripten

6. WebAssembly

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

20. Odkazy na Internetu

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.

Poznámka: ve skutečnosti není technologie transpřekladačů žádným způsobem svázána právě s JavaScriptem, protože se používala (a používá) i pro další manipulace se zdrojovými kódy. Ovšem právě v oblasti webového vývoje se používá velmi často, protože zde neexistuje možnost výběru cílového jazyka.

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
Poznámka: seznam všech (či alespoň většiny) známých transpřekladačů do JavaScriptu naleznete například na stránce https://github.com/jashke­nas/coffeescript/wiki/List-of-languages-that-compile-to-JS, i když je nutné varovat, že některé projekty (kromě výše zmíněných) jsou v dosti špatném stavu nebo již nejsou dále vyvíjeny.

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:

  1. Na vstupu celého procesu je program napsaný v céčku
  2. Nejprve je proveden překlad pomocí clang do mezikódu LLVM (LLVM Intermediate Representation)
  3. Následně je zavolán Fastcomp (jádro překladače Emscriptenu) pro překlad mezikódu z předchozího kroku do JavaScriptu
  4. 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.)
Poznámka: poslední překlad (do JavaScriptu) generuje kód kompatibilní s asm.js, tj. používá se zde cíleně omezená podmnožina konstrukcí JavaScriptu. Více informací o asm.js naleznete například na stránkách https://developer.mozilla.org/en-US/docs/Games/Tools/asm.js a http://asmjs.org/ (původní verze specifikace). Alternativně může být výsledkem i bajtkód pro WebAssembly, o čemž se zmíníme v dalších kapitolách, protože se jedná o hlavní téma dnešního článku.

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.

Poznámka: příště se zmíníme o Wasm-GC, který se snaží problém automatického správce paměti centralizovaně řešit, bez návaznosti na konkrétní runtime nějakého programovacího jazyka.

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.

Poznámka: naše testovací projekty s jedinou funkcí budou mít velikost nepřesahující několik stovek bajtů.

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ž f32i32 mají v tomto případě shodnou bitovou šířku).

Instrukce jsou rozděleny do několika oblastí:

  1. Řízení toku programu
  2. Operace typu Load a Store (práce s operační pamětí)
  3. Práce s proměnnými a parametry
  4. Aritmetické instrukce pro celočíselné operandy
  5. Aritmetické instrukce pro operandy s plovoucí řádovou čárkou
  6. Instrukce pro porovnání celočíselných operandů
  7. Instrukce pro porovnání operandů s plovoucí řádovou čárkou
  8. 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:

  1. Samotného překladače clang, který dokáže provést překlad do mezikódu
  2. 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í:

  1. Překladač clang, který dokáže provést překlad do mezikódu
  2. Nástroj opt transformující mezikód do jiného mezikódu s aplikací optimalizací
  3. 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í:

  1. 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).
  2. 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.
  3. 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.
Poznámka: zde se skutečně použije instrukce end a nikoli return!

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
}
Poznámka: oba výsledné bajtkódy se odlišují pouze třetí instrukcí. Vždy se provede součet dvou hodnot získaných ze zásobníku operandů, ovšem v prvním příkladu se předpokládá, že hodnoty jsou dvě celá čísla, kdežto ve druhém případě se jedná o hodnoty typu float/single:
 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
Poznámka: samozřejmě lze povolit optimalizace přepínačem -O3 (na rychlost) nebo -Os (na velikost).

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;
}
Poznámka: to ovšem znamená, že i když je WebAssembly binárním formátem, není možné se spoléhat na to, že „skryjete“ nějaký sofistikovaný algoritmus před jeho zkoumáním dalšími vývojáři.

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.

Školení Kubernetes

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

  1. Compiling C to WebAssembly without Emscripten
    https://surma.dev/things/c-to-webassembly/
  2. Web Assemply: Text Format
    https://webassembly.github­.io/spec/core/text/index.html
  3. Web Assembly: Binary Format
    https://webassembly.github­.io/spec/core/binary/index­.html
  4. WebAssembly
    https://webassembly.org/
  5. WebAssembly na Wiki Golangu
    https://github.com/golang/go/wi­ki/WebAssembly
  6. The future of WebAssembly – A look at upcoming features and proposals
    https://blog.scottlogic.com/2018/07/20/wasm-future.html
  7. WebAssembly Design
    https://github.com/WebAssembly/design
  8. Využití WebAssembly z programovacího jazyka Go
    https://www.root.cz/clanky/vyuziti-webassembly-z-programovaciho-jazyka-go/
  9. 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/
  10. List of languages that compile to JS
    https://github.com/jashke­nas/coffeescript/wiki/List-of-languages-that-compile-to-JS
  11. asm.js
    http://asmjs.org/
  12. Top 23 WASM Open-Source Projects
    https://www.libhunt.com/topic/wasm
  13. Made with WebAssembly
    https://madewithwebassembly.com/
  14. The Top 1,790 Wasm Open Source Projects on Github
    https://awesomeopensource­.com/projects/wasm
  15. Sanspiel
    https://sandspiel.club/
  16. Painting on HTML5 Canvas with Rust WASM
    https://www.subarctic.org/pa­inting_on_html5_canvas_wit­h_rust_wasm.html
  17. Writing WebAssembly By Hand
    https://blog.scottlogic.com/2018/04/26/we­bassembly-by-hand.html
  18. WebAssembly Specification
    https://webassembly.github­.io/spec/core/index.html
  19. Index of Instructions
    https://webassembly.github­.io/spec/core/appendix/in­dex-instructions.html
  20. The WebAssembly Binary Toolkit
    https://github.com/WebAssembly/wabt
  21. 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
  22. Webassembly as 32bit and 64bit
    https://stackoverflow.com/qu­estions/78580226/webassem­bly-as-32bit-and-64bit
  23. Portability
    https://webassembly.org/doc­s/portability/
  24. Hexadecimální prohlížeče a editory s textovým uživatelským rozhraním
    https://www.root.cz/clanky/he­xadecimalni-prohlizece-a-editory-s-textovym-uzivatelskym-rozhranim/
  25. Nástroj objdump: švýcarský nožík pro vývojáře
    https://www.root.cz/clanky/nastroj-objdump-svycarsky-nozik-pro-vyvojare/
  26. Getting Started: Building and Running Clang
    https://clang.llvm.org/get_star­ted.html
  27. Clang: a C language family frontend for LLVM
    https://clang.llvm.org/
  28. Scheduling LLVM Passes with the New Pass Manager
    https://stephenverderame.git­hub.io/blog/scheduling_llvm/

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.