Setting up the ultimate WebAssembly (C++) workflow
Date: 13th October 2017
To avoid future pain and wasted time, I’ve put together what I will be using and maintaining from now on as my WebAssembly project shell, for current and future projects. The final result is hosted on GitHub, should you wish to skip straight to that.
Below, I’ll talk about some of the tools used, the motivation for them, and some alternatives, for slightly different projects. At a glance, the main tools used are emscripten, MinGW (for Windows users), grunt, mocha + chai + sinon, google test + google mock, travis, coveralls and codecov. You will need git and nodejs v8+.
Tools and setup
So the first step, of course, would be to install emscripten. The easiest installation I’ve found is using the portable installer from the emscripten website.
Extract the files into “C:\emsdk\” and make sure to run cmd with admin privileges. These will make things easier later on.
Run emsdk update and emsdk install latest. Then run emsdk activate latest --global. The global bit will attempt to sort out the PATH. Finally, run emsdk_env.sh. This might change your nodejs path to the v4.1.1 version it just installed. If it does, just remove it from the path variables, as we’ll need v8.0 or greater.
For macs and linux, these commands may differ slightly.
MinGW and msys
If you’re using Windows, you will need msys to emulate a linux environment, for some of the C++ stuff. You will also need a few packages, from the MinGW Installation Manager. The steps I took to install it are as follows:
- Install cmake 3.9.4, into C:/ (with the path set up for all users)
- Install Python 2.7 into C:/
- Install MinGW into C:/ (when finished, you can find msys at C:\MinGW\msys\1.0\msys.bat)
- Make sure the following packages are installed:
After a re-start, you should be ready to set up your project repo.
To keep things organised, the dev files are separated into the JS and C++ folders.
The files get compiled and minified into the dist folder.
The test files will go in the test folder, where the testing framework will go (more on this and the build folder later).
The package.json will be useful for some tasks, but we’ll come back to that later. After initializing it, however, we can start with grunt. The most convenient way to compile files is to just do it automatically when you save the file. Therefore, we’ll be calling emscripten’s emcc command from within here.
grunt-contrib-watch can listen for file changes, and grunt-exec can run the emcc command. Emscripten requires the path variables to be set in the terminal you use for compiling with, so we need to call emsdk first. If you’ve installed it globally, you can try beginning your exec command with just emsdk_env.
The emcc command, afterwards needs the input file, the target, and a few flags (see the gist below).
If running emcc globally doesn’t work, and error messages are not helpful, you can fall back on using the absolute path. If you extracted it at “C:/emsdk”, this would be “C:/emsdk/emsdk_env”.
Normally I would include babel between these two, but if you are targeting WebAssembly capable browsers, this is not needed. However, if you are building two versions of your app (one for non-WebAssembly capable browsers), you may need to include grunt-babel, and use two separate task routes (see the jsNet gruntfile as an example of what that would look like).
For writing mocks, we’ll additionally use sinon, and sinon-chai. This will be crucial for stubbing out any WASM module calls from within your code.
With C++, the choice was not immediately obvious. The list of available frameworks is huge.
I have personally used the Catch framework, which is dead easy to set up, as it is just a header file that gets included in your test files. If you’re building a small project, this might be the most appropriate, and needs the least set-up.
However, the rich set of assertions, built in mocking and larger support made Google Test the obvious choice for this particular. I’ve also personally encountered some issues with Catch, once test cases count reached hundreds.
Unlike node, which has npm, installing the framework into the project is not as straight forward, and adding the framework files into your repo is not the best idea. What you can do instead, however, is add the framework as a git sub-module. This is why you need to add the --recursive flag, when cloning the repo.
Building and running
The C++ tests
The easiest way to set things up is to use make files. This is one of the main reasons we need msys on Windows, as these are files used on linux systems. The CMakeLists.txt file is used for setting up the project files, libraries, and other such things, and will be used for building the work space. You call it using cmake, from within a ‘build’ folder.
One thing to note is that if using msys, you will need to add the -G”MSYS Makefiles” flag, to make it use the correct compiler. If this fails, you may have mis-configured msys.
You can use package.json to automate running this, in a new script, called “build” (see above).
If you decided to use Catch for the C++ testing framework, you won’t need any of this, you can just set the package.json cpp-test script to something like “g++ -std=c++11 test/cpp-test.cpp -o test/catch.exe & cd test & catch.exe”.
If you need to carry out end to end testing, or otherwise just need to test data actually being passed to the WASM code, you can just import it in the JS test code. Have a look at wasm-arrays on GitHub to see an example.
The compiled project
If you were not aware already, WebAssembly files need to be served via a server, meaning you cannot just open the html file straight in the browser. Nodejs servers are easy to write, and I’ve included one in the GitHub repo.
One way to do this in browsers is to fire a JS event in the main function, in the emscripten file, and listen for it in JS.
With nodejs, you can create an onWASMLoaded global function to call instead, as nodejs has no window object for events.
For CI, I picked the tried and tested Travis. It is free for open source repos, and you also get a number of free builds for private repos. You’ll need a .travis.yml file where you can tell it what it needs to do to run the tests and coverage.
I was originally planning to use Coveralls for both the JS and C++ test coverage, but then I realized they don’t easily support coverage reports from multiple languages. Therefore I had Codecov take care of the C++ coverage, leaving Coveralls to take care of the JS coverage. It should be noted that if you want them combined, Codecov does seem to support multiple languages. However, having the metrics separated turned out to be unexpectedly useful.
Ok, so all the tools, frameworks, task runners and CI are set up. You are ready to begin developing. To start working, you need to open up a terminal and run grunt. This will now listen for file changes on development JS and C++ files, and run the compilation/packaging as needed for each one.
The JS files will be merged (with source maps for the tests) and minified. The C++ files will be compiled by emscripten, and JS output minified. Your tests will cover both JS and C++, and you are able to deeply test every corner of your program using mocks. When committing, Travis will check that tests for both languages pass, and will run and report the coverage to Coveralls and Codecov. From these, you can also get the badges for your repo, so you can show off how awesome your project is. ☻
All of this is readily available on GitHub, here. I’ve also included some sample code for some test mocking, to get things started, if this is new to you.
There is plenty of room for future work, still, of course.
One thing I was not able to get working in the time I had was gcov / lcov, for seeing C++ test coverage while developing. It seemed like a big mess to get it working on windows, and wouldn’t have been easy to set up straight from the repo. However, check back every now and then and me or someone else may have figured out a way to do it.