How to bring your C++ code to the web using Emscripten.
Table of Contents
WebAssembly's predecessor, asm.js converts C/C++, Rust code into a low level JavaScript to run on the web browser. The unveiling of WebAssembly on March 2017 has enabled code, native or managed alike, compiled down to a binary instruction format for a stack-based virtual machine making possible performance improvement from modest 20% to 600% over JavaScript, undreamed-of feat a decade ago. Today's article provides step-by-step instructions on installing the Emscripten toolset and having the "Hello World
" program running on web browser of our choice in less than 20 minutes. Reader's timing may vary depending on his internet speed/quality.
In order to use the Emscripten, we need to install Windows Subsystem For Linux (WSL), a feature available only on Windows 10, so that rules out older Windows version. If you are an apt Linux user, you are welcome to use your favourite Linux distribution. You can also download those Emscripten windows installer if you do not want to go through the hassle of installing a Linux OS. Those windows installer have never worked for me but that was a few years ago, it is possible the situation has improved. No harm trying the windows installer before going the Linux route. Latest windows installers usually are few versions behind the latest and greatest version. It might be okay if you are not those who live on the bleeding edge of technology.
Why WSL?
Other virtual machine options includes Oracle's VirtualBox and Microsoft's Hyper-V. VirtualBox only supports 32-bit guest OS. Emscripten tool can only be built with more than 4GB RAM, more accurately for linkage, not compilation. More RAM than 4GB mean 64-bit OS, so that excludes VirtualBox. Hyper-V has 64-bit guest support but only comes with Windows 10 Pro. For home users, they typically has Windows Home edition. WSL is the most attractive choice for our case.
Enabling WSL on Windows 10
Before we can install Ubuntu from Microsoft, we must first enable Developer's mode and WSL. To enable Developer mode, head to Settings > Update & Security > For Developers and select “Developer mode”.
This can also be accomplished via Powershell. Launch PowerShell in administrative mode. And type the following and hit 'Enter' key.
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux
Next, WSL has to be enabled on Windows Features. In the Windows searchbar, type "Turn Windows Features On or Off" and select that option. Scroll all the way down and check the "Windows Subsystem For Linux" option as shown.
Installing Ubuntu 18.04
Next, launch Microsoft Store by clicking its button on the taskbar. Search for "Ubuntu" and click "Install" on the Ubuntu 18.04 in the search result. It takes about 5 minutes.
Ubuntu First Launch Important Notes
After Ubuntu is installed, Windows ask you to launch it. Stop! Do not touch the Launch button! You have to launch Ubuntu with administrative rights by right-clicking it and click "Run as administrator". After this message, "Installing, this may take a few minutes..." appears and disappears, Ubuntu prompts you for username and password during the 1st launch. Now we can start installing Emscripten.
Updating Ubuntu Packages and Installing Python 2.7
Since this is the fresh Ubuntu installation, we have to update the packages prior to installing Python. Run these 3 commands. It may take quite a while but do not cancel midway.
sudo apt update
sudo apt upgrade
sudo apt install python2.7 python-pip
You may have to install Git. Run the command below:
sudo apt-get install git-core
Next, check out Emscripten from the GitHub.
git clone https://github.com/emscripten-core/emsdk.git
Finally, we are ready for Emscripten installation. Run the commands below to download and install a precompiled toolchain.
# change to the newly cloned emsdk directory.
cd emsdk
# Download and install the latest SDK tools.
./emsdk install latest
# Set up the compiler configuration to point to the "latest" SDK.
./emsdk activate latest
# Activate PATH and other environment variables in the current terminal
source ./emsdk_env.sh
For the last step, it has to be called every Ubuntu startup in order to set the environment variables and path before you utilize the Emscripten toolset.
Compiling the Toolchain for Unsupported Linux Distributions or Just for Fun
For those readers with unsupported Linux distributions, you can build the Emscripten toolchain with these commands. It can take up to 3 hours to build depending on your storage type like SSD or HDD, number of CPU cores and amount of RAM.
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit
./emsdk activate --build=Release sdk-incoming-64bit binaryen-master-64bit
In this section, we build a C "hello world
" program followed with the C++ version and compare their output file size.
Hello World C Program
This is the C source we use. There is a quirk with this program I have to let you know. The printf()
format string must end with a newline that serves a flag to flush the console output, else you get no output.
#include <stdio.h>
int main()
{
printf("Hello, world!\n");
}
We saved the source in hello.c. This is the command to build hello.c. It takes about 5 minutes as it builds the required C static libs with file extension of bc. Subsequent builds should be snappy.
emcc hello.c -s WASM=1 -o hello.html
The output of the compilation are hello.html, hello.js and lastly the hello.wasm which is the Webassembly file.
Hello World C++ Program
#include <iostream>
int main()
{
std::cout << "Hello, world!" << std::endl;
}
We saved the C++ source in hello2.cpp. The command to build hello2.cpp. As with C version, it takes awhile to build the C++ static libs.
emcc hello2.cpp -s WASM=1 -o hello2.html
The output of the compilation are hello2.html, hello2.js and lastly the hello2.wasm. Single file compilation is fine for one file. For building multiple source files with make
command, just open your existing Makefile in text editor and replace all the "gcc" or "g++" occurrences with "emcc". The C++ output files are 400KB more than C ones due to template bloat brought in merely by including iostream
header.
To view the HTML, open up Visual Studio 2019 and create an ASP.NET project and add the just mentioned HTML, JavaScript and wasm files into the newly created project. Right click on the hello.html to view in the browser. Do the same thing for hello2.html. Visual Studio 2017 development web server has problems serving out a wasm file. Use VS2019 or other web server. For other web server, you may have to add the MIME type for Webassembly (Content-Type=application/wasm) to its configuration file if it hasn't been aded already. The exact method of doing this differs from each web server, be sure to check the manual or instruction guide. If you do not wish to set the MIME type and want to see the HTML output, try setting WASM=0
during emcc build to generate asm.js code. asm.js file type is legitimate JavaScript file that all web servers have no problem serving. asm.js is in maintenance mode, meaning all the new shiny features shall only come to Webassembly. For the next 2 sections, we explore C/C++ interaction with JavaScript.
To call JavaScript from C++, put your JavaScript code within EM_ASM()
. Since EM_ASM()
isn't legal C++ code, you have to guard the code with a macro to stop C++ compiler from parsing this. In my case, I declare __EMSCRIPTEN__
in my Emscripten Makefile. The example below find a WebAudio
element named MyMusic
and call its play
method to play the MP3.
#ifdef __EMSCRIPTEN__
EM_ASM(
document.getElementById("MyMusic").play();
);
#endif
To pass some arguments to the EM_ASM
JavaScript snippets, call the EM_ASM_
with a underscore suffix with your arguments: $0
is the placeholder for first argument and $1
is the second and so on.
EM_ASM_({
console.log('I received: ' + $0);
}, 100);
To return a integer or double value from JavaScript snippet, call EM_ASM_INT
or EM_ASM_DOUBLE
.
int x = EM_ASM_INT({
console.log('I received: ' + $0);
return $0 + 1;
}, 100);
printf("%d\n", x);
In order for JavaScript to call the C function, you have to export the C function during compilation and call Module.cwrap()
in JavaScript to put it in a callable wrapper. Below is the signature of the gen_enum_conv
. Since all C++ compilers mangle/change the function names: to avoid that so that the function name remained intact for JavaScript to find it, we must declare extern "C"
before the function signature.
extern "C" const char* gen_enum_conv(const char* cs);
This is the Makefile with exported function of main
and gen_enum_conv
: both names are preceded with underscore which is a naming convention.
CC=emcc
SOURCES:=~/EnumStrConv.cpp
SOURCES+=~/ParseEnum.cpp
LDFLAGS=-O2 --llvm-opts 2
OUTPUT=~/EnumConvGen.html
EMCC_DEBUG=1
all: $(SOURCES) $(OUTPUT)
$(OUTPUT): $(SOURCES)
$(CC) $(SOURCES) --bind -s NO_EXIT_RUNTIME=1 -s
EXPORTED_FUNCTIONS="['_main', '_gen_enum_conv']" -s
ALLOW_MEMORY_GROWTH=1 -s DEMANGLE_SUPPORT=1 -s ASSERTIONS=1
-D__EMSCRIPTEN__ -std=c++11 $(LDFLAGS) -o $(OUTPUT)
clean:
rm $(OUTPUT)
The JavaScript code to wrap this function and call with a button click is shown below. To peruse more of the code, please go to EnumConvGen GitHub. EnumConvGen
is a C++ project to generate C++ enum
to string
conversion functions and vice-versa.
<script>
var gen_enum_conv_func;
function btnClick()
{
var input_str = document.getElementById("InputTextArea").value;
document.getElementById("OutputTextArea").value = gen_enum_conv_func(input_str);
}
$( document ).ready(function() {
gen_enum_conv_func = Module.cwrap('gen_enum_conv', 'string', ['string']);
$('[name="GenButton"]').click(btnClick);
});
</script>
What About Calling C++ Member Function?
What I have just shown you is calling C function. Then what about calling C++ member function from JavaScript? You have to use embind to accomplish that. Refer to its documentation on how to do that. For myself, I do not use embind because of its complexity. What I usually do, is I encapsulate all my C++ calls inside the C function body. I haven't encountered a situation that specifically calls for me to use embind to do what I need to do.
Emscripten is restricted to calling portable functions from C++ Standard Library and the emscripten ported C/C++ libraries (see list below). OS specific functions like win32 and functions with assembly code are out of question. The list of libraries mainly in areas of graphics, audio, network and font, as you can see, are geared towards enabling game programming.
- SDL2
- regal
- HarfBuzz
- SDL2_mixer
- SDL2_image
- Cocos2d
- FreeType
- asio
- SDL2_net
- SDL2_ttf
- Vorbis
- Ogg
- Bullet
- libpng
- zlib
That's all, folks! Stay tuned for second installment of the "Bring your XXX" article series! Meanwhile, have fun with Webassembly!
Other Articles in the Bring Your... Series
History
- 29th June, 2019: Initial version