Běh aplikací psaných v C či C++ využívajících knihovnu SDL v okně webového prohlížeče

13. 9. 2022
Doba čtení: 32 minut

Sdílet

 Autor: Depositphotos
Ukážeme si, jak provozovat aplikace psané v C či C++, které využívají knihovny SDL nebo SDL2, přímo v okně webového prohlížeče. Zdrojové kódy lze totiž přeložit nástrojem Emscripten buď do WebAssembly nebo do asm.js.

Obsah

1. Běh aplikací psaných v C či C++ využívajících knihovnu SDL v okně webového prohlížeče

2. Malé ohlédnutí: způsob využití různých programovacích jazyků na webových stránkách

3. Transpřekladače do JavaScriptu

4. WebAssembly

5. Nástroj Emscripten se představuje

6. Stručný popis využití celého toolchainu Emscriptenu

7. Běh grafických aplikací naprogramovaných v céčku a využívajících SDL a SDL2 ve webovém prohlížeči

8. Podpora SDL i SDL2

9. Instalace závislostí pro Emscripten (Fedora 36)

10. Instalace samotného Emscriptenu

11. Testovací příklad určený pro překlad do binární nativní aplikace

12. Úprava příkladu pro Emscripten

13. Překlad demonstračního příkladu do WebAssembly

14. Načtení příkladu přes webový server

15. Překlad příkladu do JavaScriptu (asm.js) s jeho spuštěním

16. Příklad s jednoduchou animací: nativní verze

17. Příklad s jednoduchou animací: úprava pro Emscripten

18. Příloha: Makefile pro překlad všech demonstračních příkladů

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Běh aplikací psaných v C či C++ využívajících knihovnu SDL v okně webového prohlížeče

V dnešním článku si ukážeme, jakým způsobem je možné spouštět aplikace naprogramované v jazycích C či C++, které využívají knihovny SDL1 nebo SDL2, přímo v okně webového prohlížeče. Zdrojové kódy těchto aplikací je totiž možné přeložit nástrojem Emscripten buď do bajtkódu WebAssembly (WASM) nebo do asm.js, což je podmnožina JavaScriptu. I přesto, že ekosystém postavený okolo nástroje Emscriptenu a WebAssembly podporuje knihovny SDL i SDL2, je většinou nutné aplikace alespoň minimálně modifikovat. Typicky se jedná o nutnost úpravy smyčky událostí (event loop), kterou lze v nativních aplikacích spustit přímo, zatímco ve webovém prohlížeči je řízení ponecháno na runtime a nastavuje se například funkcí https://emscripten.org/doc­s/api_reference/emscripten­.h.html#c.emscripten_set_ma­in_loop.

Poznámka: díky těmto technologiím je například možné ve webovém prohlížeči spouštět DOSové hry přes DOSBox přeložený do WebAssembly.

2. Malé ohlédnutí: způsob využití různých programovacích jazyků na webových stránkách

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

Pravděpodobně nejjednodušší a taktéž i 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í 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í dalších skriptovacích jazyků do webových prohlížečů (z historického pohledu se jednalo minimálně o Tcl, později VBScript, a v neposlední řadě o 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 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, musely psát pouze v JavaScriptu, jenž 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 pořád ještě vzniká mnoho projektů, jejichž cílem je umožnit tvorbu webových aplikací pro prohlížeč v jiných programovacích jazycích. Zdrojové kódy je pak nutné nějaký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í je hned několik – lze použít plugin (velmi problematické a dnes značně nepopulární řešení – ovlivněné především bezpečnostními problémy, které se v minulosti některým nejmenovaným pluginům nevyhnuly), 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 ve stručnosti zabývat v navazujících kapitolách. A zapomenout v žádném případě nesmíme na technologii WebAssembly (WASM), kterou ostatně dnes využijeme.

3. Transpřekladače 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í jen jednou na vývojářském počítači, samotní klienti již mají k dispozici JavaScriptový kód – což znamená, že vývojář může použít jakýkoli vhodný jazyk, zatímco uživatel si vystačí s běžným webovým prohlížečem.

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; ostatně jedná se o téma, kterému jsme se na stránkách Roota již několikrát věnovali – připomeňme si například projekt Coconut, MoonScript či ClojureScript).

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 (což je z dnešního pohledu už obskurní jazyk):

<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 v prohlížeči, jiné provádí překlad do JavaScriptu na příkazovém řádku, existují i kombinace obou způsobů (opět viz 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
12 Fable transpřekladač jazyka F# do JavaScriptu (přes Babel)
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.

4. WebAssembly

Konečně se dostáváme k technologii 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 (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á (například v případě virtuálního stroje Javy – JVM – je tomu jinak). To je ovšem v mnoha ohledech výhoda, protože u 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) a u Rustu je řešen již v čase překladu. Správa paměti řízená přímo WebAssembly je prozatím ve fázi vývoje a dnes 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.

5. Nástroj Emscripten se představuje

Co však mají společného technologie zmíněné ve třetí a ve čtvrté kapitole? Obě technologie (kromě jiného) ve větší či menší míře umožňují resp. usnadňují tvorbu aplikací určených pro běh v rámci webového prohlížeče, a to napsaného nikoli přímo v JavaScriptu, ale v prakticky libovolném programovacím jazyku. Existuje zde jedna podmínka: pro daný jazyk musí existovat buď transpřekladač do JavaScriptu nebo překladač do bajtkódu kompatibilního s WebAssembly. A právě na tomto místě se setkáváme s nástrojem s poněkud nevyslovitelným názvem Emscripten. Tento nástroj umožňuje – nyní z pohledu běžného vývojáře – překlad kódu z libovolného jazyka podporovaného LLVM (Rust, C, C++, Objective C, D, Ada, Fortran atd.) buď do JavaScriptu nebo do bajtkódu WebAssembly.

Přitom jsou pro tento účel využívány další nástroje, typicky překladače výše zmíněných jazyků do LLVM IR (takzvaný intermediate code neboli mezikód). Příkladem takového překladače je Clang určený pro programovací jazyky C, C++ a Objective-C. Výsledkem překladu a současně i první fáze optimalizací je právě mezikód neboli již zmíněný LLVM IR. Tento mezikód se následně přeloží buď do nativního kódu (což nás dnes nezajímá) nebo přes Emscripten do JavaScriptu (asm.js) popř. do bajtkódu WebAssembly. Přitom jsou prováděny další optimalizace, tentokrát již s ohledem na cílovou platformu.

6. Stručný popis využití celého toolchainu Emscriptenu

Podívejme se nyní ve stručnosti na základní 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 IR (LLVM Intermediate Representation)
  3. Následně je zavolán Fastcomp (což je jádro překladače Emscriptenu) pro překlad mezikódu z předchozího kroku do JavaScriptu
  4. Výsledkem překladu je jak kód v JavaScriptu, tak i kód webové stránky určený pro spuštění tohoto kódu
  5. 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).

