5
$\begingroup$

Having done my type checker, I'm thinking on targetting C++. How can I efficiently add debugging information to thrown exceptions?

Similiar to JavaScript, you can throw anything. Error, when created, should have its call stack information based from where its constructor was invoked. Errors may be created and thrown indirectly, like:

const e = new Error
throw e

Basically what I thought is that I can have a global, program-static stack, in the systems language side, holding such debug information, so it'd look like:

callStackDebug = [
    '[top-level] at index.vs:1',
    'f() at index.vs:3',
]

For this example:

f()
function f(): void {
    throw new Error('Message')
}

Then you get something like:

Error: Message

  • at f() at index.vs:3
  • at [top-level] at index.vs:1

However, I'm worried that this program-static debug information stack may not work for asynchronous functions or concurrent code. It may also be problematic in case code suspends due to a throw and an exception handler catches it, leading to wrong information:

try {
    // ...
} catch (e: RangeError) {
    // ...
}
// debug information can be incorrect now due to call depth?

Considering I'm going to target C++, I ask, how can I overcome this?

$\endgroup$

1 Answer 1

4
$\begingroup$

Conceptually, the call stack information you want already lives in the call stack itself- the return addresses correspond to the source positions immediately after each active call.

This still applies to asynchronous and concurrent code: each individual task or thread gets its own stack or some equivalent. For instance, when using native threads, there will be multiple native call stacks, while async/await-style tasks tend to have some representation of what to do on completion, which typically is (or includes) their awaiter.

If you're using native threads, you can walk the stack to collect a stack trace like this in various platform-specific ways. There are libraries like Boost's stacktrace, libunwind, etc. These typically use native debug info to map return addresses to source positions, so C++ #line directives may be useful here, though this may depend on your C++ compiler. The main advantage of this approach is that you have just one "source of truth" about the current stack trace- the stack itself.

You can also maintain a data structure like your global callStackDebug, as long as you keep a separate copy for each thread/task. This adds some additional pervasive overhead to call/return and try/catch, but it is also doable when targeting C++, as long as you can generate the appropriate fixup logic for each of those events. For instance, your try/catch example would need to truncate the debug stack in the catch block back to what it was on entry to the try block.

$\endgroup$
2
  • $\begingroup$ I thought that #line directive was only supported in C, interesting! I never used much C/C++, so I played with #line now and am wondering why is there no full call stack? Is it possible to get the full call stack when creating an exception? sololearn.com/compiler-playground/cC3U2cqWAbIS $\endgroup$
    – Hydroper
    Jun 28, 2023 at 17:35
  • 2
    $\begingroup$ I don't believe assert typically includes a call stack in C or C++- the standard only requires it to include the current source position. Exceptions don't typically include the call stack either; you'll need to collect it yourself using something like libunwind or C++23 <stacktrace>. $\endgroup$
    – rpjohnst
    Jun 28, 2023 at 17:42

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .