Debugging WebAssembly with modern tools
The road so far
A year ago, Chrome announced initial support for native WebAssembly debugging in Chrome DevTools.
We demonstrated basic stepping support and talked about opportunities usage of DWARF information instead of source maps opens for us in the future:
- Resolving variable names
- Pretty-printing types
- Evaluating expressions in source languages
- …and much more!
Today, we’re excited to showcase the promised features come into life and the progress Emscripten and Chrome DevTools teams have made over this year, in particular, for C and C++ apps.
Before we start, please keep in mind that this is still a beta version of the new experience, you need to use the latest version of all tools at your own risk, and if you run into any issues, please report them to https://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue.
Let’s start with the same simple C example as the last time:
#include <stdlib.h>
void assert_less(int x, int y) {
if (x >= y) {
abort();
}
}
int main() {
assert_less(10, 20);
assert_less(30, 20);
}
To compile it, we use latest
Emscripten
and pass a -g
flag, just like in the original post, to include debug
information:
emcc -g temp.c -o temp.html
Now we can serve the generated page from a localhost HTTP server (for example, with serve), and open it in the latest Chrome Canary.
This time we’ll also need a helper extension that integrates with Chrome DevTools and helps it make sense of all the debugging information encoded in the WebAssembly file. Please install it by going to this link: goo.gle/wasm-debugging-extension
You’ll also want to enable WebAssembly debugging in the DevTools Experiments. Open Chrome DevTools, click the gear (⚙) icon in the top right corner of DevTools pane, go to the Experiments panel and tick WebAssembly Debugging: Enable DWARF support.
When you close the Settings, DevTools will suggest to reload itself to apply settings, so let’s do just that. That’s it for the one-off setup.
Now we can go back to the Sources panel, enable Pause on exceptions (⏸ icon), then check Pause on caught exceptions and reload the page. You should see the DevTools paused on an exception:
By default, it stops on an Emscripten-generated glue code, but on the
right you can see a Call Stack view representing the stacktrace of
the error, and can navigate to the original C line that invoked
abort
:
Now, if you look in the Scope view, you can see the original names
and values of variables in the C/C++ code, and no longer have to figure
out what mangled names like $localN
mean and how they relate to the
source code you’ve written.
This applies not only to primitive values like integers, but to compound types like structures, classes, arrays, etc., too!
Rich type support
Let’s take a look at a more complicated example to show those. This time, we’ll draw a Mandelbrot fractal with the following C++ code:
#include <SDL2/SDL.h>
#include <complex>
int main() {
// Init SDL.
int width = 600, height = 600;
SDL_Init(SDL_INIT_VIDEO);
SDL_Window* window;
SDL_Renderer* renderer;
SDL_CreateWindowAndRenderer(width, height, SDL_WINDOW_OPENGL, &window,
&renderer);
// Generate a palette with random colors.
enum { MAX_ITER_COUNT = 256 };
SDL_Color palette[MAX_ITER_COUNT];
srand(time(0));
for (int i = 0; i < MAX_ITER_COUNT; ++i) {
palette[i] = {
.r = (uint8_t)rand(),
.g = (uint8_t)rand(),
.b = (uint8_t)rand(),
.a = 255,
};
}
// Calculate and draw the Mandelbrot set.
std::complex<double> center(0.5, 0.5);
double scale = 4.0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
std::complex<double> point((double)x / width, (double)y / height);
std::complex<double> c = (point - center) * scale;
std::complex<double> z(0, 0);
int i = 0;
for (; i < MAX_ITER_COUNT - 1; i++) {
z = z * z + c;
if (abs(z) > 2.0)
break;
}
SDL_Color color = palette[i];
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
SDL_RenderDrawPoint(renderer, x, y);
}
}
// Render everything we've drawn to the canvas.
SDL_RenderPresent(renderer);
// SDL_Quit();
}
You can see that this application is still fairly small – it’s a single file containing 50 lines of code – but this time I’m also using some external APIs, like SDL library for graphics as well as complex numbers from the C++ standard library.
I’m going to compile it with the same -g
flag as above to include
debug information, and also I’ll ask Emscripten to provide the SDL2
library and allow arbitrarily-sized memory:
emcc -g mandelbrot.cc -o mandelbrot.html \ -s USE_SDL=2 \ -s ALLOW_MEMORY_GROWTH=1
When I visit the generated page in the browser, I can see the beautiful fractal shape with some random colors:
When I open DevTools, once again, I can see the original C++ file. This time, however, we don’t have an error in the code (whew!), so let’s set some breakpoint at the beginning of our code instead.
When we reload the page again, the debugger will pause right inside our C++ source:
We can already see all our variables on the right, but only width
and height
are initialized at the moment, so there isn’t much to
inspect.
Let’s set another breakpoint inside our main Mandelbrot loop, and resume execution to skip a bit forward.
At this point our palette
has been filled with some random colors,
and we can expand both the array itself, as well as the individual
SDL_Color
structures and inspect their components to verify that
everything looks good (for example, that “alpha” channel is always set
to full opacity). Similarly, we can expand and check the real and
imaginary parts of the complex number stored in the center
variable.
If you want to access a deeply nested property that is otherwise hard to navigate to via the Scope view, you can use the Console evaluation, too! However, note that more complex C++ expressions are not yet supported.
Let’s resume execution a few times and we can see how the inner x
is
changing as well – either by looking in the Scope view again, adding
the variable name to the watch list, evaluating it in the console, or by
hovering over the variable in the source code:
From here, we can step-in or step-over C++ statements, and observe how other variables are changing too:
Okay, so this all works great when a debug information is available, but what if we want to debug a code that wasn’t built with the debugging options?
Raw WebAssembly debugging
For example, we asked Emscripten to provide a prebuilt SDL library for
us, instead of compiling it ourselves from the source, so – at least
currently – there’s no way for the debugger to find associated sources.
Let’s step-in again to get into the SDL_RenderDrawColor
:
We’re back to the raw WebAssembly debugging experience.
Now, it looks a bit scary and isn’t something most Web developers will ever need to deal with, but occasionally you might want to debug a library built without debug information – whether because it’s a 3rd-party library you have no control over, or because you’re running into one of those bugs that occurs only on production.
To aid in those cases, we’ve made some improvements to the basic debugging experience, too.
First of all, if you used raw WebAssembly debugging before, you might
notice that the entire disassembly is now shown in a single file – no
more guessing which function a Sources entry wasm-53834e3e/
wasm-53834e3e-7
possibly corresponds to.
New name generation scheme
We improved names in the disassembly view, too. Previously you’d see just numeric indices, or, in case of functions, no name at all.
Now we’re generating names similarly to other disassembly tools, by
using hints from the WebAssembly name
section,
import/export paths and, finally, if everything else fails, generating
them based on the type and the index of the item like $func123
. You can
see how, in the screenshot above, this already helps to get slightly
more readable stacktraces and disassembly.
When there is no type information available, it might be hard to inspect any values besides the primitives – for example, pointers will show up as regular integers, with no way of knowing what’s stored behind them in memory.
Memory inspection
Previously, you could only expand the WebAssembly memory object –
represented by env.memory
in the Scope view – to look up
individual bytes. This worked in some trivial scenarios, but wasn’t
particularly convenient to expand and didn’t allow to reinterpret data
in formats other than byte values. We’ve added a new feature to help
with this, too: a linear memory inspector.
If you right-click on the env.memory
, you should now see a new
option called Inspect memory:
Once clicked, it will bring up a Memory Inspector, in which you can inspect the WebAssembly memory in hexadecimal and ASCII views, navigate to specific addresses, as well as interpret the data in different formats:
Advanced scenarios and caveats
Profiling WebAssembly code
When you open DevTools, WebAssembly code gets “tiered down” to an
unoptimized version to enable debugging. This version is a lot slower,
which means that you can’t rely on console.time
, performance.now
and other methods of measuring speed of your code while DevTools are
open, as the numbers you get won’t represent the real-world performance
at all.
Instead, you should use the DevTools Performance panel which will run the code at the full speed and provide you with a detailed breakdown of the time spent in different functions:
Alternatively, you can run your application with DevTools closed, and open them once finished to inspect the Console.
We’ll be improving profiling scenarios in the future, but for now it’s a caveat to be aware of. If you want to learn more about WebAssembly tiering scenarios, check out our docs on WebAssembly compilation pipeline.
Building and debugging on different machines (including Docker / host)
When building in a Docker, virtual machine, or on a remote build server, you will likely run into situations where the paths to the source files used during the build don’t match the paths on your own filesystem where the Chrome DevTools are running. In this case, files will show up in the Sources panel but fail to load.
To fix this issue, we have implemented a path mapping functionality in the C/C++ extension options. You can use it to remap arbitrary paths and help the DevTools locate sources.
For example, if the project on your host machine is under a path
C:\src\my_project
, but was built inside a Docker container where
that path was represented as /mnt/c/src/my_project
, you can remap
it back during debugging by specifying those paths as prefixes:
The first matched prefix “wins”. If you’re familiar with other C++
debuggers, this option is similar to the set substitute-path
command
in GDB or a target.source-map
setting in LLDB.
Debugging optimized builds
Like with any other languages, debugging works best if optimizations are disabled. Optimizations might inline functions one into another, reorder code, or remove parts of the code altogether – and all of this has a chance to confuse the debugger and, consequently, you as the user.
If you don’t mind a more limited debugging experience and still want to
debug an optimized build, then most of the optimizations will work as
expected, except for function inlining. We plan to address the remaining
issues in the future, but, for now, please use -fno-inline
to
disable it when compiling with any -O
level optimizations, e.g.:
emcc -g temp.c -o temp.html \ -O3 -fno-inline
Separating the debug information
Debug information preserves lots of details about your code, defined types, variables, functions, scopes, and locations – anything that might be useful to the debugger. As a result, it often can be larger than the code itself.
To speed up loading and compilation of the WebAssembly module, you might
want to split out this debug information into a separate WebAssembly
file. To do that in Emscripten, pass a -gseparate-dwarf=…
flag with
a desired filename:
emcc -g temp.c -o temp.html \ -gseparate-dwarf=temp.debug.wasm
In this case, the main application will only store a filename
temp.debug.wasm
, and the helper extension will be able to locate and
load it when you open DevTools.
When combined with optimizations like described above, this feature can be even used to ship almost-optimized production builds of your application, and later debug them with a local side file. In this case, we’ll additionally need to override the stored URL to help the extension find the side file, for example:
emcc -g temp.c -o temp.html \ -O3 -fno-inline \ -gseparate-dwarf=temp.debug.wasm \ -s SEPARATE_DWARF_URL=file://[local path to temp.debug.wasm]
To be continued…
Whew, that was a lot of new features!
With all those new integrations, Chrome DevTools becomes a viable, powerful, debugger not only for JavaScript, but also for C and C++ apps, making it easier than ever to take apps, built in a variety of technologies and bring them to a shared, cross-platform Web.
However, our journey is not over yet. Some of the things we’ll be working on from here on:
- Cleaning up the rough edges in the debugging experience.
- Adding support for custom type formatters.
- Working on improvements to the profiling for WebAssembly apps.
- Adding support for code coverage to make it easier to find unused code.
- Improving support for expressions in console evaluation.
- Adding support for more languages.
- …and more!
Meanwhile, please help us out by trying the current beta on your own code and reporting any found issues to https://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue.
Stay tuned for future updates!
<<../../_shared/devtools-feedback.md>>
<<../../_shared/discover-devtools-blog.md>>