Překlad do WebAssembly je jen nepatrně odlišný:

  1. Na vstupu celého procesu je program napsaný v céčku
  2. Nejprve je proveden překlad pomocí clang do mezikódu LLVM IR (LLVM Intermediate Representation)
  3. Následně je zavolán Fastcomp (což je jádro překladače Emscriptenu) pro překlad mezikódu z předchozího kroku do bajtkódu WebAssembly
  4. Výsledkem překladu je jak binární soubor .wasm, tak i podpůrný kód v JavaScriptu i kód webové stránky určený pro spuštění nové aplikace

7. Běh grafických aplikací naprogramovaných v céčku a využívajících SDL a SDL2 ve webovém prohlížeči

Výše uvedený postup překladu programů původně určených pro překlad do nativního kódu do WebAssembly nebo asm.js však ještě nenabízí úplné řešení tohoto problému. Navíc je totiž nutné, aby programy měly přístup ke všem knihovním funkcím, které používají. Týká se to například knihoven pro grafiku (tu lze emulovat nebo volat příslušné funkce dostupné prohlížeči), práci s vlákny (do jisté míry lze taktéž emulovat), práci se sítí atd. Navíc je nutné vyřešit i další problematiku – kód, který načítá dynamické knihovny, protože ty nejsou v současné verzi komba Emscripten+WebAssembly podporovány (viz též https://github.com/imvu/em­scripten/wiki/Library-Support).

8. Podpora SDL i SDL2

Nicméně i přes potenciální problémy zmíněné výše je v případě knihoven SDL (první verze) i SDL2 tato situace již vyřešena a zejména jednodušší projekty potřebují jen minimální zásahy do svého zdrojového kódu.

Navíc je možné pro grafický výstup použít i EGL a OpenGL, což je však téma, které přesahuje kontext dnešního článku.

9. Instalace závislostí pro Emscripten (Fedora 36)

Pro lokální instalaci Emscriptenu je vyžadováno, aby operační systém obsahoval několik balíčků. Většinou mají vývojáři tyto balíčky již dávno nainstalovány, ovšem pro úplnost si ukažme instalaci těchto závislostí na minimální instalaci Fedory 36 (podobné je tomu ovšem například i na Debianu atd. – pouze se použije odlišný správce balíčků).

Další kroky byly provedeny na systému:

# cat /etc/redhat-release
 
Fedora release 36 (Thirty Six)

Pro některé dále popsané operace je vyžadován nástroj CMake:

# dnf install cmake
 
Last metadata expiration check: 0:15:51 ago on Fri 09 Sep 2022 01:58:11 PM EDT.
Dependencies resolved.
================================================================================
 Package              Arch       Version                Repository         Size
================================================================================
Installing:
 cmake                x86_64     3.22.2-1.fc36          beaker-Fedora     6.3 M
Installing dependencies:
 cmake-data           noarch     3.22.2-1.fc36          beaker-Fedora     1.6 M
 cmake-filesystem     x86_64     3.22.2-1.fc36          beaker-Fedora      18 k
 cmake-rpm-macros     noarch     3.22.2-1.fc36          beaker-Fedora      17 k
 jsoncpp              x86_64     1.9.5-2.fc36           beaker-Fedora      98 k
 libuv                x86_64     1:1.44.2-1.fc36        updates           151 k
 rhash                x86_64     1.4.2-2.fc36           beaker-Fedora     185 k
 
Transaction Summary
================================================================================
Install  7 Packages
 
Total download size: 8.3 M
Installed size: 31 M
Is this ok [y/N]:

Dále je vyžadován Git (ale ten pravděpodobně již máte dávno nainstalovaný):

# dnf install git
 
Last metadata expiration check: 0:18:15 ago on Fri 09 Sep 2022 01:58:11 PM EDT.
Dependencies resolved.
================================================================================
 Package              Arch       Version                Repository         Size
================================================================================
Installing:
 git                  x86_64     2.37.3-1.fc36          updates            65 k
Installing dependencies:
 git-core             x86_64     2.37.3-1.fc36          updates           4.0 M
 git-core-doc         noarch     2.37.3-1.fc36          updates           2.4 M
 perl-Error           noarch     1:0.17029-8.fc36       beaker-Fedora      41 k
 perl-File-Find       noarch     1.39-486.fc36          beaker-Fedora      30 k
 perl-Git             noarch     2.37.3-1.fc36          updates            42 k
 perl-TermReadKey     x86_64     2.38-12.fc36           beaker-Fedora      36 k
 perl-lib             x86_64     0.65-486.fc36          beaker-Fedora      20 k
 
Transaction Summary
================================================================================
Install  8 Packages
 
Total download size: 6.7 M
Installed size: 34 M
Is this ok [y/N]:

V průběhu instalace se používá i klasický nástroj tar, který kupodivu v minimální instalaci Fedory 36 nenajdeme:

# dnf install tar
 
Last metadata expiration check: 0:23:59 ago on Fri 09 Sep 2022 01:58:11 PM EDT.
Dependencies resolved.
================================================================================
 Package     Architecture   Version                 Repository             Size
================================================================================
Installing:
 tar         x86_64         2:1.34-3.fc36           beaker-Fedora         877 k
 
Transaction Summary
================================================================================
Install  1 Package
 
Total download size: 877 k
Installed size: 3.0 M
Is this ok [y/N]: y

10. Instalace samotného Emscriptenu

Další kroky, které si popíšeme v této kapitole, povedou k instalaci samotného Emscriptenu. Výhodou přitom je, že celá instalace je „lokální“ v tom ohledu, že nezasahuje do systémových adresářů a tudíž ani nevyžaduje práva roota. Do určité míry se tak podobá například virtual environmentu Pythonu.

Nejprve si naklonujeme repositář s podpůrnými skripty Emscriptenu:

$ git clone https://github.com/emscripten-core/emsdk.git
 
Cloning into 'emsdk'...
remote: Enumerating objects: 3390, done.
remote: Counting objects: 100% (6/6), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 3390 (delta 0), reused 4 (delta 0), pack-reused 3384
Receiving objects: 100% (3390/3390), 1.96 MiB | 9.75 MiB/s, done.
Resolving deltas: 100% (2220/2220), done.

Přesuneme se do naklonovaného repositáře:

$ cd emsdk

Provedeme lokální instalaci poslední verze Emscriptenu:

$ ./emsdk install latest
 
Resolving SDK alias 'latest' to '3.1.20'
Resolving SDK version '3.1.20' to 'sdk-releases-upstream-d92c8639f406582d70a5dde27855f74ecf602f45-64bit'
Installing SDK 'sdk-releases-upstream-d92c8639f406582d70a5dde27855f74ecf602f45-64bit'..
 
Done installing tool 'releases-upstream-d92c8639f406582d70a5dde27855f74ecf602f45-64bit'.
Done installing SDK 'sdk-releases-upstream-d92c8639f406582d70a5dde27855f74ecf602f45-64bit'.

A následně tuto verzi aktivujeme:

$ ./emsdk activate latest
 
Resolving SDK alias 'latest' to '3.1.20'
Resolving SDK version '3.1.20' to 'sdk-releases-upstream-d92c8639f406582d70a5dde27855f74ecf602f45-64bit'
Setting the following tools as active:
   node-14.18.2-64bit
   releases-upstream-d92c8639f406582d70a5dde27855f74ecf602f45-64bit
 
Next steps:
- To conveniently access emsdk tools from the command line,
  consider adding the following directories to your PATH:
    /home/tester/emsdk
    /home/tester/emsdk/node/14.18.2_64bit/bin
    /home/tester/emsdk/upstream/emscripten
- This can be done for the current shell by running:
    source "/home/tester/emsdk/emsdk_env.sh"
- Configure emsdk in your shell startup scripts by running:
    echo 'source "/home/tester/emsdk/emsdk_env.sh"' >> $HOME/.bash_profile

11. Testovací příklad určený pro překlad do binární nativní aplikace

Ukažme si nyní velmi jednoduchou aplikaci, která po svém spuštění zobrazí barvovou paletu, resp. přesněji řečeno gradientní barvový přechod. Nativní varianta této aplikace vypadá následovně:

Obrázek 1: Dnešní první demonstrační příklad po spuštění (jako nativní aplikace).

Samotný zdrojový kód aplikace je sice relativně dlouhý, ovšem většinou se jedná o reakci na chyby, které mohou nastat. V krátkosti lze její funkci popsat takto:

  1. Inicializace knihovny SDL
  2. Otevření okna, do kterého se bude provádět vykreslení barvové palety
  3. Získání „grafické plochy“ určené pro vykreslení a reprezentující plochu okna
  4. Konstrukce druhé „grafické plochy“, která bude použita de facto jako druhý zadní buffer (back buffer)
  5. Modifikace všech pixelů zadního bufferu – vykreslení barvového gradientu
  6. Vykreslení zadního bufferu do plochy okna (BitBLT – SDL_BlitSurface)
  7. Čekání na ukončení aplikace nebo stisk klávesy Esc popř. Q/q
  8. Uvolnění všech prostředků – grafických ploch i okna aplikace
Poznámka: použití grafických ploch (surface) tímto způsobem je typické spíše pro aplikace naprogramované v SDL 1. Pro SDL 2 se doporučuje používat grafické plochy uložené jako textury v paměti grafického akcelerátoru (což je důvod, proč není SDL 2 kompatibilní s SDL 1). To ovšem nemá pro tento konkrétní příklad velký význam.

Ukažme si nyní, jak vypadá zdrojový kód této aplikace:

#include <SDL2/SDL.h>
 
#define TITLE "SDL2 demo #1"
#define WIDTH  256
#define HEIGHT 256
 
typedef struct State {
    SDL_Window *window;
    SDL_Surface *screen_surface;
} State;
 
static void init_sdl(State * state, const int width, const int height)
{
    state->window = NULL;
    state->screen_surface = NULL;
 
    /* vlastni inicializace SDL */
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        fprintf(stderr, "Error initializing SDL: %s\n", SDL_GetError());
        exit(1);
    } else {
        puts("SDL_Init ok");
    }
 
    /* inicializace okna pro vykreslovani */
    state->window =
        SDL_CreateWindow(TITLE, SDL_WINDOWPOS_UNDEFINED,
                         SDL_WINDOWPOS_UNDEFINED, width, height,
                         SDL_WINDOW_SHOWN);
 
    if (!state->window) {
        puts("Error creating window");
        puts(SDL_GetError());
        exit(1);
    } else {
        puts("SDL_CreateWindow ok");
    }
 
    /* ziskani kreslici plochy okna */
    state->screen_surface = SDL_GetWindowSurface(state->window);
 
    if (!state->screen_surface) {
        fprintf(stderr, "Error setting video mode: %s\n", SDL_GetError());
        exit(1);
    } else {
        puts("SDL_GetWindowSurface ok");
    }
}
 
static void show_pixmap(State * state, SDL_Surface * surface)
{
    /* vykresleni pixmapy do plochy okna */
    SDL_BlitSurface(surface, NULL, state->screen_surface, NULL);
    SDL_UpdateWindowSurface(state->window);
}
 
static void draw_palette(SDL_Surface * surface)
{
    Uint8 *pixel;
    int x, y;
 
    if (SDL_MUSTLOCK(surface)) {
        SDL_LockSurface(surface);
    }
 
    /* nyni jiz muzeme pristupovat k pixelum pixmapy */
    pixel = (Uint8 *) surface->pixels;
 
    for (y = 0; y < surface->h; y++) {
        for (x = 0; x < surface->w; x++) {
            /* nastaveni barvy pixelu, ignorovani pruhlednosti */
            *pixel++ = x;
            *pixel++ = y;
            *pixel++ = 128;
            pixel++;
        }
    }
 
    if (SDL_MUSTLOCK(surface))
        SDL_UnlockSurface(surface);
 
}
 
static void main_event_loop(void)
{
    SDL_Event event;
 
    while (1) {
        /*SDL_WaitEvent(&event); */
        while (SDL_PollEvent(&event)) {
            switch (event.type) {
            case SDL_QUIT:
                return;
                break;          /* zbytecne, ale musime uchlacholit linter */
            case SDL_KEYDOWN:
                switch (event.key.keysym.sym) {
                case SDLK_ESCAPE:
                case SDLK_q:
                    return;
                    break;      /* zbytecne, ale musime uchlacholit linter */
                default:
                    break;
                }
                break;
            default:
                break;
            }
        }
    }
}
 
static void finalize(State * state, SDL_Surface * pixmap)
{
    /* uvolneni vsech prostredku */
    SDL_FreeSurface(pixmap);
    SDL_FreeSurface(state->screen_surface);
    SDL_DestroyWindow(state->window);
}
 
