How to simplify debugging unit tests in D

Johannes Riecken
dunnhumby Science blog
6 min readOct 22, 2019
Dependencies when debugging associative arrays

Introduction

At my previous job, where I worked as a C# developer, I found the visual debugger extremely useful, so before I started my job as a D developer, I fretted over how I could still maintain all my beloved debugging workflows after starting to use D. And so I spent the weeks before starting my job by feverishly cramming the entire manual of the GNU debugger gdb, the most feature-rich debugger for C, C++ and D. This made me reasonably confident about my debugging skills.

Problem

But when I tried to actually use it at my job, there came the disappointment: Symbols were incorrectly demangled, incorrect memory addresses are given in backtraces and the print command didn’t print the contents of associative arrays. Had I learned about gdb in vain? Would I have to go back to inserting print statements? Thanks to the help of my colleagues and some investigation on my side, I could improve the debugging experience considerably. So let’s take a look at what happens when a unit test fails and how we can debug it.

Requirements

In order to follow along with the examples you will need:

D compiler

  • A D compiler. I recommend GDC 9, as in my experience it demangles symbols better (as much as symbol names like TypeInfo_AAya can lead to amusement in the office) and version 9 introduced the switch -fmain which can execute unit tests. On Ubuntu 18.04 (bionic), I could install gdc-9 with apt by adding a custom apt repository.

gdb

  • The GNU Debugger. I’m using the gdb version 8.2 that shipped with my Ubuntu version. Newer versions might offer better demangling or even one day allow printing associative arrays out of the box (It’s on the to-do list of the gdc maintainer, who also works in our office).

Operating system

I’ve only tested this trick on Linux, but it should work on any system supported by the D compiler and gdb.

Problem

So on to the problem. Let’s write a simple failing unit test in a file test.d:

unittest
{
int[int] dict = [1: 10, 2: 20, 3: 30];
assert(dict == [2: 20, 1: 10, 3: 30]); // passes
assert(dict == [1: 10]); // fails
}

Simple enough. Let’s compile it with gdc-9 -g -fmain -funittest -o test test.d. The -g option tells gdc to compile in debug symbols, whereas -fmain and -funittest allow us to run unit tests in a module not containing a main function. If your module already contains a main function, then you may decide not to compile it for running unit tests by writing

version (unittest) {}
else
int main ()
{
...
}

Assertions don’t give values

Now if we run this by executing the generated executable ./test, what do we get?

core.exception.AssertError@test.d(5): unittest failure
----------------
../../../../src/libphobos/libdruntime/core/exception.d:459 onAssertErrorMsg [0x7ffff7b376d3]
??:? void test.__unittestL1_1() [0x555555554ba6]
??:? void test.__modtest() [0x555555554bc2]
../../../../src/libphobos/libdruntime/core/runtime.d:561 __foreachbody2 [0x7ffff7b38ab7]
../../../../src/libphobos/libdruntime/rt/minfo.d:777 __foreachbody2 [0x7ffff7b618f8]
../../../../src/libphobos/libdruntime/gcc/sections/elf_shared.d:109 int gcc.sections.elf_shared.DSO.opApply(scope int delegate(ref gcc.sections.elf_shared.DSO)) [0x7ffff7b4ac61]
../../../../src/libphobos/libdruntime/rt/minfo.d:770 int rt.minfo.moduleinfos_apply(scope int delegate(immutable(object.ModuleInfo*))) [0x7ffff7b636ab]
../../../../src/libphobos/libdruntime/object.d:1598 int object.ModuleInfo.opApply(scope int delegate(object.ModuleInfo*)) [0x7ffff7b56c4b]
../../../../src/libphobos/libdruntime/core/runtime.d:551 runModuleUnitTests [0x7ffff7b38db1]
../../../../src/libphobos/libdruntime/rt/dmain2.d:488 runAll [0x7ffff7b5e5e4]
../../../../src/libphobos/libdruntime/rt/dmain2.d:464 tryExec [0x7ffff7b5e1ed]
../../../../src/libphobos/libdruntime/rt/dmain2.d:497 _d_run_main [0x7ffff7b5e3b4]
??:? main [0x555555554bec]
??:? __libc_start_main [0x7ffff76d9b96]
??:? _start [0x5555555548a9]
??:? ???[0xffffffffffffffff]

That’s a lot of information for such a small unit test, isn’t it? And the first line is even kind of useful, telling us that the unit test failed in line 5 of test.d, which we expected. Now wouldn’t it be even more useful if we knew why the unit test failed? Apparently D doesn’t print that information automatically on assertion failure, supposedly for performance reasons. In the case of unit tests, this problem can be dealt with by adding a second parameter like this:

assert(dict == [1: 10], “%s != %s”.format(dict, [1:10]));

But in some cases it’s necessary to use a debugger and then printing associative arrays requires some trickery.

gdb can’t print associative arrays

Remember how we compiled debug information into the executable by specifying the -g flag? Let's fire up gdb and see what information we get:

$ gdb ./test

From the backtrace when running our unit test we know that the unit test fails on line 5 of the file test.d, so let’s set a breakpoint there:

(gdb) break test.d:5
Breakpoint 1 at 0xae4: file test.d, line 5.

And let’s run the program:

(gdb) run
Starting program: /tmp/test
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, test.__unittestL1_1() () at test.d:5
5 assert(dict == [1: 10]); // fails

Good! It stops right where it should stop, but now we’re stuck:

(gdb) print dict
$1 = {ptr = 0x7ffff7ec2000}
(gdb) print dict.ptr
$2 = (void *) 0x7ffff7ec2000

Printing the associative array just gives us a memory address and we don’t have a way to dereference it. But there still is a way to get at the value during a debugging session, even if it’s a bit complex:

Approach

One of the most useful functions in D surely is writeln in the std.stdio module. Pass it any value and it will find a way to print it, mostly printing out the information we need about a value. Wouldn't it be nice if we could use writeln during a debugging session? The problem is that the D compiler has to write a specific writeln function every time the function is called with a different parameter type, so we can't just load the machine code of the writeln function into gdb and have it work with any parameter type. One way to use the writeln function during debugging would be to write a customized writeln function into the module we are compiling, but there is a better way that does not require rewriting our module and doesn't even require us to stop and restart our debugging session. To achieve this we will compile a dynamic library containing the customized writeln implementations and use dynamic loading to make sure these functions are callable from the debugging session.

Printing associative arrays

Create a file helpers.d:

import std.stdio;extern (C)
void writelnAssocIntInt(int[int] dict)
{
writeln(dict);
}

extern (C) is specified so that the function's name doesn't change in gdb. Now compile this into a dynamic library with gdc-9 -fPIC -shared -g -o libhelpers.so helpers.d. In our gdb session we can use the dynamic loading functionality from the C library to load our library. The function to load a dynamic library has the following signature:

void *dlopen(const char *filename, int flags);

For the filename, it’s easiest to use the absolute path to our library and for the flag I had to use RTLD_LAZY for lazy binding, as for some reason not all symbols in the library could be bound in my case. In gdb, we also have to specify the return type of the function explicitly (after breaking on line 5 again), like this:

call (void*)dlopen("./libhelpers.so", RTLD_LAZY)

Now we can finally get at the value of our associative array:

(gdb) p writelnAssocIntInt (dict)
[3:30, 2:20, 1:10]

Printing private data types

Let’s continue with the following unit test module:

module test;private struct S
{
int i;
double d;
}
unittest
{
auto s_dict = [1: S(42, 3.14)];
assert(s_dict == [1: S(42, 2.718)]);
}

The private struct S can't be referred to directly within the helper’s module. But there is a simple workaround. Just prepend the module name:

import std.stdio;
import test;
extern (C)
void writelnAssocIntS (test.S[int] dict)
{
writeln(dict);
}

After recompiling (gdc-9 -fPIC -shared -g -o libhelpers.so test.d helpers.d) and dlopening, we can print the structs in associative arrays.

(gdb) p writelnAssocIntS (s_dict)
[1:S(42, 3.14)]
$2 = void

Troubleshooting

Because there are so many steps involved, and the steps work at a very low level, many things can go wrong. Here are the most common pitfalls I encountered:

dlopen returns null pointer

If anything goes awry when loading the dynamic library, dlopen will return a null pointer. Luckily, we can call dlerror (with print (char*)dlerror()) to find out the reason. If the error mentions a module info not being found, then the mentioned module must also be added to the command line used to compile the helper’s library.

Function name doesn’t appear

If the library loaded correctly, but the name of its writeln function doesn’t appear, then maybe the function wasn’t annotated with extern (C).

Dynamic library can’t be loaded

The dynamic linker has a special set of rules to search for the location of dynamic libraries. Check man dlopen for further information.

Conclusion

I hope this article will help you to get started with debugging D projects using gdb. Over its long history gdb has accumulated an endless amount of features and usually there’s always a way to get at the information one is looking for, and even to automate tedious debugging tasks (Writing and loading the helper’s module would also be a great task to automate). gdb debugging support has improved a lot with time, which is almost entirely the work of Iain Buclaw, the GDC maintainer. I’m looking forward to compiler developers further improving debug info quality, which might help us to solve the most thorny debugging problems. I’d be glad to know what other debugging tricks you have found. Happy debugging!

--

--