Skip to content

Instantly share code, notes, and snippets.

@brccabral
Last active October 17, 2025 21:22
Show Gist options
  • Save brccabral/280328a8e093ea9b7661ec4d1912129b to your computer and use it in GitHub Desktop.
Save brccabral/280328a8e093ea9b7661ec4d1912129b to your computer and use it in GitHub Desktop.
Emscripten

Emscripten

  • Clone and install SDK
    • save $EMSDK as environment var in your .profile/.zprofile/.bashrc/zshrc to make it easier later
    • install will run for a long time
git clone --depth=1 --recurse-submodules --shallow-submodules https://github.com/emscripten-core/emsdk.git
cd emsdk
export EMSDK=$(pwd) # save in you .profile or .bashrc/.zshrc
$EMSDK/emsdk install --shallow sdk-main-64bit # this takes a long time
# ??? # $EMSDK/emsdk install --shallow emscripten-main-64bit
  • Anytime you want to use Emscripten, activate the enviroment
$EMSDK/emsdk activate sdk-main-64bit
# ??? # $EMSDK/emsdk activate emscripten-main-64bit
$EMSDK/source "$EMSDK/emsdk_env.sh"
  • To compile C use emcc and C++ use em++
    • options -s may vary between projects
    • --preload-file ../assets saves all files in assets as one large .data file that can be loaded by the program
