Exception Stack Tracing
Stack Traces, often taken for granted by Java developers, feel quite missing from D. Their lack of inclusion makes some sense, given D's heritage and how closely it holds to both windows and linux exception handling mechanisms. Its a nice-to-have that makes debugging very easy to do, even without a debugger or IDE handy.
Fortunately, with D's potential to resolve symbols to runtime addresses, we can improve the situation. Quite possibly the best way to accomplish this is to:
- Modify the Exception class to contain minimal stack unwind information.
- A tweak to the exception handling loop to record the stack info.
- Some extra class to actually generate a readable trace
class Exception : Object { void*[] ipList; this(char[] msg); void print(); char[] toString(); }
The ipList is used to track the calling IP's, and is built up during the stack unwind later on. It is always primed with the calling location of the call to new() for this Exception, as extracted from the call stack.
DDL will need an extension to provide line number information to allow reverse-lookups of what line number a given address corresponds to. This will largely be done via debug information provided in the loaded libraries. Obviously, non-debug builds will not be able to furnish accurate line information, if any at all.
// shown here are additions only, for brevity struct LineInfo{ DynamicModule mod; ExportSymbol sym; uint linenumber; static LineInfo NONE; } class DynamicModule{ LineInfo getLineInfo(void* address); } class DynamicLibrary{ LineInfo getLineInfo(void* address); } abstract class LoaderRegistery{ LineInfo getLineInfo(void* address); }
The Exception's 'ipList' field will be populated with the stack frames between throw and catch, iteratively, via each call to eh_find_caller. This is also used within deh2.d to unwind the stack.
uint __eh_find_caller(uint regbp, uint *pretaddr,inout void*[] ipList) { uint bp = *cast(uint *)regbp; if (bp) // if not end of call chain { // Perform sanity checks on new EBP. // If it is screwed up, terminate() hopefully before we do more damage. if (bp <= regbp) // stack should grow to smaller values terminate(); *pretaddr = *cast(uint *)(regbp + int.sizeof); ipList ~= *pretaddr; //NEW: add the return address to the list } return bp; } //TODO: somewhere in _d_throw, pass along the exception's ipList to eh_find_caller
The _d_throw() routine will now forward the properly modified Exception object to the proper catch handler, wherever it may be. The StackPrinter? class then provides the last piece of functionality: marrying the list of IP locations with line number information extracted from DDL to make a readable report.
class StackPrinter{ LoaderRegistry registry; this(LoaderRegistry registry){ this.registry = registry; } char[] printStackTrace(Exception e){ char[] trace = ""; for(loc; e.ipList){ auto lineInfo = registry.getLineInfo(loc); if(lineInfo != LineInfo.NONE) with(lineInfo){ char[] filename; if("filename" in mod.getAttributes) filename = mod.getAttributes["filename"]; else filename = "???"; trace ~= "in " ~ filename ~ "(" ~ itoa(linenumber) ~ "): " ~ demangle(sym.name) ~ "\n"; } else{ trace ~= "<unknown location>\n"; } } } return trace; } }
The last step is actually generating the stack trace. Thanks to the steps taken above, this is largely transparent.
void myfunction(){ try{ doSomething(); } catch(Exception e){ with(new StackPrinter(myDDLRegistry)) writefln(printStackTrace(e)); } }
Issues
The scheme breaks (partially) if an exception isn't thrown right away. While I haven't a clue where or why a developer would do this, but its possible that the first location (origin) in the ipList is created and technically correct, but then the entire Exception is then thrown from an entirely different place and time in the application. One possible fix is to denote the first line of output differently, as a "point of creation" for the Exception, and leave it at that.
Another problem is on the DDL side. The current example only searches through registered libs. Non-registered, but held, libraries should be made searchable for this too. This can be covered by allowing an alternate search that allows for a set of libraries to be passed in, such that they are searchable along side the reigstered set.
Lastly, and most obvious: this requires a modification to Ares/Phobos.
