JavaScript, HTML5 and WebGL
Building, testing and deploying HTML5 and WebGL projects.
The following guide explains basic workflow of using Emscripten for deploying HTML5 apps using WebGL.
At the very least you need to have Emscripten installed. Running console applications requires Node.js, running browser apps require a webserver that's able to serve static content (for example Apache, if you have Python installed, it has a builtin webserver too).
Cross-compilation to Emscripten is done using a CMake toolchain that's part of the toolchains repository at https:/toolchains/
subdirectory.
git submodule add https://github.com/mosra/toolchains
There are two toolchain files. The generic/Emscripten.cmake
is for the classical (asm.js) build, the generic/Emscripten-wasm.cmake
is for WebAssembly build. The following guide will work with the WASM toolchain. Don't forget to adapt EMSCRIPTEN_PREFIX
variable in toolchains/generic/Emscripten*.cmake
to path where Emscripten is installed; you can also pass it explicitly on command-line using -DEMSCRIPTEN_PREFIX
. Default is /usr/emscripten
.
Building and running console applications
Emscripten allows you to run arbitrary console utilities and tests via Node.js, except for all code that accesses browsers APIs such as WebGL or audio. Assuming you have Magnum installed in the Emscripten path as described in Cross-compiling for Emscripten, build your project simply as this, using one of the toolchain files from above:
mkdir build-emscripten-wasm && cd build-emscripten-wasm cmake .. \ -DCMAKE_TOOLCHAIN_FILE="path/to/toolchains/generic/Emscripten-wasm.cmake" \ -DCMAKE_BUILD_TYPE=Release cmake --build .
Note that the CMAKE_TOOLCHAIN_FILE
path needs to be absolute — otherwise CMake will silently ignore it and continue compiling natively.
After that you can run the generated JavaScript file using Node.js. Note that it looks for the corresponding *.wasm
file in the current directory, so you need to cd
there first:
cd build-emscripten-wasm/src node my-application.js
Building and deploying graphical apps
Magnum provides Emscripten application wrappers in Platform::
To target the web browser, you need to provide a HTML markup for your application. Template one is below. The markup references two files, EmscriptenApplication.js
and WebApplication.css
, both are in the src/share/magnum/
inside your install prefix and if you use CMake, their full path is available through the MAGNUM_EMSCRIPTENAPPLICATION_JS
and MAGNUM_WEBAPPLICATION_CSS
variables.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Magnum Emscripten Application</title> <link rel="stylesheet" href="WebApplication.css" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> </head> <body> <h1>Magnum Emscripten Application</h1> <div class="mn-container"> <div class="mn-sizer"><div class="mn-expander"><div class="mn-listener"> <canvas class="mn-canvas" id="canvas"></canvas> <div class="mn-status" id="status">Initialization...</div> <div class="mn-status-description" id="status-description"></div> <script src="EmscriptenApplication.js"></script> <script async="async" src="{{ application }}.js"></script> </div></div></div> </div> </body> </html>
For basic usage you don't need to modify the CSS and JS files at all and everything can be set up directly from the HTML markup. Replace {{ application }}
with the name of your application executable and adapt page title and heading as desired. You can modify all files to your liking, but the HTML file must contain at least a <canvas>
referenced using an ID in Module.canvas
— by default it's #canvas
but it's possible to override it. The JavaScript file contains event listeners that print loading status on the page. The status is displayed in the remaining two <div>
s, if they are available. The CSS file contains a basic style, see below for available options to tweak the default look.
In order to deploy the app, you need to put the JS driver code, the WebAssembly binary (or the asm.js memory image, in case you are compiling with the classic asm.js toolchain), the HTML markup and the JS/CSS files to a common location. The following CMake snippet handles all of that:
if(CORRADE_TARGET_EMSCRIPTEN) install(TARGETS my-application DESTINATION ${CMAKE_INSTALL_PREFIX}) install(FILES my-application.html ${MAGNUM_EMSCRIPTENAPPLICATION_JS} ${MAGNUM_WEBAPPLICATION_CSS} DESTINATION ${CMAKE_INSTALL_PREFIX}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/my-application.js.mem ${CMAKE_CURRENT_BINARY_DIR}/my-application.wasm DESTINATION ${CMAKE_INSTALL_PREFIX} OPTIONAL) endif()
To deploy, you can either point CMAKE_INSTALL_PREFIX
to a location inside your system webserver or you can point it to an arbitrary directory and use Python's builtin webserver to serve its contents:
cd build-emscripten-wasm cmake -DCMAKE_INSTALL_PREFIX=/path/to/my/emscripten/deploy .. cmake --build . --target install cd /path/to/my/emscripten/deploy python -m http.server # or python -m SimpleHTTPServer with Python 2
After that, you can open http:/
Building and deploying windowless apps
Windowless Magnum apps (i.e. apps that use the OpenGL context without a window) can be run in the browser as well using the Platform::
Similarly to graphics apps, you need to provide a HTML markup for your application. Template one is below, its main difference from the one above is that it shows the console output instead of the canvas. The markup references two files, WindowlessEmscriptenApplication.js
and WebApplication.css
, both are again in the src/share/magnum/
inside your install prefix and available through MAGNUM_WINDOWLESSEMSCRIPTENAPPLICATION_JS
and MAGNUM_WEBAPPLICATION_CSS
CMake variables.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Magnum Windowless Emscripten Application</title> <link rel="stylesheet" href="WebApplication.css" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> </head> <body> <h1>Magnum Windowless Emscripten Application</h1> <div class="mn-container"> <div class="mn-sizer"><div class="mn-expander"><div class="mn-listener"> <canvas class="mn-canvas mn-hidden" id="canvas"></canvas> <pre class="mn-log" id="log"></pre> <div class="mn-status" id="status">Initialization...</div> <div class="mn-status-description" id="status-description"></div> <script src="WindowlessEmscriptenApplication.js"></script> <script async="async" src="{{ application }}.js"></script> </div></div></div> </div> </body> </html>
Replace {{ application }}
with the name of your application executable. You can modify all the files to your liking, but the HTML file must contain at least the <canvas>
enclosed in listener <div>
and the <pre id="log">
for displaying the output. The JavaScript file contains event listeners which print loading status on the page. The status displayed in the remaining two <div>
s, if they are available. The CSS file is shared with graphical apps, again see below for available options to tweak the default look.
Deployment is similar to graphical apps, only referencing a different JS file — ${MAGNUM_WINDOWLESSEMSCRIPTENAPPLICATION_JS}
instead of ${MAGNUM_EMSCRIPTENAPPLICATION_JS}
:
if(CORRADE_TARGET_EMSCRIPTEN) install(TARGETS my-application DESTINATION ${CMAKE_INSTALL_PREFIX}) install(FILES my-application.html ${MAGNUM_WINDOWLESSEMSCRIPTENAPPLICATION_JS} ${MAGNUM_WEBAPPLICATION_CSS} DESTINATION ${CMAKE_INSTALL_PREFIX}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/my-application.js.mem ${CMAKE_CURRENT_BINARY_DIR}/my-application.wasm DESTINATION ${CMAKE_INSTALL_PREFIX} OPTIONAL) endif()
Multiple applications on one page
Running multiple applications on a single page is possible, but requires a few changes. By default, Emscripten populates a global Module
object with things like callbacks and the canvas element to render into. This is not suitable when using more than one application. Emscripten's MODULARIZE
option creates a script that defines a function with a local Module
object instead. This way each application runs inside its own function with a separate environment:
# If you have CMake 3.13+ target_link_options(my-application PRIVATE "SHELL:-s MODULARIZE" "SHELL:-s EXPORT_NAME=createModule") # Or alternatively, supported by older CMake versions as well target_link_libraries(my-application PRIVATE "-s MODULARIZE -s EXPORT_NAME=createModule")
Instead of running instantly, the application can then be instantiated using the function name set with EXPORT_NAME
. The function optionally takes an object to populate the local Module
.
While EmscriptenApplication.js
populates the Module
object by default, it also defines a createMagnumModule()
function that lets you create new module objects to pass to the Emscripten module function. Pass an object to it to override the default values. This is the easiest way to set the canvas or status elements of each application:
<script src="EmscriptenApplication.js"></script> <script src="{{ application }}.js"></script> <script> const myModule = createMagnumModule({ canvas: document.getElementById('my-canvas'), status: document.getElementById('my-status'), statusDescription: document.getElementById('my-status-description') }); createModule(myModule).then(function(myModule) { // application loaded and running }); </script>
Modifying page style, canvas size and aspect ratio
The WebApplication.css
file contains a basic style and the additional .mn-container
, .mn-sizer
, .mn-expander
and .mn-listener
<div>
s take care of aligning the canvas to the center and making it responsively scale on narrow screens, preserving aspect ratio. For proper responsiveness on all platforms it's important to include the <meta name="viewport">
tag in the HTML markup as well.
By default the canvas is 640px
wide with a 4:3 aspect ratio, you can modify this by placing one of the .mn-aspect-*
CSS classes on the <div class="mn-container">
:
Aspect ratio | CSS class (landscape/portrait) |
---|---|
1:1 | .mn-aspect-1-1 |
4:3 (1.33:1) | .mn-aspect-4-3 (default) / .mn-aspect-3-4 |
3:2 (1.5:1) | .mn-aspect-3-2 / .mn-aspect-2-3 |
16:9 (1.78:1) | .mn-aspect-16-9 / .mn-aspect-9-16 |
2:1 | .mn-aspect-2-1 / .mn-aspect-1-2 |
For example, for a 640x360 canvas (16:9 aspect ratio), you would use <div class="mn-container mn-aspect-16-9">
. Besides the predefined classes above, it's also possible to specify your own aspect ratio using a padding-bottom
style with a percentage equal to inverse of the ratio for div#expander
— for example, a 2.35:1 ratio would be <div class="mn-expander" style="padding-bottom: 42.553%">
.
Size of the canvas can be also overridden by specifying one of the .mn-width-*
CSS classes on the <div class="mn-container">
:
Width | CSS class |
---|---|
240px | .mn-width-240 |
320px | .mn-width-320 |
360px | .mn-width-360 |
480px | .mn-width-480 |
600px | .mn-width-600 |
640px | .mn-width-640 (default) |
800px | .mn-width-800 |
For example, for a 480x640 canvas (3:4, portrait) you would use <div class="mn-container mn-width-480 mn-aspect-3-4">
. Besides the predefined classes above, it's also possible to specify your own by adding a width
style to the div.mn-sizer
— for example, a 1024x768 canvas would be <div class="mn-sizer" style="width: 1024px">
. Again note that if the canvas is larger than window width, it gets automatically scaled down, preserving its aspect ratio.
It's also possible to stretch the canvas to occupy the full page using the .mn-fullsize
CSS class set to div.mn-container
. In this case it's advised to remove all other elements (such as the <h1>
element) from the page to avoid them affecting the canvas. Combining .mn-fullsize
with the other .mn-aspect-*
or .mn-width-*
classes is not supported.
Besides the canvas, there's minimal styling for <h1>
, <p>
and <code>
tags. Putting extra content inside the div.mn-sizer
will center it, following the canvas width. If you need more advanced styling, check out m.css.
Bundling files
Emscripten applications don't have access to a filesystem, which means you need to bundle the files explicitly. One possibility is via Corrade::*.wasm
binary, which is the most optimal way, but it means you need to port your code away from the usual filesystem APIs. Another possibility is via the emscripten_embed_file()
CMake macro, which is available when you build for Emscripten, coming from the UseEmscripten.cmake module:
add_executable(MyApplication main.cpp) if(CORRADE_TARGET_EMSCRIPTEN) include(UseEmscripten) emscripten_embed_file(MyApplication file.dat /path/in/virtual/fs/file.dat) endif()
This approach will make the file available in Emscripten's virtual filesystem, meaning you can use the usual filesystem APIs to get it. However, the file will be Base64-encoded in the *.js
file (and not the *.wasm
binary), inflating the file size much more. This approach is thus most useful for quick demos or test executables where size doesn't matter that much. If you use Corrade::FILES
option in the corrade_
Among other possibilities for bundling and accessing files is Emscripten's file_
Controlling event behavior
Keyboard events
By default, the graphical Emscripten application grabs all keyboard input anywhere on the page (so e.g. F5 or opening browser console via a keyboard shortcut doesn't work). If you don't want it to grab keyboard input at all, set Module.doNotCaptureKeyboard
to true
, either by modifying the EmscriptenApplication.js
file or directly in the HTML source:
<script> // after EmscriptenApplication.js has been loaded Module.doNotCaptureKeyboard = true; </script>
The above is implicitly set for Platform::
Module.doNotCaptureKeyboard
is not supported by Platform::
Another solution is to specify the element on which it should capture keybard using Module.keyboardListeningElement
— it requires the actual element instance (not just its ID). The element then needs to be activated (for example with a mouse click) in order to start receiving input. The <canvas>
element is a bit special in this regard — in order to make it receive keyboard input, you have to add a tabindex
attribute to it like this:
<canvas id="canvas" tabindex="0"></canvas>
After that, the canvas can be focused with a Tab key. But because Emscripten eats all mouse input, the mousedown
event won't be propagated to focus the canvas unless you do that manually:
Module.keyboardListeningElement = Module.canvas; Module.canvas.addEventListener('mousedown', function(event) { event.target.focus(); });
Terminal output, environment and command-line arguments
When running console apps using Node.js, command-line arguments and terminal output work like usual.
For graphical apps in the browser, EmscriptenApplication.js
redirects all output (thus also Debug, Warning and Error) to JavaScript console. For windowless apps, WindowlessEmscriptenApplication.js
redirects output to the <pre id="log">
element on the page.
It's possible to pass command-line arguments to main()
using GET URL parameters. For example, /app/?foo=bar&fizz&buzz=3
will go to the app as ['--foo', 'bar', '--fizz', '--buzz', '3']
.
Emscripten provides its own set of environment variables through std::
Differences between WebGL and OpenGL ES
WebGL is subset of OpenGL ES with some specific restrictions, namely requirement for unique buffer target binding, aligned buffer offset and stride and some other restrictions. The most prominent difference is that while the following was enough on desktop:
GL::Buffer vertices; GL::Buffer indices;
On WebGL (even 2.0) you always have to initialize the buffers like this (and other target hints for UBOs etc.):
GL::Buffer vertices{GL::Buffer::TargetHint::Array}; GL::Buffer indices{GL::Buffer::TargetHint::ElementArray};
See GL::
Timer queries not available in the browser
Similarly to Rowhammer, GPU timer queries could be abused to do a bit-flip attack, which is the reason why EXT_webgl.enable-privileged-extensions
in about:config
(source). More details about the GLitch vulnerability.
Queries always report zero results
Compared to OpenGL ES, WebGL has an additional restriction where it's not guaranteed for a GL::
If you're on Firefox, you can enable the webgl.allow-immediate-queries
option in about:flags
, which will make these work even without giving the control back to the browser.
Compilation time / code size tradeoffs
By default, the toolchain configures the compiler and linker to use -O3
in Release
builds, together with enabling -flto
for the linker. When deploying, ensure your CMAKE_BUILD_TYPE
is set to Release
, otherwise the generated file sizes will be huge due to the additional debug information and non-minified JS driver file. Also, unless you need compatibility with non-WebAssembly-capable browsers, ensure you're compiling with the Emscripten-wasm.cmake
toolchain. Further optimizations are possible but are not enabled by default in the toolchain file due to the features either slowing down the build significantly or being very recent:
- use
-Os
or-Oz
to compile and link with optimizations for code size at the expense of performance and increased compile times - enable
--closure 1
for further minification of the JavaScript driver file (the Audio library needs at least Emscripten 1.38.21, see emscripten-core/emscripten#7558 for more information) — however, be prepared for serious increase in compile times and memory usage of the Closure Compiler - disable assertions with
-s ASSERTIONS=0
(enabled by default) -s MINIMAL_RUNTIME=1
(new since 1.38.27) — note that, while this leads to significant size savings, it currently requires significant amount of work on the application side and Magnum was not yet tested with this option enabled
On the other hand, if you need shorter compile times and don't care much about code size (for example when testing on the CIs), consider removing some of these flags — especially removing -O3
and -flto
can result in significant compile speed gains.
Server-side compression
Servers are usually configured to apply GZip compression to plain text files (CSS, JS and HTML), but the WebAssembly binary is nicely compressible as well. With Apache, for example, adding this line to .htaccess
will make the *.wasm
files compressed on-the-fly as well:
AddOutputFilter DEFLATE wasm
Apart from GZip, some modern browsers support Brotli compression, which leads to much bigger size savings. Details in this Qt blog post.
Troubleshooting
Setting up Emscripten on macOS
Emscripten is available through Homebrew:
brew install emscripten
Because LLVM is also distributed as part of Xcode, it will get picked up by default. Emscripten however needs to use its own bundled version, so you may need to export the $LLVM
environment variable to port to the other location:
export LLVM=/usr/local/opt/emscripten/libexec/llvm/bin
CMake insists on using Visual C++ as a compiler
On Windows, running CMake as-is without specifying a generator via -G
will always use the Visual Studio compiler and seemingly ignore the Emscripten toolchain file altogether. With recent toolchain files, you may get this message instead:
CMake Error at toolchains/generic/Emscripten-wasm.cmake:19 (message): Visual Studio project generator doesn't support cross-compiling to Emscripten. Please use -G Ninja or other generators instead.
This is because Visual Studio Project Files as the default generator on Windows is not able to build for any other system than Windows itself. To fix it, use a different generator — for example Ninja. Download the binary, put it somewhere on your PATH
and pass -G Ninja
to CMake. Alternatively, pass the path to it using -DCMAKE_MAKE_PROGRAM=C:/path/to/ninja.exe
.
First Emscripten run takes long or fails
Emscripten downloads and builds a lot of things on first startup or after upgrade. That's expected and might take quite some time. If you are calling Emscripten through the CMake toolchain, it might be attempting to bootstrap itself multiple times, taking extreme amounts of time, or even fail during the initial CMake compiler checks for various reasons such as
File "/usr/lib/python2.7/subprocess.py", line 1025, in _execute_child raise child_exception OSError: [Errno 13] Permission denied
The CMake toolchain might interfere with the bootstrap operation, causing it to fail. Solution is to wipe all Emscripten caches and trigger a rebuild of all needed libraries by compiling a minimal project, as shown in the shell snippet below — enter it into the console prior to building anything else. It will take a while to download and build various system libraries and random tools. The -s WASM=1
flag is needed in order to enable a rebuild of the binaryen
tool as well:
cd /tmp emcc --clear-cache emcc --clear-ports echo "int main() {}" > main.cpp em++ -s WASM=1 main.cpp
After this step it might be also needed to purge all CMake build directories and set them up again to ensure no bad state got cached.
Cursed errors deep inside standard headers
If you encounter errors similar to the one below, the reason is often that a system-wide directory such as /usr/include
got accidentally added to the include path. The compiler then ends up randomly pulling in mutually conflicting versions of the same header, causing all sorts of issues.
In file included from /some/path/.emscripten_cache/sysroot/include/c++/v1/utility:277: /some/path/.emscripten_cache/sysroot/include/c++/v1/cstdlib:90:5: error: <cstdlib> tried including <stdlib.h> but didn't find libc++'s <stdlib.h> header. This usually means that your header search paths are not configured properly. The header search paths should contain the C++ Standard Library headers before any C Standard Library, and you are probably using compiler flags that make that not be the case. 90 | # error <cstdlib> tried including <stdlib.h> but didn't find libc++'s <stdlib.h> header. \ | ^ /some/path/.emscripten_cache/sysroot/include/c++/v1/cstdlib:132:9: error: target of using declaration conflicts with declaration already in scope 132 | using ::abs _LIBCPP_USING_IF_EXISTS; | ^ /usr/include/stdlib.h:980:12: note: target of using declaration 980 | extern int abs (int __x) __THROW __attribute__ ((__const__)) __wur; | ^ /some/path/.emscripten_cache/sysroot/include/c++/v1/cmath:353:1: note: conflicting declaration 353 | using ::abs _LIBCPP_USING_IF_EXISTS; | ^
The solution is to check the compiler command line for system-wide include paths and then finding a dependency that might have been pulled from there. The -- Found
entries in CMake configure log may hint at the problem, if not then look for the offending path in CMake cache. Packages found via CMake config files don't usually add their include paths to the cache, in that case look for <Package>_DIR
that point directly to /usr
, /usr/lib/cmake
and such.
This often happens with zlib, zstd and other common system libraries if an Emscripten-compiled version isn't found first.
Including files directly from the emscripten source tree is not supported
Emscripten 3.0.9 and newer don't allow to include files directly from the system/include
directory from Emscripten installations, failing with an error similar to the following. Emscripten 3.0.4 to 3.0.8 may instead fail due to version.h
not found, which is a consequence of the same problem.
In file included from /usr/lib/emscripten/system/include/emscripten/emscripten.h:29: /usr/lib/emscripten/system/include/emscripten/version.h:8:2: error: "Including files directly from the emscripten source tree is not supported. Please use the cache/sysroot/include directory". 8 | #error "Including files directly from the emscripten source tree is not supported. Please use the cache/sysroot/include directory". | ^
The preferred way is to include files from the local Emscripten cache instead, where Emscripten copies them, along with generating a version.h
header there. Magnum's Emscripten toolchain queries this location and adds it to the CMake find root path. However, library files from there (e.g., installed prebuilt dependencies) are not copied to the cache, so the system directory still has to be on the find root path as well.
The cache is only updated when Emscripten itself feels like doing that, which doesn't include the case when the installed dependencies are updated or when a new library is installed there. This in turn may lead to either stale includes being used, or dependency includes being found in the system directory instead of the cache, leading to the above error. You need to explicitly run the following in order to refresh the copy of all includes in the cache:
embuilder build sysroot --force
CMake can't find CORRADE_INCLUDE_DIR, _CORRADE_CONFIGURE_FILE or _CORRADE_MODULE_DIR
If you have Corrade installed in a custom path, you might end up with variants of the following error:
Could NOT find Corrade (missing: _CORRADE_MODULE_DIR)
The solution is to explicitly pass CMAKE_PREFIX_PATH
pointing to directory where Corrade is installed. In some cases it might also be needed to point CMAKE_FIND_ROOT_PATH
to the same location. For example:
mkdir build-emscripten && cd build-emscripte cmake .. \ -DCMAKE_TOOLCHAIN_FILE=../toolchains/generic/Emscripten-wasm.cmake \ -DCMAKE_PREFIX_PATH=/your/emscripten/libs/ \ -DCMAKE_FIND_ROOT_PATH=/your/emscripten/libs/ \ ...
Application fails to load
Depending on what's the exact error printed in the browser console, the following scenarios are possible:
- By default, the size of Emscripten heap is restricted to 16 MB. That might not be enough if you have large compiled-in resources or allocate large amount of memory. This can be solved with either of these:
- Adding
-s TOTAL_MEMORY=<bytes>
to compiler/linker flags, where <bytes> is the new heap size - Adding
-s ALLOW_MEMORY_GROWTH=1
to compiler/linker flags. This is useful in case you don't know how much memory you need in advance and might disable some optimizations. - Setting
Module { TOTAL_MEMORY: <bytes>; }
in the JavaScript driver file
- Adding
- Sometimes Chromium-based browsers refuse to create WebGL context on a particular page, while on other sites it works and the same page works in other browsers such as Firefox. This can be caused by Chromium running for too long, restart it and try again.
- If you compile your application with a different set of compiler / linker flags or a different Emscripten version than your dependencies, it can fail to load for a variety of random reasons. Try to rebuild everything with the same set of flags.
Incorrect response MIME type
Depending on your browser, you might see a warning similar to the following in the console:
wasm streaming compile failed: TypeError: Failed to execute 'compile' on 'WebAssembly': Incorrect response MIME type. Expected 'application/wasm'. falling back to ArrayBuffer instantiation
This is not a critical error, but causes slow startup since the browser usually attempts to load the file twice. This is because the HTTP Content-Type
header is not set properly by the webserver. In case you use Apache, fixing this is a matter of adding the following line to your .htaccess
:
AddType application/wasm .wasm
It is not possible when using Python http.server
from the command line, but the mapping can be added programmatically:
http.server.SimpleHTTPRequestHandler.extensions_map['.wasm'] = 'application/wasm'
Warnings about $ characters when using EM_ASM
The EM_$0
, $1
, ... to refer to macro arguments from JavaScript code. Using them in C++ sources triggers a warning from Clang:
warning: '$' in identifier
Solution is to disable the warning for the offending lines, below for example when modifying a title element contents through DOM. No #ifdef CORRADE_TARGET_CLANG
is necessary, as the code in question is only ever compiled by Emscripten's Clang anyway:
#pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdollar-in-identifier-extension" Containers::String title; EM_ASM_({document.getElementById('title').innerHTML = UTF8ToString($0)}, title.data()); #pragma GCC diagnostic pop