How To Debug Webassemly?

Debugging by printing is a very popular debug method. This article shows how to implement a small, but useful, Webassembly print based debugging infrastructure. If you missed the previous article talking about Webassembly debugging, check it out here.

Why print based debugging is so popular?

Advantages

  • No Barrier to start, it is free, and it doesn’t require learning a tool.
  • It allows debugging your code when it is running at your customer’s site. You know, bugs never happens at the developer’s machine.
  • It is ironic, but, this debug method is the tool to show us how our code really behaves. It is not unusual to hear: huh, I did not expect the code to pass here…
  • No extra Debug symbols file is needed for debugging.

Disadvantages

  • It can leave the source code cluttered, making it hard to follow, assuming the print statements are left in the source code, which is usual.
  • The code size can grow out of control through the years unless a debug/production build scheme is used, with production build having debug print statements turned off.

The Linux kernel uses the debug by printing method, its debug printing function is called printk(), k is for kernel. As of March of 2018, the printk module has more than 3000 lines of C code, and its first version is from 1991!

How it Looks Like?

The following demo shows the basics of this implementation. There is a button that counts how many times the button is clicked, the count happens inside a Webassembly module. There is an input box that allow us to enter the debug level. In this example, entering a debug level 7 in the input box will make the Browser’s console display Webassembly debug messages with levels up to 7. We are going to debug the count that happens inside the Webassembly module by placing debug messages prior/after the count increment. Note the debug messages show several properties of its location: the C file name, function, and line number.

Debug by printing in the browser

Our Webassembly Debug by Printing (Poor Man’s Debug) implementation

The following are the content of debug-wasm.h and debug-wasm.c files. For details, take a look at the comments in the code:

  • There is a main switch that enables or disable debug in all code. When DEBUG_WASM_ENABLED is set to 0 no debug code becomes part of the compiled Webassembly module. So you can remove the entire debug code for a production build.
  • Similarly to Linux printk implementation, it defines debug levels that can be associated with each debug message, that way it is possible to enable debug messages with some specific level range, for instance calling the Dbg_Set_Level() function with DEBUG_WASM_ERR will instruct the debug module to print messages with levels lower or equal to DEBUG_WASM_ERR.
  • Functions to set and get the debug level: Dbg_Set_Level() and Dbg_Get_Level()
  • The macro to be placed in your C/C++ code to print the debug messages: DBG_LOG()

wasm-debug.h:


#include <stdarg.h>

// This define controls the debug message system at compilation time:
//  0-Disables the debug message system. If optimization is used(-Os,-Oz,...),
//      then the debug code will be optimized out because it becomes dead code.
// any other number enables it and leave the debug code ready for use
#define DEBUG_WASM_ENABLED  (1)

// Debug levels to be associated with each debug message,
// based on printk implemented in the linux kernel
// Only takes effect if DEBUG_WASM_ENABLED is not 0
typedef enum DEBUG_WASM_LEVELS
{
    DEBUG_WASM_EMERG =      0,
    DEBUG_WASM_ALERT =      1,
    DEBUG_WASM_CRIT =       2,
    DEBUG_WASM_ERR =        3,
    DEBUG_WASM_WARN =       4,
    DEBUG_WASM_NOTICE =     5,
    DEBUG_WASM_INFO =       6,
    DEBUG_WASM_DEBUG =      7
} DEBUG_WASM_LEVELS;

// Prototypes for set/get debug level, to be called from Javascript
void Dbg_Set_Level(const DEBUG_WASM_LEVELS level);
DEBUG_WASM_LEVELS Dbg_Get_Level(void);

// Main debug print function:
//  level-This debug message level
//  fmt-Message format
//  ...-Variable number of parameters with the message to be used
void Dbg_Printf(const DEBUG_WASM_LEVELS level, const char *fmt, ...);

// Main macro to be used in the wasm module.
//  It is a macro because it can then get information about the location where
//  it is placed, like file, line, function, etc...
//
#define DBG_LOG(level, fmt, ...) \
do \
{ \
    if (DEBUG_WASM_ENABLED) \
    { \
        Dbg_Printf(level,"%s:%d:%s(): " fmt, __FILE__, __LINE__, __func__, __VA_ARGS__); \
    } \
} while (0)

wasm-debug.c:


#include <emscripten.h>
#include "debug-wasm.h"

// Static variable that holds the current debug level for the entire wasm module
// By default only print messages with levels EMERG and lower
static DEBUG_WASM_LEVELS debugWasmLevel=DEBUG_WASM_EMERG;

//
// Sets a new debug level
//
EMSCRIPTEN_KEEPALIVE void Dbg_Set_Level(const DEBUG_WASM_LEVELS level)
{
    debugWasmLevel=level;
}

//
// Gets current debug level
//
EMSCRIPTEN_KEEPALIVE DEBUG_WASM_LEVELS Dbg_Get_Level(void)
{
    return debugWasmLevel;
}

//
// Main debug print function:
//  Note EMSCRIPTEN implements stderr to print into the Browser's console
//
void Dbg_Printf(const DEBUG_WASM_LEVELS level, const char *fmt, ...)
{
    // Only logs the message if the wasm debug level is equal or higher than
    // this message level
    if( level <= debugWasmLevel )
    {
        // Gets the variable argument list and feeds to vfprintf
        // EMSCRIPTEN logs writes to stderr to the console, exactly what we need
        va_list args;
        va_start(args, fmt);
        vfprintf(stderr, fmt, args);
        va_end(args);
    }
}

Placing Debug Messages Inside Your Webassembly Code

Use the DBG_LOG macro. The tricky part is to choose the places in your code where the messages are going to be placed and what debug level to use. In the example below I put these messages before and after the variable clicks is being incremented, I use the level DEBUG because I don’t want these messages to show on user’s browser all the time, only when I am debugging. It is reasonable though, to leave some critical messages always enabled because you want to know if and when they happen.

Placing debug messages inside your code and feel no regrets in telling the world you are unsure about the code…

click-count.c:


#include <emscripten.h>
#include "debug-wasm.h"

//
// This function will change the HTML value of a node(or element) that has id=countButton
//
// Important things to note:
//
// EMSCRIPTEN_KEEPALIVE
//  It forces this function to be present in the webassembly module.
//  There is no reference to this function in the module, the optimization would
//  wipe it out as it appears to be dead code.
//  This function is called from Javascript though
//
// EM_ASM_
//  EMSCRIPTEN macro that does all the interface with Javascript by us.
//  It creates a function inside the generated javascript file and call it from
//  here.
//  Generated function from the Javascript file:
//  var ASM_CONSTS = [function($0) { document.getElementById("countButton").value='Webassembly click count: '+$0 }];
// 
void EMSCRIPTEN_KEEPALIVE IncrementClickCountOnValue()
{
    static int clicks=0;
    
    // Log the content of variable clicks before it gets incremented.
    // Log message will only be shown if:
    //  -DEBUG_CODE_COMPILED_IN is not 0 (defined in debug-wasm.h) AND:
    //  -debugWasmLevel >= DEBUG_WASM_DEBUG
    DBG_LOG(DEBUG_WASM_DEBUG, "Variable clicks before=%d\n", clicks);
    
    // Here is where the variable clicks gets incremented
    EM_ASM_( {document.getElementById("countButton").value='Webassembly click count: '+$0}, ++clicks );
    
    // Log the content of variable clicks after it gets incremented
    DBG_LOG(DEBUG_WASM_DEBUG, "Variable clicks after=%d\n", clicks);
}

Demo Application

Implements what is shown in the gif at the beginning of this article, a button, a input box, etc…:


<html>
<head>
  <meta charset="UTF-8">
  <script type="text/javascript" src="debug.js"></script>
  <script>
  
    // EMSCRIPTEN "-s MODULARIZED=1" option makes it easy to load a webassembly module,
    // it generates a .js file with a Module() function that does the job.
    // The .js file also puts some glue code that allows changing the DOM from within
    // the Webassembly code (through JavaScript)
    Module()
    .then(function(instance){
        var exports = instance['asm']; // the .js file puts the exports in the 'asm' field
        // Add button listener, calls Webassembly's IncrementClickCountOnValue() when clicked
        var button = document.getElementById('countButton');
        button.addEventListener('click', function() {
            exports._IncrementClickCountOnValue();
        }, false);
        // Add button listener, change Webassembly's debug level
        var input = document.getElementById('debugLevelInput');
        input.addEventListener('change', function() {
            exports._Dbg_Set_Level(input.value);
        }, false);
      }
  );
  </script>
</head>
<body>
  <input type="button" id="countButton" value="click here to see wasm changing this message"/>
  </br> Enter New Debug Level and Hit Enter: <input type="text" id="debugLevelInput" name="txt" value="">
</body>
</html>

Compiling and Running

Two C files combined in one compilation command:


emcc click-count.c debug-wasm.c -O1 -s MODULARIZE=1 -s WASM=1 -o debug.js

Start EMSCRIPTEN server:


emrun --no_browser --port 8080 .

Point your browser to: http://localhost:8080/dom.html

A Teaser

Take a closer look at the DBG_LOG macro in debug-wasm.h. Did you notice that there is a do {} while(0) around the print stuff? Why this nonsense is there? To be answered in a future post.

Conclusion

The browser environment lends itself well for the debug by printing method as it provides a console where messages can be logged to, the method is free and easy to implement. No reason to not have this method implemented.


Leave a message below. Webassembly is evolving rapidly, please let me know if this post got outdated.

Enjoyed this post?

Don't miss new posts: Share it with your friends:

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *