Building large projects with Emscripten is very easy. Emscripten provides two simple scripts that configure your makefiles to use emcc as a drop-in replacement for gcc — in most cases the rest of your project’s current build system remains unchanged.
To build using Emscripten you need to replace gcc with emcc in your makefiles. This is done using emconfigure, which sets the appropriate environment variables like CXX (C++ compiler) and CC (the compiler).
Consider the case where you normally build with the following commands:
To build with Emscripten, you would instead use the following commands:
emconfigure is called with the normal configure as an argument (in configure-based build systems), and emmake with make as an argument. If your build system doesn’t use configure, then you can omit the first step.
We recommend you call both emconfigure and emmake scripts in configure-based build systems. Whether you actually need to call both tools depends on the build system (some systems will store the environment variables in the configure step, and others will not).
Consider the examples below:
Unfortunately each build-system defines its own mechanisms for setting compiler and optimization methods. You will need to work out the correct approach to set the LLVM optimization flags for your system.
Each build-system defines its own mechanisms for setting debug flags. To get Clang to emit LLVM debug information, you will need to work out the correct approach for your system.
For more general information, see the topic Debugging.
Built-in support is available for a number of standard libraries: libc, libc++ and SDL. These will automatically be linked when you compile code that uses them (you do not even need to add -lSDL, but see below for more SDL-specific details).
For example, consider the case where a project “project” uses a library “libstuff”:
# Compile libstuff to bitcode ./emconfigure ./configure ./emmake make # Compile project to bitcode ./emconfigure ./configure ./emmake make # Compile the library and code together to HTML emcc project.bc libstuff.bc -o final.html
# Generate bitcode files project.bc and libstuff.bc ... # Link together the bitcode files emcc project.bc libstuff.bc -o allproject.bc # Compile the combined bitcode to HTML emcc allproject.bc -o final.html
Emscripten Ports is a collection of useful libraries, ported to Emscripten. They reside on github, and have integration support in emcc. When you request that a port be used, emcc will fetch it from the remote server, set it up and build it locally, then link it with your project, add necessary include to your build commands, etc. For example, SDL2 is in ports, and you can request that it be used with -s USE_SDL=2. For example,
./emcc tests/sdl2glshader.c -s USE_SDL=2 -s LEGACY_GL_EMULATION=1 -o sdl2.html
You should see some notifications about SDL2 being used, and built if it wasn’t previously. You can then view sdl2.html in your browser.
SDL_image has also been added to ports, use it with -s USE_SDL_IMAGE=2. To see a list of all available ports, run emcc --show-ports. For SDL2_image to be useful, you generally need to specify the image formats you are planning on using with -s SDL2_IMAGE_FORMATS=’[“png”]’. This will also ensure that IMG_Init works properly. Alternatively, you can use specify emcc --use-preload-plugins (and --preload-file your images, so the browser codecs decode them), but then your calls to IMG_Init will fail.
SDL_net has also been added to ports, use it with -s USE_SDL_NET=2. To see a list of all available ports, run emcc --show-ports.
Emscripten also has support for older SDL1, which is built-in. If you do not specify SDL2 as in the command above, then SDL1 is linked in and the SDL1 include paths are used. SDL1 has support for sdl-config, which is present in system/bin. Using the native sdl-config may result in compilation or missing-symbol errors. You will need to modify the build system to look for files in emscripten/system or emscripten/system/bin in order to use the Emscripten sdl-config.
Adding more ports is fairly easy. Basically, the steps are
- Make sure the port is open source and has a suitable license.
- Add it to emscripten-ports on github. The ports maintainers can create the repo and add the relevant developers to a team for that repo, so they have write access.
- Add a script to handle it under tools/ports/ (see existing code for examples) and use it in tools/ports/__init__.py.
- Add testing in the test suite.
Some large projects generate executables and run them in order to generate input for later parts of the build process (for example, a parser may be built and then run on a grammar, which then generates C/C++ code that implements that grammar). This sort of build process causes problems when using Emscripten because you cannot directly run the code you are generating.
In some cases it makes sense to modify the build scripts so that they build the generated executable natively. For example, this can be done by specifying two compilers in the build scripts, emcc and gcc, and using gcc just for generated executables. However, this can be more complicated than the previous solution because you need to modify the project build scripts, and you may have to work around cases where code is compiled and used both for the final result and for a generated executable.
Projects that use configure, cmake, or some other portable configuration method may run checks during the configure phase to verify that the toolchain and paths are set up properly. Emcc tries to get checks to pass where possible, but you may need to disable tests that fail due to a “false negative” (for example, tests that would pass in the final execution environment, but not in the shell during configure).
Ensure that if a check is disabled, the tested functionality does work. This might involve manually adding commands to the make files using a build system-specific method.
In general configure is not a good match for a cross-compiler like Emscripten. configure is designed to build natively for the local setup, and works hard to find the native build system and the local system headers. With a cross-compiler, you are targeting a different system, and ignoring these headers etc.
Emscripten supports .a archive files, which are bundles of object files. This is an old format for libraries, and it has special semantics - for example, the order of linking matters with .a files, but not with plain object files (in .bc, .o or .so). For the most part those special semantics should work in Emscripten, however, we support .a files using llvm’s tools, which have a few limitations.
The main limitation is that if you have multiple files in a single .a archive that have the same basename (for example, dir1/a.o, dir2/a.o), then llvm-ar cannot access both of those files. Emscripten will attempt to work around this by adding a hash to the basename, but collisions are still possible in principle.
Where possible it is better to generate shared library files (.so) rather than archives (.a) — this is generally a simple change in your project’s build system. Shared libraries are simpler, and are more predictable with respect to linking.
In addition to the capabilities it shares with gcc, emcc supports options to optimize code, control what debug information is emitted, generate HTML and other output formats, etc. These options are documented in the emcc tool reference (./emcc --help on the command line).
Emscripten provides the following preprocessor macros that can be used to identify the compiler version and platform:
- The preprocessor define __EMSCRIPTEN__ is always defined when compiling programs with Emscripten.
- The preprocessor variables __EMSCRIPTEN_major__, __EMSCRIPTEN_minor__ and __EMSCRIPTEN_tiny__ specify, as integers, the currently used Emscripten compiler version.
- Emscripten behaves like a variant of Unix, so the preprocessor defines unix, __unix and __unix__ are always present when compiling code with Emscripten.
- Emscripten uses Clang/LLVM as its underlying codegen compiler, so the preprocessor defines __llvm__ and __clang__ are defined, and the preprocessor defines __clang_major__, __clang_minor__ and __clang_patchlevel__ indicate the version of Clang that is used.
- Clang/LLVM is GCC-compatible, so the preprocessor defines __GNUC__, __GNUC_MINOR__ and __GNUC_PATCHLEVEL__ are also defined to represent the level of GCC compatibility that Clang/LLVM provides.
- The preprocessor string __VERSION__ indicates the GCC compatible version, which is expanded to also show Emscripten version information.
- Likewise, __clang_version__ is present and indicates both Emscripten and LLVM version information.
- Emscripten is a 32-bit platform, so size_t is a 32-bit unsigned integer, __POINTER_WIDTH__=32, __SIZEOF_LONG__=4 and __LONG_MAX__ equals 2147483647L.
- When targeting asm.js, the preprocessor defines __asmjs and __asmjs__ are present.
- When targeting SSEx SIMD APIs using one of the command line compiler flags -msse, -msse2, -msse3, -mssse3, or -msse4.1, one or more of the preprocessor flags __SSE__, __SSE2__, __SSE3__, __SSSE3__, __SSE4_1__ will be present to indicate available support for these instruction sets.
- If targeting the pthreads multithreading support with the compiler & linker flag -s USE_PTHREADS=1, the preprocessor define __EMSCRIPTEN_PTHREADS__ will be present.
The Emscripten test suite (tests/runner.py) contains a number of good examples — large C/C++ projects that are built using their normal build systems as described above: freetype, openjpeg, zlib, bullet and poppler.
It is also worth looking at the build scripts in the ammo.js project.
Make sure to use bitcode-aware llvm-ar instead of ar (which may discard code). emmake and emconfigure set the AR environment variable correctly, but a build system might incorrectly hardcode ar.
The compilation error multiply defined symbol indicates that the project has linked a particular static library multiple times. The project will need to be changed so that the problem library is linked only once.
You can use llvm-nm to see which symbols are defined in each bitcode file.
One solution is to use the building-projects-dynamic-linking-workaround approach described above. This ensures that libraries are linked only once, in the final build stage.