static SDL_Surface *create_pixmap(const int width, const int height)
{
    SDL_Surface *pixmap;
    pixmap =
        SDL_CreateRGBSurface(SDL_SWSURFACE, WIDTH, HEIGHT, 32, 0x00ff0000,
                             0x0000ff00, 0x000000ff, 0x00000000);
    if (!pixmap) {
        puts("Can not create pixmap");
        exit(1);
    } else {
        puts("Off screen pixmap created");
    }
    return pixmap;
}
 
int main(int argc, char **argv)
{
    State state;
    SDL_Surface *pixmap;
 
    /* inicializace SDL, vytvoreni okna a ziskani kreslici plochy okna */
    init_sdl(&state, WIDTH, HEIGHT);
 
    /* vytvoreni offscreen pixmapy */
    pixmap = create_pixmap(WIDTH, HEIGHT);
 
    /* vykresleni palety */
    draw_palette(pixmap);
 
    /* zobrazeni palety */
    show_pixmap(&state, pixmap);
 
    /* cekani na stisk klavesy */
    main_event_loop();
 
    /* uvolneni prostredku */
    finalize(&state, pixmap);
 
    return 0;
}

Překlad se provede takto:

$ gcc -Wall -std=c89 -pedantic t2.c -lSDL2

12. Úprava příkladu pro Emscripten

Jak jsme si již řekli v předchozí kapitole, je příklad určen pro překlad do nativního kódu. Aby bylo možné tento příklad spustit ve webovém prohlížeči (po jeho překladu do WebAssembly nebo do JavaScriptu), musíme odkomentovat tu část kódu, která spouští smyčku pro obsluhu událostí – jedná se totiž o jedinou vlastnost SDL, kterou je nutné pouze odsimulovat. Vzhledem k tomu, že nepotřebujeme vytvořit animaci ani reagovat na stisk kláves nebo pohyb myši, lze zpracování událostí zcela odstranit. Pro tento účel využijeme existenci resp. neexistenci symbolu __EMSCRIPTEN__ ve fázi preprocessingu – viz též zvýrazněné části:

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
 
#include <SDL2/SDL.h>
 
#define TITLE "SDL2 demo #1"
#define WIDTH  256
#define HEIGHT 256
 
typedef struct State {
    SDL_Window *window;
    SDL_Surface *screen_surface;
} State;
 
static void init_sdl(State * state, const int width, const int height)
{
    state->window = NULL;
    state->screen_surface = NULL;
 
    /* vlastni inicializace SDL */
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        fprintf(stderr, "Error initializing SDL: %s\n", SDL_GetError());
        exit(1);
    } else {
        puts("SDL_Init ok");
    }
 
    /* inicializace okna pro vykreslovani */
    state->window =
        SDL_CreateWindow(TITLE, SDL_WINDOWPOS_UNDEFINED,
                         SDL_WINDOWPOS_UNDEFINED, width, height,
                         SDL_WINDOW_SHOWN);
 
    if (!state->window) {
        puts("Error creating window");
        puts(SDL_GetError());
        exit(1);
    } else {
        puts("SDL_CreateWindow ok");
    }
 
    /* ziskani kreslici plochy okna */
    state->screen_surface = SDL_GetWindowSurface(state->window);
 
    if (!state->screen_surface) {
        fprintf(stderr, "Error setting video mode: %s\n", SDL_GetError());
        exit(1);
    } else {
        puts("SDL_GetWindowSurface ok");
    }
}
 
static void show_pixmap(State * state, SDL_Surface * surface)
{
    /* vykresleni pixmapy do plochy okna */
    SDL_BlitSurface(surface, NULL, state->screen_surface, NULL);
    SDL_UpdateWindowSurface(state->window);
}
 
static void draw_palette(SDL_Surface * surface)
{
    Uint8 *pixel;
    int x, y;
 
    if (SDL_MUSTLOCK(surface)) {
        SDL_LockSurface(surface);
    }
 
    /* nyni jiz muzeme pristupovat k pixelum pixmapy */
    pixel = (Uint8 *) surface->pixels;
 
    for (y = 0; y < surface->h; y++) {
        for (x = 0; x < surface->w; x++) {
            /* nastaveni barvy pixelu, ignorovani pruhlednosti */
            *pixel++ = x;
            *pixel++ = y;
            *pixel++ = 128;
            pixel++;
        }
    }
 
    if (SDL_MUSTLOCK(surface))
        SDL_UnlockSurface(surface);
 
}
 
static void main_event_loop(void)
{
    SDL_Event event;
 
    while (1) {
        /*SDL_WaitEvent(&event); */
        while (SDL_PollEvent(&event)) {
            switch (event.type) {
            case SDL_QUIT:
                return;
                break;          /* zbytecne, ale musime uchlacholit linter */
            case SDL_KEYDOWN:
                switch (event.key.keysym.sym) {
                case SDLK_ESCAPE:
                case SDLK_q:
                    return;
                    break;      /* zbytecne, ale musime uchlacholit linter */
                default:
                    break;
                }
                break;
            default:
                break;
            }
        }
    }
}
 
static void finalize(State * state, SDL_Surface * pixmap)
{
    /* uvolneni vsech prostredku */
    SDL_FreeSurface(pixmap);
    SDL_FreeSurface(state->screen_surface);
    SDL_DestroyWindow(state->window);
}
 
static SDL_Surface *create_pixmap(const int width, const int height)
{
    SDL_Surface *pixmap;
    pixmap =
        SDL_CreateRGBSurface(SDL_SWSURFACE, WIDTH, HEIGHT, 32, 0x00ff0000,
                             0x0000ff00, 0x000000ff, 0x00000000);
    if (!pixmap) {
        puts("Can not create pixmap");
        exit(1);
    } else {
        puts("Off screen pixmap created");
    }
    return pixmap;
}
 
int main(int argc, char **argv)
{
    State state;
    SDL_Surface *pixmap;
 
    /* inicializace SDL, vytvoreni okna a ziskani kreslici plochy okna */
    init_sdl(&state, WIDTH, HEIGHT);
 
    /* vytvoreni offscreen pixmapy */
    pixmap = create_pixmap(WIDTH, HEIGHT);
 
    /* vykresleni palety */
    draw_palette(pixmap);
 
    /* zobrazeni palety */
    show_pixmap(&state, pixmap);
 
#ifndef __EMSCRIPTEN__
    /* cekani na stisk klavesy */
    main_event_loop();
 
    /* uvolneni prostredku */
    finalize(&state, pixmap);
#endif
 
    return 0;
}
Poznámka: v této podobě je stále možné příklad přeložit do nativního kódu.

13. Překlad příkladu do WebAssembly

Podívejme se nyní na způsob překladu výše uvedeného demonstračního příkladu do bajtkódu WebAssembly. Nejprve je nutné nastavit prostředí Emscriptenu, a to přímo z adresáře, do kterého byl Emscripten naklonován (viz desátou kapitolu):

$ source ./emsdk_env.sh
 
Setting up EMSDK environment (suppress these messages with EMSDK_QUIET=1)
Adding directories to PATH:
PATH += /home/tester/emsdk
PATH += /home/tester/emsdk/upstream/emscripten
PATH += /home/tester/emsdk/node/14.18.2_64bit/bin
 
Setting environment variables:
PATH = /home/tester/emsdk:/home/tester/emsdk/upstream/emscripten:/home/tester/emsdk/node/14.18.2_64bit/bin:/home/tester/.local/bin:/home/tester/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
EMSDK = /home/tester/emsdk
EM_CONFIG = /home/tester/emsdk/.emscripten
EMSDK_NODE = /home/tester/emsdk/node/14.18.2_64bit/bin/node

Následně provedeme překlad do objektového kódu:

$ emcc -c sdl_test1.c -o sdl_test1.o -s USE_SDL=2
 
ports:INFO: retrieving port: sdl2 from https://github.com/libsdl-org/SDL/archive/0fcfaf9e9482953ee425cc15b91160b92de3df7f.zip
ports:INFO: unpacking port: sdl2
cache:INFO: generating port: sysroot/lib/wasm32-emscripten/libSDL2.a... (this will be cached in "/home/tester/emsdk/upstream/emscripten/cache/sysroot/lib/wasm32-emscripten/libSDL2.a" for subsequent builds)
cache:INFO:  - ok
Poznámka: cache se vytvoří pouze při prvním překladu.

Následně si necháme vygenerovat HMTL stránku, JavaScriptový podpůrný kód i vlastní WebAssembly s demem:

$ emcc sdl_test1.o -o sdl_test1.html -s USE_SDL=2

Výsledkem budou tyto soubory:

-rw-rw-r-- 1 ptisnovs ptisnovs 102510 Sep 12 10:46 sdl_demo_1B.html
-rw-rw-r-- 1 ptisnovs ptisnovs 365717 Sep 12 10:46 sdl_demo_1B.js
-rw-rw-r-- 1 ptisnovs ptisnovs   3136 Sep 12 10:46 sdl_demo_1B.o
-rwxrwxr-x 1 ptisnovs ptisnovs 443198 Sep 12 10:46 sdl_demo_1B.wasm
Poznámka: nutno uznat, že velikosti JavaScriptového podpůrného skriptu i vlastního bajtkódu jsou až neuvěřitelně velké (navíc se jedná o původně céčkový kód, takže není slinkován kód pro garbage collector ani pro celý runtime, jak je tomu například v případě jazyka Go). Ovšem ve skutečnosti bude společně se zvyšující se složitostí aplikace růst velikost těchto souborů pouze minimálně.

14. Načtení příkladu přes webový server

V případě, že se pokusíte otevřít vygenerovanou stránku v HTML prohlížeči, pochopitelně se tato stránka načte a zobrazí, ale samotná aplikace se již nemusí spustit:

Obrázek 2: Takto vypadá stránka ve chvíli, kdy aplikace není načtena.

Proč tomu tak je? Ve skutečnosti se načte pouze daná stránka a skript, ale nikoli již bajtkód WebAssembly:

Obrázek 3: Důvod, proč se aplikace nenačetla.

Důvody pro toto chování jsou vysvětleny na stránce https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Error­s/CORSRequestNotHttp (musím se přiznat, že osobně těm důvodům příliš nerozumím, což je jeden z důvodů, proč se ani nesnažím pochopit celou logiku „moderního“ front-endu :-)

Samozřejmě je možné změnit konfiguraci webového prohlížeče. Alternativně lze spustit jednoduchý webový server v adresáři s přeloženou aplikací, což již webový prohlížeč uspokojí:

$ python3 -m http.server 9000
 
Serving HTTP on 0.0.0.0 port 9000 (http://0.0.0.0:9000/) ...

Dále otevřít stránku na adrese localhost:9000:

Obrázek 4: Soubory nabízené právě spuštěným webovým serverem.

127.0.0.1 - - [09/Sep/2022 20:31:52] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [09/Sep/2022 20:31:52] code 404, message File not found
127.0.0.1 - - [09/Sep/2022 20:31:52] "GET /favicon.ico HTTP/1.1" 404 -
127.0.0.1 - - [09/Sep/2022 20:31:55] "GET /sdl_test1.html HTTP/1.1" 200 -
127.0.0.1 - - [09/Sep/2022 20:31:55] "GET /sdl_test1.js HTTP/1.1" 200 -
127.0.0.1 - - [09/Sep/2022 20:31:55] "GET /sdl_test1.wasm HTTP/1.1" 200 -

A vybrat stránku sdl_demo_1B.html:

Obrázek 5: Nyní se již naše demo aplikace korektně spustí.

15. Překlad příkladu do JavaScriptu (asm.js) s jeho spuštěním

Alternativně můžeme naši aplikaci přeložit do JavaScriptu, konkrétně do jeho podmnožiny nazývané asm.js. K tomuto účelu je nutné provést jedinou změnu, a to předání parametru -s WASM=0 do linkeru:

$ emcc -c t2.c -o t2.o -s USE_SDL=2
$ emcc t2.o -o t2.html -s USE_SDL=2 -s WASM=0

Nyní bude výsledkem překladu jen trojice souborů, ovšem sdl_demo_1B.js poměrně podstatným způsobem naroste, protože bude obsahovat mechanicky přeložený LLVM IR:

-rw-rw-r-- 1 ptisnovs ptisnovs  102510 Sep 12 12:27 sdl_demo_1B.html
-rw-rw-r-- 1 ptisnovs ptisnovs 3502214 Sep 12 12:27 sdl_demo_1B.js
-rw-rw-r-- 1 ptisnovs ptisnovs    3136 Sep 12 12:27 sdl_demo_1B.o

Aplikaci bude možné spustit přímo ve webovém prohlížeči otevřením stránky sdl_demo_1B.html:

Obrázek 6: Druhá podoba naší demo aplikace.

Poznámka: aplikace vypadá z pohledu uživatele naprosto stejně, jako aplikace z obrázku číslo 5, ovšem interně je založena na zcela odlišné technologii (a pravděpodobně bude i pomalejší – to ovšem zjistíme až z benchmarků později).

16. Příklad s jednoduchou animací: nativní verze

Podívejme se nyní na nepatrně složitější příklad, v němž nebudeme zobrazovat pouze statický obrázek (i když vykreslený v runtime), ale nějakou animaci. Pro jednoduchost se může jednat o simulaci šumu zobrazeného na analogových televizorech při odpojení antény („mravenčení“, „sněžení“, Neal Stephenson použil termín „snow crash“). Použijeme přitom standardní generátor pseudonáhodných čísel:

void draw_snowcrash(SDL_Surface * surface)
{
    Uint8 *pixel;
    int x, y;
 
    if (SDL_MUSTLOCK(surface)) {
        SDL_LockSurface(surface);
    }
 
    /* nyni jiz muzeme pristupovat k pixelum pixmapy */
    pixel = surface->pixels;
 
    for (y = 0; y < surface->h; y++) {
        for (x = 0; x < surface->w; x++) {
            /* nastaveni barvy pixelu, ignorovani pruhlednosti */
            *pixel++ = rand() % 255;
            *pixel++ = rand() % 255;
            *pixel++ = rand() % 255;
            pixel++;
        }
    }
 
    if (SDL_MUSTLOCK(surface))
        SDL_UnlockSurface(surface);
}

Šum budeme generovat a zobrazovat kontinuálně, což lze v nativním kódu realizovat například těmito třemi programovými řádky volanými ze smyčky pro zpracování událostí:

draw_snowcrash(pixmap);
show_pixmap(state, pixmap);
SDL_Delay(16);

Obrázek 7: Jeden snímek šumu zobrazený druhým demonstračním příkladem přeloženým do podoby nativní aplikace.

Na rozdíl od prvního demonstračního příkladu budeme druhý zadní buffer (back buffer) vykreslovat nikoli operací SDL_BlitSurface, ale využijeme texturovací jednotku grafického akcelerátoru (takže si můžete pro vaše účely vybrat jeden z nabízených způsobů):

/* vykresleni pixmapy do plochy okna */
SDL_Texture *screenTexture =
    SDL_CreateTextureFromSurface(state->renderer, surface);
 
SDL_RenderClear(state->renderer);
SDL_RenderCopy(state->renderer, screenTexture, NULL, NULL);
SDL_RenderPresent(state->renderer);
 
SDL_DestroyTexture(screenTexture);

Úplný zdrojový kód tohoto příkladu vypadá takto:

#include <SDL2/SDL.h>
 
#define TITLE "SDL2 demo #2"
#define WIDTH  256
#define HEIGHT 256
 
typedef struct State {
    SDL_Window *window;
    SDL_Renderer *renderer;
    SDL_Surface *screen_surface;
} State;
 
static void init_sdl(State * state, const int width, const int height)
{
    state->window = NULL;
    state->screen_surface = NULL;
 
    /* vlastni inicializace SDL */
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        fprintf(stderr, "Error initializing SDL: %s\n", SDL_GetError());
        exit(1);
    } else {
        puts("SDL_Init ok");
    }
 
    /* inicializace okna pro vykreslovani */
    state->window =
        SDL_CreateWindow(TITLE, SDL_WINDOWPOS_UNDEFINED,
                         SDL_WINDOWPOS_UNDEFINED, width, height,
                         SDL_WINDOW_SHOWN);
    if (!state->window) {
        puts("Error creating window");
        puts(SDL_GetError());
        exit(1);
    } else {
        puts("SDL_CreateWindow ok");
    }
 
    state->renderer =
        SDL_CreateRenderer(state->window, -1, SDL_RENDERER_ACCELERATED);
    if (!state->renderer) {
        puts("Error creating renderer");
        puts(SDL_GetError());
        exit(1);
    } else {
        puts("SDL_CreateRenderer ok");
    }
 
    /* ziskani kreslici plochy okna */
    state->screen_surface = SDL_GetWindowSurface(state->window);
 
    if (!state->screen_surface) {
        fprintf(stderr, "Error setting video mode: %s\n", SDL_GetError());
        exit(1);
    } else {
        puts("SDL_GetWindowSurface ok");
    }
}
 
void draw_snowcrash(SDL_Surface * surface)
{
    Uint8 *pixel;
    int x, y;
 
    if (SDL_MUSTLOCK(surface)) {
        SDL_LockSurface(surface);
    }
 
    /* nyni jiz muzeme pristupovat k pixelum pixmapy */
    pixel = surface->pixels;
 
    for (y = 0; y < surface->h; y++) {
        for (x = 0; x < surface->w; x++) {
            /* nastaveni barvy pixelu, ignorovani pruhlednosti */
            *pixel++ = rand() % 255;
            *pixel++ = rand() % 255;
            *pixel++ = rand() % 255;
            pixel++;
        }
    }
 
    if (SDL_MUSTLOCK(surface))
        SDL_UnlockSurface(surface);
}
 
static void show_pixmap(State * state, SDL_Surface * surface)
{
    /* vykresleni pixmapy do plochy okna */
    SDL_Texture *screenTexture =
        SDL_CreateTextureFromSurface(state->renderer, surface);
 
    SDL_RenderClear(state->renderer);
    SDL_RenderCopy(state->renderer, screenTexture, NULL, NULL);
    SDL_RenderPresent(state->renderer);
 
    SDL_DestroyTexture(screenTexture);
}
 
static SDL_Surface *create_pixmap(const int width, const int height)
{
    SDL_Surface *pixmap;
    pixmap =
        SDL_CreateRGBSurface(SDL_SWSURFACE, WIDTH, HEIGHT, 32, 0x00ff0000,
                             0x0000ff00, 0x000000ff, 0x00000000);
    if (!pixmap) {
        puts("Can not create pixmap");
        exit(1);
    } else {
        puts("Off screen pixmap created");
    }
    return pixmap;
}
 
static void main_event_loop(State * state, SDL_Surface * pixmap)
{
    SDL_Event event;
 
    while (1) {
        /*SDL_WaitEvent(&event); */
        while (SDL_PollEvent(&event)) {
            switch (event.type) {
            case SDL_QUIT:
                return;
                break;          /* zbytecne, ale musime uchlacholit linter */
            case SDL_KEYDOWN:
                switch (event.key.keysym.sym) {
                case SDLK_ESCAPE:
                case SDLK_q:
                    return;
                    break;      /* zbytecne, ale musime uchlacholit linter */
                default:
                    break;
                }
                break;
            default:
                break;
            }
        }
        draw_snowcrash(pixmap);
        show_pixmap(state, pixmap);
        SDL_Delay(16);
    }
}
 
static void finalize(State * state, SDL_Surface * pixmap)
{
    /* uvolneni vsech prostredku */
    SDL_FreeSurface(pixmap);
    SDL_FreeSurface(state->screen_surface);
    SDL_DestroyRenderer(state->renderer);
    SDL_DestroyWindow(state->window);
}
 
int main(int argc, char *argv[])
{
    State state;
    SDL_Surface *pixmap;
 
    /* inicializace SDL, vytvoreni okna a ziskani kreslici plochy okna */
    init_sdl(&state, WIDTH, HEIGHT);
 
    /* vytvoreni offscreen pixmapy */
    pixmap = create_pixmap(WIDTH, HEIGHT);
 
    main_event_loop(&state, pixmap);
 
    finalize(&state, pixmap);
 
    return 0;
}

17. Příklad s jednoduchou animací: úprava pro Emscripten

V předchozím demonstračním příkladu je realizována nekonečná smyčka ve funkci main_event_loop, což je pro nativní aplikaci zcela korektní řešení: ve smyčce se totiž postupně načítají události z fronty a reaguje se na ně, a to včetně překreslení celé scény. Teprve pokud získáme událost typu „žádost o ukončení aplikace“ nebo ve chvíli, kdy je stisknuta klávesa Esc nebo Q, je smyčka přerušena. Ovšem v případě, že má aplikace běžet v ploše webového prohlížeče, je situace odlišná, protože smyčku událostí implementuje samotný runtime prohlížeče. V naší aplikaci tedy nemůžeme implementovat další (nekonečnou) smyčku, protože by například již nedošlo k překreslení okna prohlížeče atd.

Existuje však náhradní řešení – opakovaně volat nějakou funkci, která bude překreslení provádět popř. bude reagovat na stisk kláves atd. Automatické zavolání této funkce se povoluje na nastavuje přes emscripten_set_main_loop, které se předá ukazatel na příslušnou funkci a taktéž dva parametry – frekvence volání této funkce (tedy kolikrát za sekundu má být zavolána) a taktéž příznak, jestli se má simulovat nekonečná smyčka. V případě simulace se po uplynutí nastaveného intervalu vyvolá výjimka, aby tato smyčka byla ukončena, což je v našem céčkovém kódu zcela ignorujeme. Namísto toho budeme opakovaně volat funkci draw_and_show_snowcrash, která je bez parametrů, protože používá globální proměnné:

#ifdef __EMSCRIPTEN__
    emscripten_set_main_loop(draw_and_show_snowcrash, 0, 1);
#else
    main_event_loop(&state, pixmap);
#endif

Samotná implementace výše zmíněné funkce vypadá takto:

void draw_and_show_snowcrash()
{
    draw_snowcrash(pixmap);
    show_pixmap(&state, pixmap);
}
Poznámka: korektnější by bylo se globálním proměnným vyhnout, což však v případě céčka kód poněkud komplikuje (v GCC lze pro tento účel použít funkci definovanou uvnitř jiné funkce, což je velmi pěkné rozšíření jazyka C, které je ovšem nekompatibilní se standardním C).

Výsledek na ploše webového prohlížeče by měl vypadat následovně:

Obrázek 8: Druhý demonstrační příklad spuštěný na ploše webového prohlížeče.

bitcoin_skoleni

#include <SDL2/SDL.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
#include <stdlib.h>
 
#define TITLE "SDL2 demo #2"
#define WIDTH  256
#define HEIGHT 256
 
typedef struct State {
    SDL_Window *window;
    SDL_Renderer *renderer;
    SDL_Surface *screen_surface;
} State;
 
State state;
 
SDL_Surface *pixmap;
 
static void init_sdl(State * state, const int width, const int height)
{
    state->window = NULL;
    state->screen_surface = NULL;
 
    /* vlastni inicializace SDL */
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        fprintf(stderr, "Error initializing SDL: %s\n", SDL_GetError());
        exit(1);
    } else {
        puts("SDL_Init ok");
    }
 
    /* inicializace okna pro vykreslovani */
    state->window =
        SDL_CreateWindow(TITLE, SDL_WINDOWPOS_UNDEFINED,
                         SDL_WINDOWPOS_UNDEFINED, width, height,
                         SDL_WINDOW_SHOWN);
    if (!state->window) {
        puts("Error creating window");
        puts(SDL_GetError());
        exit(1);
    } else {
        puts("SDL_CreateWindow ok");
    }
 
    state->renderer =
        SDL_CreateRenderer(state->window, -1, SDL_RENDERER_ACCELERATED);
    if (!state->renderer) {
        puts("Error creating renderer");
        puts(SDL_GetError());
        exit(1);
    } else {
        puts("SDL_CreateRenderer ok");
    }
 
    /* ziskani kreslici plochy okna */
    state->screen_surface = SDL_GetWindowSurface(state->window);
 
    if (!state->screen_surface) {
        fprintf(stderr, "Error setting video mode: %s\n", SDL_GetError());
        exit(1);
    } else {
        puts("SDL_GetWindowSurface ok");
    }
}
 
void draw_snowcrash(SDL_Surface * surface)
{
    Uint8 *pixel;
    int x, y;
 
    if (SDL_MUSTLOCK(surface)) {
        SDL_LockSurface(surface);
    }
 
    /* nyni jiz muzeme pristupovat k pixelum pixmapy */
    pixel = surface->pixels;
 
    for (y = 0; y < surface->h; y++) {
        for (x = 0; x < surface->w; x++) {
            /* nastaveni barvy pixelu, ignorovani pruhlednosti */
            *pixel++ = rand() % 255;
            *pixel++ = rand() % 255;
            *pixel++ = rand() % 255;
            pixel++;
        }
    }
 
    if (SDL_MUSTLOCK(surface))
        SDL_UnlockSurface(surface);
}
 
static void show_pixmap(State * state, SDL_Surface * surface)
{
    /* vykresleni pixmapy do plochy okna */
    SDL_Texture *screenTexture =
        SDL_CreateTextureFromSurface(state->renderer, surface);
 
    SDL_RenderClear(state->renderer);
    SDL_RenderCopy(state->renderer, screenTexture, NULL, NULL);
    SDL_RenderPresent(state->renderer);
 
    SDL_DestroyTexture(screenTexture);
}
 
void draw_and_show_snowcrash()
{
    draw_snowcrash(pixmap);
    show_pixmap(&state, pixmap);
}
 
static SDL_Surface *create_pixmap(const int width, const int height)
{
    SDL_Surface *pixmap;
    pixmap =
        SDL_CreateRGBSurface(SDL_SWSURFACE, WIDTH, HEIGHT, 32, 0x00ff0000,
                             0x0000ff00, 0x000000ff, 0x00000000);
    if (!pixmap) {
        puts("Can not create pixmap");
        exit(1);
    } else {
        puts("Off screen pixmap created");
    }
    return pixmap;
}
 