$EMSDK/source "$EMSDK/emsdk_env.sh"
mkdir -p build_em
cd build_em
export ProjectName=MyProject
em++ -std=c++17 -O2 -s ALLOW_MEMORY_GROWTH=1 -s MAX_WEBGL_VERSION=2 -s MIN_WEBGL_VERSION=2 -s USE_LIBPNG=1 ../*.cpp -o $ProjectName.html --preload-file ../assets
  • If the project uses CMakeLists.txt, configure with emcmake cmake ..
$EMSDK/source "$EMSDK/emsdk_env.sh"
mkdir -p build_emcmake
cd build_emcmake
emcmake cmake ..
cmake --build . -j10
  • Emscripten will generate files .data, .html, .js and .wasm
  • Run with
$EMSDK/source "$EMSDK/emsdk_env.sh"
emrun $ProjectName.html

or

python -m http.server 8080

Thirdparty libraries

Emscripten can't open shared libraries .so/..dll files when linking, need to get the source code or install STATIC libraries in $EMSDK/emscripten/main/cache/sysroot (on Windows %EMSDK%/upstream/emscripten/cache/sysroot).
In CMakeLists.txt, add the library source directory add_subdirectory(${SDL3_SOURCE} SDL-build).

# with source code
    add_subdirectory(${SDL3_SOURCE} SDL)
    add_subdirectory(${SDL3IMAGE_SOURCE} SDL_image)
    add_subdirectory(${SDL3MIXER_SOURCE} SDL_mixer)
    add_subdirectory(${GLM_SOURCE} glm)
    add_subdirectory(${TINYXML2_SOURCE} tinyxml2)
# or .a/.lib installed in $EMSDK/emscripten/main/cache/sysroot
    find_package(SDL3 REQUIRED)
    find_package(SDL3_image REQUIRED)
    find_package(SDL3_mixer REQUIRED)
    find_package(glm REQUIRED)
    find_package(tinyxml2 REQUIRED)

CLion

In CLion, add Emscripten in a Toolchain, and add a CMake profile that uses the toolchain

  • open Settings -> Buid, Execution, Deployment -> Toolchains
  • Click on Add button +
    • Name: Emscripten (will be used in next step)
    • Environment file: put the path $EMSDK/emsdk_env.sh
    • CMake: we need to execute emcmake cmake when configuring, but only cmake when building.
      • create the script below, put it in $EMSDK/emscripten/main (or anywhere in $PATH).
      • Use this script as CMake in CLion.
      #!/bin/sh
      for arg in "$@"; do
          # if there is --target, we are building
          if [ "$arg" = "--target" ]; then
              cmake $@
              exit $?
          fi
      done
      # there is no --target, we are configuring
      emcmake cmake $@
      Windows
      @echo off
      setlocal
      
      set found_target=0
      
      rem Loop through all parameters
      for %%a in (%*) do (
          if "%%a"=="--target" set found_target=1
      )
      
      if %found_target%==1 (
          cmake %*
      ) else (
          emcmake cmake %*
      )
      
      endlocal
    • Build Tool: leave blank (default ninja)
    • C Compiler: emcc
    • C++ Compiler: em++
    • Debugger: leave as Bundled GDB
  • open Settings -> Buid, Execution, Deployment -> CMake
  • Click on Add button +
    • Name: Debug-EM (any name you want)
    • Build type: choose Debug/Release
    • Toolchain: Emscripten (the name you chose as Toolchain above)
    • Generator: leave Use default
    • CMake options: these options are added by default by emcmake, but not by CLion
      • there is no need to add these if you are using the CMake script above.
      • -DCMAKE_TOOLCHAIN_FILE=$EMSDK/emscripten/main/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_CROSSCOMPILING_EMULATOR=$EMSDK/node/20.18.0_64bit/bin/node
      • Add more options for your project
    • All other options will vary depending on each project

Assets/Data

Choose between these options (I don't know if they can be together).

  1. embed-file with every single file
foreach (res IN LISTS DATA_FILES)
    get_filename_component(res_name "${res}" NAME)
    get_filename_component(res_dir "${res}" DIRECTORY)
    string(REPLACE ${CMAKE_CURRENT_SOURCE_DIR}/ "" RELPATH ${res_dir})
    target_link_options(sdl3-demo PRIVATE "SHELL:--embed-file \"${res}@${RELPATH}/${res_name}\"")
endforeach ()
  1. preload-file - concatenate all files into a single Blob
target_link_options(sdl3-demo PRIVATE --preload-file "${CMAKE_CURRENT_SOURCE_DIR}/data@data")

More robust solution:

set(resources_dir ${CMAKE_CURRENT_SOURCE_DIR}/resources)
if (EXISTS ${resources_dir})
    # The local resources path needs to be mapped to /resources virtual path
    string(APPEND resources_dir "@resources")
    target_link_options(${PROJECT_NAME} PRIVATE --preload-file "${resources_dir}")
endif ()

Output

Set target suffix to .html, and enable -sALLOW_MEMORY_GROWTH=1.

set_property(TARGET ${MYPROGRAM} PROPERTY SUFFIX ".html")
target_link_options(${MYPROGRAM} PRIVATE -sALLOW_MEMORY_GROWTH=1)

Options

Compile options

  • -g3 maximum debug info (keeps everything, including local var. There are 0,1,2,3.

Link options

  • -gsource-map --source-map-base http://127.0.0.1:8081/ specifically tells Emscripten to emit a .wasm.map file that browsers can use to show your original C/C++ source in DevTools
  • -sALLOW_MEMORY_GROWTH=1
  • -sEXPORTED_RUNTIME_METHODS=[requestFullscreen] allow page to open Fullscreen
  • -sUSE_GLFW=3 use GLFW 3 from Emscripten dependencies
  • -sSTACK_SIZE=131072 increase stack size (default 65536)
  • -sASYNCIFY=1 allow "emscripten_sleep()" in main thread
  • -sASSERTIONS=2 enable assertions (good for logic errors, null derefs, etc.)
  • --shell-file "$ENV{EMSCRIPTEN}/src/shell.html" select a different shell file
  • -sSAFE_HEAP=1 helps detect invalid memory reads/writes by instrumenting memory access

Shell

Default page created by Emscripten uses a template called shell.html located at $EMSDK/emscripten/main/src/shell.html.
It wraps your program in a html page with a Emscripten banner and shows a console output.
Emscripten provides a shell_minimal.html, but it just reduces the banner size.
I have created the file below with a bare minimal shell. It only centralizes the program on the screen.
From this bare minimal, you can customize it for your project (add background, favicon, header, footer, etc).
Tell Emscripten linker to use the new shell.

target_link_options(${MYPROGRAM} PRIVATE --shell-file "/path/to/my_shell.html")

On Linux we can use "$ENV{EMSCRIPTEN}/src/shell.html", but on Windows it is "${EMSCRIPTEN_ROOT_PATH}/../../emscripten/main/src/shell.html"

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
canvas {
display: block;
margin: auto;
text-align: center;
}
</style>
</head>
<body>
<!-- Create the canvas that the C++ code will draw into -->
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas>
<!-- Allow the C++ to access the canvas element -->
<script type='text/javascript'>
var Module = {
canvas: (function() { return document.getElementById('canvas'); })()
};
</script>
<!-- Add the javascript glue code (index.js) as generated by Emscripten -->
{{{ SCRIPT }}}
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="icon" type="image/png" href="images/favico.ico">
<style>
canvas {
display: block;
margin: auto;
text-align: center
}
body {
height: 100%;
}
body {
margin: 0;
font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, "Apple Color Emoji", "Segoe UI Emoji";
color: var(--fg);
display: grid;
place-items: start center;
background-image: url('images/background.jpg') !important;
background-size: cover !important;
background-attachment: fixed !important;
background-repeat: no-repeat !important;
background-position: center !important;
}
@-webkit-keyframes rotation {
from {-webkit-transform: rotate(0deg);}
to {-webkit-transform: rotate(360deg);}
}
@-moz-keyframes rotation {
from {-moz-transform: rotate(0deg);}
to {-moz-transform: rotate(360deg);}
}
@-o-keyframes rotation {
from {-o-transform: rotate(0deg);}
to {-o-transform: rotate(360deg);}
}
@keyframes rotation {
from {transform: rotate(0deg);}
to {transform: rotate(360deg);}
}
.spinner {
height: 50px;
width: 50px;
margin: 0px auto;
-webkit-animation: rotation .8s linear infinite;
-moz-animation: rotation .8s linear infinite;
-o-animation: rotation .8s linear infinite;
animation: rotation 0.8s linear infinite;
border-left: 10px solid rgb(0, 240, 0);
border-right: 10px solid rgb(0, 240, 0);
border-bottom: 10px solid rgb(0, 240, 0);
border-top: 10px solid rgb(1, 110, 1);
border-radius: 100%;
background-color: rgb(0, 0, 0);
}
.status {
color: #0f0;
}
progress{
background-color: #0F0;
}
</style>
</head>
<body>
<figure style="overflow:visible;" id="spinner">
<div class="spinner"></div>
<center style="margin-top:0.5em"><strong>.</strong></center>
</figure>
<div class="status" id="status">Downloading...</div>
<div class="status">
<progress value="0" max="100" id="progress" hidden=1></progress>
</div>
<!-- Create the canvas that the C++ code will draw into -->
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex="-1"></canvas>
<!-- Allow the C++ to access the canvas element -->
<script type='text/javascript'>
var statusElement = document.getElementById('status');
var progressElement = document.getElementById('progress');
var spinnerElement = document.getElementById('spinner');
var canvasElement = document.getElementById('canvas');
// As a default initial behavior, pop up an alert when webgl context is lost. To make your
// application robust, you may want to override this behavior before shipping!
// See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
canvasElement.addEventListener("webglcontextlost", (e) => {
alert('WebGL context lost. You will need to reload the page.');
e.preventDefault();
}, false);
var Module = {
print(...args) {
console.log(...args);
},
canvas: canvasElement,
setStatus(text) {
Module.setStatus.last ??= { time: Date.now(), text: '' };
if (text === Module.setStatus.last.text) return;
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
var now = Date.now();
if (m && now - Module.setStatus.last.time < 30) return; // if this is a progress update, skip it if too soon
Module.setStatus.last.time = now;
Module.setStatus.last.text = text;
if (m) {
text = m[1];
progressElement.value = parseInt(m[2]) * 100;
progressElement.max = parseInt(m[4]) * 100;
progressElement.hidden = false;
spinnerElement.hidden = false;
} else {
progressElement.value = null;
progressElement.max = null;
progressElement.hidden = true;
if (!text) spinnerElement.hidden = true;
}
statusElement.innerHTML = text;
},
totalDependencies: 0,
monitorRunDependencies(left) {
this.totalDependencies = Math.max(this.totalDependencies, left);
Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies - left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
}
};
Module.setStatus('Downloading...');
window.onerror = () => {
Module.setStatus('Exception thrown, see JavaScript console');
spinnerElement.style.display = 'none';
Module.setStatus = (text) => {
if (text) console.error('[post-exception status] ' + text);
};
};
</script>
<!-- Add the javascript glue code (index.js) as generated by Emscripten -->
{{{ SCRIPT }}}
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment