Catching C++ exceptions in D with LDC/Calypso

By bridging the LDC compiler with Clang, Calypso has enabled wider interoperability with C++ and much quicker too, as no binding is required.

Recently it acquired the ability to catch any C++ exception, as illustrated by the examples in https://github.com/Syniurge/Calypso/tree/master/tests/calypso/eh

If our C++ exception class example is the following:

// C++ code
class ooops : public std::exception
{
public:
    const char* what() const noexcept { return "Ooops!"; }
};

void throwOoops() { throw ooops(); }

And here’s our try-catch block mixing D and C++ catches:

// D code
try {
    writeln("Throwing an ooops exception");
    throwOoops();
} catch (Throwable t) {
    writeln("Nope, catched a Throwable");
} catch (C++) (uint n) {
    writeln("Wrong catch, this is for C++ uint exceptions");
} catch (C++) (ref exception e) {
    writefln("Catching the std::exception, e.what() == %s", to!string(e.what()));
}

Then the thrown ooops class object inheriting from std::exception gets caught by the last catch block:

    Throwing an ooops exception
    Catching the std::exception, e.what() == Ooops!

And the ooops object gets destroyed at the end of the block.

Note that (for good reasons) in Calypso C++ classes are values, not references, hence the need of ref to catch the thrown ooops class value by reference.

Backstage tour

Since there’s ongoing work to implement catching C++ exceptions in the reference D compiler, I will detail how it’s implemented in Calypso. The starting point was this great series of articles:

https://monoinfinito.wordpress.com/series/exception-handling-in-c/

This is a must-read if you ever need an in-depth look into libunwind and how C++ exception handling is implemented. Concise and densely informative on a poorly documented topic, it demystified C++ exceptions enough to get me started.

Both GDC and LDC implement DWARF exception handling on Unix-like targets. When a D or C++ exception is thrown, libunwind climbs up the stack and calls the personality routines associated with each try-catch block, asking the routines whether or not the catch blocks may handle the thrown exception. As a matter of fact vanilla LDC already sees thrown C++ exceptions, but when it does its personality function simply terminates the program:

 //TODO: Treat foreign exceptions with more respect
 if ((cast(char*)&exception_class)[0..8] != _d_exception_class)
     return _Unwind_Reason_Code.FATAL_PHASE1_ERROR;

So what does handling C++ exceptions involve?

Unlike D which only allows classes derived from Throwable to be thrown, in C++ any type is susceptible to get thrown, and the corresponding std::type_info value is set in the exception header by __cxa_throw(). This type_info is then matched against the type_info values from catch clauses by __gxx_personality_v0, the personality routine called by libunwind.

So it means dealing with std::type_info, and at least as far as the GNU libstdc++ is concerned, determining if a given type_info from a catch may handles an exception’s type_info is done by calling its virtual function type_info::__do_catch(), which also adjusts base class pointers for us. Not a big deal for vanilla D compilers, able to bind to C++ classes and call virtual functions as long as there’s no multiple inheritance involved.

But there is one issue that was brought up while discussing C++ exceptions on the D forums: the std::type_info value for a given type has to be generated by the compiler for its address to be put inside the exception table. Although probably manageable to implement from scratch, Calypso just cheats by summoning Clang:

  // Convert the DMD type to a Clang type, something Calypso had already been doing for template instantiations
  auto CatchType = TypeMapper().toType(loc, t, irs->func()->decl->scope);
  // Get our std::type_info global variable from Clang, which handles everything
  auto TypeInfo = CGM->GetAddrOfRTTIDescriptor(CatchType, /*ForEH=*/true);

And that’s it to get our catch clause, or almost.

As far as I could gather LLVM allows almost no LSDA customization (most likely for good reasons). As catch clauses LLVM only takes global variable pointers, and it’s impossible to stuff more information into the exception tables unless we’re ready to tweak LLVM’s own code.

Hence if we were simply putting a std::type_info pointer next to TypeInfo_Class pointers in the catch clause table (the action table), there would be no way to differentiate the two. Calypso wraps the type_info pointer inside a D class, so cast() may be used to keep in sight which is which:

class __cpp_type_info_ptr
{
    type_info *p;
}

The rest is straightforward. When the modified LDC personality function encounters the C++ exception class, it queries registered foreign exception handlers, looking for one able to handle that class and if one does, it lets the handler determine the right catch for the exception:

interface ForeignHandler
{
    // address points to the catch clause in the exception action table, returns true if the catch does handle the thrown exception
    bool doCatch(void* address, ubyte encoding);
    
    // returns the value passed to the catch entry function, __cxa_begin_catch for C++
    void *getException();
}

interface ForeignHandlerFactory
{
    bool doHandleExceptionClass(ulong exception_class) shared;
    ForeignHandler create(_Unwind_Context_Ptr context, _Unwind_Exception* exception_info) shared;
}

// LDC's personality routine context
struct NativeContext
{
    ForeignHandler foreign = null;
    ...

ForeignHandler implementation for C++ exceptions: https://github.com/Syniurge/druntime/blob/release-0.16.1/src/ldc/eh/cpp/gnu.d

We’re almost there now. The last ingredient for C++ exception handling in D is that we need to call __cxa_begin_catch() and __cxa_end_catch() in C++ catches instead of _d_eh_enter_catch(), as they update some info in the exception header and more importantly __cxa_end_catch destroys the exception if caught and not rethrown. For this reason and because every type is throwable in C++, special catch (C++) blocks were added, which during code generation are setup exactly like catch blocks are in C++, since once again Clang does it for us:

// RAII object to capture the 'scope(exit)' IR, e.g to generate the __cxa_end_catch call on destruction
clang::CodeGenFunction::RunCleanupsScope Cleanup(CGF);

// Generates the IR for catchVar = __cxa_begin_catch(ehPtr)
clang::CodeGen::InitCatchParam(CGF, ehPtr, CatchParamTy, ..., catchVar, ...);

And that’s it for catching C++ exceptions in D on many platforms!
Once we’re able to catch them in the DWARF exception model+GNU libstdc++, full C++ exception handling isn’t too far away. What’s essentially left to implement in Calypso:

  • rethrowing
  • libc++ doesn’t offer __do_catch(), an alternative is needed
  • MSVC ABI support (Structured Exception Handling?)
Advertisements