Trying to Port an SDL2 Program to Emscripten on Windows
2023-05-16
Recently, I attempted to port a game I made in C++ with SDL to Emscripten on Windows. There were a few bumps on the road, so I figured I should write down how I did it. I’m not an expert in these topics and this was my first ever encounter with Emscripten. I’m simply documenting what worked for me. Versions and setup:
- Windows 10
- Python 3.11.3
- Emscripten 3.1.38
- PowerShell and VS Code
Installation
Read https://emscripten.org/docs/getting_started/downloads.html. For these commands, I ran PowerShell as Administrator, though that may not have been necessary.
Clone the Git repo and cd into it:
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
You’ll need to hold on to emsdk/, so I suggest moving it somewhere permanently. I chose C:\.
Ignore emsdk and emsdk.bat and instead run:
python .\emsdk.py install latest
You may see a stack trace and an error message containing:
Error: Downloading URL 'https://storage.googleapis.com/webassembly/emscripten-releases-builds/deps/node-v15.14.0-win-x64.zip': [ASN1] nested asn1 error (_ssl.c:4004)
error: installation failed!
On one of my retries, I saw a slightly different message:
Error: Downloading URL 'https://storage.googleapis.com/webassembly/emscripten-releases-builds/deps/node-v15.14.0-win-x64.zip': not enough data: cadata does not contain a certificate (_ssl.c:4015)
error: installation failed!
Update your Python version and run:
pip install certifi
This will not actually fix anything. What finally helped me was this StackOverflow question. The user added code to emsdk.py that changes HTTPS URLs to HTTP. At the start of download_file()
, they added:
if url.startswith("https://storage.googleapis.com/"): # TODO fix/remove me
url = "http"+url[5:] # change https to http # TODO fix/remove me
(Credits go to fgrieu on StackOverflow.) It’s hacky, but it worked! The install downloaded a bunch of files and finished.
As an aside, I was able to download the zip file myself, so, on the suggestion from #6275, I created a zips/ directory and put the file there. Rerunning emsdk.py simply emptied this directory and returned the same error message.
If you managed to install emsdk, run:
python .\emsdk.py activate latest --permanent
This command modifies the PATH variable. For some reason, restarting my PowerShell did not pick up the change, but restarting my computer did. Anyway, emcc -v
and em++ -v
should report the version successfully.
Code
emcc and em++ should be drop-in replacements for GCC, but some code modifications are required. These are useful reads:
- https://emscripten.org/docs/getting_started/Tutorial.html
- https://emscripten.org/docs/porting/emscripten-runtime-environment.html
Overall, my code ended up looking something like this:
// other includes
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
struct GameLoopData {
// ...
};
// Returns whether the game should keep running.
bool oneIteration(GameLoopData &data) {
// ...
}
#ifdef __EMSCRIPTEN__
void oneIterationEmscripten(void *data) {
if (!oneIteration(*reinterpret_cast<GameLoopData*>(data))) {
emscripten_force_exit(0);
}
}
#endif
int main(int argc, char *argv[]) {
// Init SDL, GameLoopData, and everything else.
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop_arg(oneIterationEmscripten, &gameLoopData, 0, true);
#else
while (true) {
if (!oneIteration(gameLoopData)) break;
}
#endif
return 0;
}
emscripten_set_main_loop_arg()
has four arguments:
- The first is a callback that performs one game loop iteration.
- The second is a
void*
to user data, in this case toGameLoopData
. If you don’t need this, you can callemscripten_set_main_loop()
instead. - The third is your target FPS. It is recommended to use a non-positive value in which case the browser decides the refresh rate. My game uses VSync (passing
SDL_RENDERER_PRESENTVSYNC
toSDL_CreateRenderer
) - according to #11788 and my testing it is ok to leave it enabled. Speaking of which, I was passingSDL_WINDOW_SHOWN | SDL_WINDOW_BORDERLESS
toSDL_CreateWindow
and that did not seem to cause issues either. There is always the option of using the preprocessor to not set those when__EMSCRIPTEN__
is defined, if you so desire. - The fourth argument is called
simulate_infinite_loop
. There is an explanation in the API reference. My understanding of it and of some Github comments I’ve read is that the code after the call toemscripten_set_main_loop_arg()
will simply never run. I believe this includes destructors of local variables, because otherwise I should have seen issues since I use destructors to clean up assets and quit SDL.
Notice I used emscripten_force_exit()
. The reference mentions building with -sEXIT_RUNTIME
. This function terminates the program, though I could still see the last frame of the game when I ran it in browser with emrun.
Compilation
I use Make, but my command to compile was similar to:
em++ main.cpp -O3 -std=c++20 -sUSE_SDL=2 -sUSE_SDL_IMAGE=2 -sUSE_SDL_TTF=2 -sUSE_SDL_MIXER=2 -sSDL2_IMAGE_FORMATS='["bmp","png"]' -sEXIT_RUNTIME --preload-file assets -o bin/game.html
I ran an experiment and I believe that warning flags (-Wall
, etc.) have no effect in em++.
Instead of using -lSDL2
as you would in GCC or Clang, you build with -sUSE_SDL=2
(and the rest). Run emcc --show-ports
for more options. Otherwise, your code will flat-out fail to compile or you’ll see these errors:
wasm-ld: error: unable to find library -lSDL2main
wasm-ld: error: unable to find library -lSDL2_image
wasm-ld: error: unable to find library -lSDL2_ttf
or something like:
wasm-ld: error: C:\Users\Foo\AppData\Local\Temp\emscripten_temp_abc123\main_0.o:
undefined symbol: TTF_OpenFont
wasm-ld: error: C:\Users\Foo\AppData\Local\Temp\emscripten_temp_abc123\main_0.o:
undefined symbol: IMG_Load
wasm-ld: error: C:\Users\Foo\AppData\Local\Temp\emscripten_temp_abc123\main_0.o:
undefined symbol: Mix_LoadWAV_RW
(The reason I’m including the errors is so that someone in the future may find them. SEO++)
-sSDL2_IMAGE_FORMATS
is required to allow various image formats to be parsed. Otherwise, if your code logs SDL errors, the browser console may tell you: ERROR: PNG images are not supported
.
On Windows, you may get argument parsing issues (https://github.com/emscripten-ports/SDL2_image/issues/4). If that’s the case, I suggest changing the terminal you use!
--preload-file
lets you package files and directories for preloading: https://emscripten.org/docs/porting/files/packaging_files.html. Without it, you may see an SDL error like: ERROR: Couldn't open assets/ttf/sansation.ttf
.
If all goes well, you may now see this error:
Traceback (most recent call last):
File "C:\emsdk\upstream\emscripten\tools\ports\__init__.py", line 255, in retrieve
import requests
ModuleNotFoundError: No module named 'requests'
One GitHub comment suggested running em++ --clear-ports
, but that did not help. You want to use pip to install the module. Emscripten ships with its own Python environment, but, unfortunately, there is no pip in it. Inside the emsdk directory run this:
.\python.exe -m pip install requests
.\python.exe -m pip install certifi
With that, I was able to successfully compile with Emscripten.
If you want a favicon for your program, simply paste an ICO file called favicon.ico into the compiler output directory.
Running
I used emrun to run the program:
cd bin
emrun game.html
You can also use Node or start a local server with Python. See:
- https://emscripten.org/docs/compiling/Running-html-files-with-emrun.html
- https://emscripten.org/docs/getting_started/Tutorial.html#running-emscripten
- https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing
Reference
https://emscripten.org/docs/getting_started/downloads.html
https://emscripten.org/docs/getting_started/Tutorial.html
https://emscripten.org/docs/porting/emscripten-runtime-environment.html
https://emscripten.org/docs/api_reference/emscripten.h.html
https://emscripten.org/docs/porting/files/packaging_files.html
https://emscripten.org/docs/compiling/Running-html-files-with-emrun.html
https://emscripten.org/docs/getting_started/FAQ.html
https://github.com/emscripten-core/emscripten/issues/6275
https://github.com/emscripten-core/emsdk/issues/588
https://github.com/emscripten-core/emscripten/issues/16466
https://github.com/emscripten-core/emscripten/issues/16398
https://github.com/emscripten-core/emscripten/issues/5076
https://github.com/emscripten-ports/SDL2_image/issues/4
https://github.com/emscripten-core/emscripten/issues/11788
https://lyceum-allotments.github.io/2016/06/emscripten-and-sdl2-tutorial-part-4-look-owl