static void main_event_loop(State * state, SDL_Surface * pixmap)
{
    SDL_Event event;
 
    while (1) {
        /*SDL_WaitEvent(&event); */
        while (SDL_PollEvent(&event)) {
            switch (event.type) {
            case SDL_QUIT:
                return;
                break;          /* zbytecne, ale musime uchlacholit linter */
            case SDL_KEYDOWN:
                switch (event.key.keysym.sym) {
                case SDLK_ESCAPE:
                case SDLK_q:
                    return;
                    break;      /* zbytecne, ale musime uchlacholit linter */
                default:
                    break;
                }
                break;
            default:
                break;
            }
        }
        draw_snowcrash(pixmap);
        show_pixmap(state, pixmap);
        SDL_Delay(16);
    }
}
 
static void finalize(State * state, SDL_Surface * pixmap)
{
    /* uvolneni vsech prostredku */
    SDL_FreeSurface(pixmap);
    SDL_FreeSurface(state->screen_surface);
    SDL_DestroyRenderer(state->renderer);
    SDL_DestroyWindow(state->window);
}
 
int main(int argc, char *argv[])
{
    /* inicializace SDL, vytvoreni okna a ziskani kreslici plochy okna */
    init_sdl(&state, WIDTH, HEIGHT);
 
    /* vytvoreni offscreen pixmapy */
    pixmap = create_pixmap(WIDTH, HEIGHT);
 
#ifdef __EMSCRIPTEN__
    emscripten_set_main_loop(draw_and_show_snowcrash, 0, 1);
#else
    main_event_loop(&state, pixmap);
#endif
 
    finalize(&state, pixmap);
 
    return 0;
}

18. Příloha: Makefile pro překlad všech demonstračních příkladů

Jednoduchý Makefile určený pro překlad všech demonstračních příkladů popsaných v předchozích kapitolách může vypadat následovně. Schválně jsem se snažil vyhýbat „magickým“ vlastnostem nástroje Make, takže je Makefile delší, než je nutné:

CC=gcc
CFLAGS=-ffast-math -O9 -Wall -pedantic -std=c89
LFLAGS=-lSDL2
 
EMCC=emcc
 
all:    sdl_demo_1A sdl_demo_2A sdl_demo_1B.html sdl_demo_2B.html
 
clean:
        rm sdl_demo_1A
        rm sdl_demo_2A
        rm *.o
 
sdl_demo_1A:    sdl_demo_1A.o
        gcc -o $@ $< $(LFLAGS)
 
sdl_demo_1A.o:  sdl_demo_1A.c
        $(CC) $(CFLAGS) -c -o $@ $<
 
sdl_demo_2A:    sdl_demo_2A.o
        gcc -o $@ $< $(LFLAGS)
 
sdl_demo_2A.o:  sdl_demo_2A.c
        $(CC) $(CFLAGS) -c -o $@ $<
 
sdl_demo_1B.html:       sdl_demo_1B.o
        $(EMCC) $< -o $@ -s USE_SDL=2
 
sdl_demo_1B.o:  sdl_demo_1B.c
        $(EMCC) -c $< -o $@ -s USE_SDL=2
 
sdl_demo_2B.html:       sdl_demo_2B.o
        $(EMCC) $< -o $@ -s USE_SDL=2
 
sdl_demo_2B.o:  sdl_demo_2B.c
        $(EMCC) -c $< -o $@ -s USE_SDL=2

19. Repositář s demonstračními příklady

Demonstrační příklady napsané v jazyku C, které jsou určené pro překlad jak s využitím běžného překladače (gcc, clang), tak i pomocí Emscriptenu, byly uložen do Git repositáře, který je dostupný na adrese https://github.com/tisnik/pre­sentations. Jednotlivé demonstrační příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý (dnes již velmi rozsáhlý) repositář:

# Příklad Stručný popis Adresa
1 sdl_demo_1A.c vykreslení barvové palety https://github.com/tisnik/pre­sentations/blob/master/We­bAssembly_SDL/sdl_demo_1A­.c
2 sdl_demo_1B.c úprava předchozího příkladu pro Emscripten https://github.com/tisnik/pre­sentations/blob/master/We­bAssembly_SDL/sdl_demo_1B­.c
3 sdl_demo_2A.c vykreslení TV šumu https://github.com/tisnik/pre­sentations/blob/master/We­bAssembly_SDL/sdl_demo_2A­.c
4 sdl_demo_2B.c úprava předchozího příkladu pro Emscripten https://github.com/tisnik/pre­sentations/blob/master/We­bAssembly_SDL/sdl_demo_2B­.c
       
5 Makefile soubor pro překlad demonstračních příkladů https://github.com/tisnik/pre­sentations/blob/master/We­bAssembly_SDL/Makefile

20. Odkazy na Internetu

  1. Stránka projektu Emscripten
    https://emscripten.org/
  2. Emscripten (Wikipedia)
    https://en.wikipedia.org/wi­ki/Emscripten
  3. Building to WebAssembly
    https://emscripten.ru/doc­s/compiling/WebAssembly.html
  4. Emscripten and WebAssembly
    https://kripken.github.io/tal­ks/wasm.html#/
  5. Compiling to WebAssembly: It’s Happening!
    https://hacks.mozilla.org/2015/12/com­piling-to-webassembly-its-happening/
  6. WebAssembly
    https://webassembly.org/
  7. Blogy o WASM a Emscripten
    https://www.jamesfmackenzi­e.com/sitemap/#emscripten
  8. wat2wasm demo
    https://webassembly.github­.io/wabt/demo/wat2wasm/
  9. WABT: The WebAssembly Binary Toolkit
    https://github.com/WebAssembly/wabt
  10. Programming using Web Assembly
    https://medium.com/@alexc73/pro­gramming-using-web-assembly-c4c73a4e09a9
  11. Experiments with image manipulation in WASM using Go
    https://agniva.me/wasm/2018/06/18/shim­mer-wasm.html
  12. WebAssembly Lesson 2: Graphics with SDL
    https://www.jamesfmackenzi­e.com/2019/12/01/webassem­bly-graphics-with-sdl/
  13. Image transformation in wasm using Go
    https://github.com/agnivade/shimmer
  14. Hexy (demo)
    https://stdiopt.github.io/gowasm-experiments/hexy/
  15. Bumpu
    https://stdiopt.github.io/gowasm-experiments/bumpy/
  16. HTML canvas createImageData() Method
    https://www.w3schools.com/Tag­s/canvas_createimagedata.asp
  17. HTML canvas putImageData() Method
    https://www.w3schools.com/Tag­s/canvas_putimagedata.asp
  18. HTML canvas getImageData() Method
    https://www.w3schools.com/Tag­s/canvas_getimagedata.asp
  19. Fable
    https://fable.io/

Autor článku

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