First up, here’s a big disclaimer if the title didn’t warn you enough: this is a hack. It’s just a proof-of-concept
for getting extern(C++)
classes working with betterC D. Also,
DMD keeps getting better quickly, so if you’re reading this post when something more recent than version 2.080 is out,
this hack is probably obsolete. Hopefully you’ll find this post interesting anyway if you’re either
- Interested in all things
-betterC
, or - Curious about how stuff like classes work in D.
If you haven’t read my earlier post about how polymorphism and inheritance work yet, I recommend doing that first.
Recap on Classes in D
In C++, there’s no essential difference between a struct
and a class
— they’re both like C structs with extra
features. In particular, they support polymorphic inheritance, which requires hidden vtables. In D, a struct
is like a plain, non-inheritable C struct, while a class
is a more powerful thing that inherits from a base Object
class. The Object
class implements lots of convenient functionality like hash
function support. In short, D structs are lower-level than C++ structs/classes, but D classes are higher-level.
It’s actually possible to get C++-style classes in D by marking the declaration with extern(C++)
. An extern(C++)
class supports inheritance, but it doesn’t automatically
inherit from D’s Object
.
The Problem and Plan of Attack
The Object
class is defined in the D runtime, so regular D
classes don’t work in -betterC
. In theory, extern(C++)
classes should work in -betterC
, but they don’t with the current version of DMD (version 2.080)
because extern(C++)
classes still rely on runtime type
information (RTTI). RTTI is implemented using the TypeInfo
D
class, which creates a dependency on Object
. Although this is
only a link-time dependency, DMD barfs if it tries to compile a class definition with -betterC
. (This is a good thing because compiler errors are much
friendlier than linker errors, but it’s relevant to the hack.)
I did some experimenting, and the only real dependency on RTTI that I found was in class instance construction. Specifically, when constructing a new class instance, D initialises the memory used by the instance before calling the user-written constructor. This initialisation populates things like vtable pointers and default values for members (as defined in the class definition). It actually works by just blitting from a sample that’s stored in RTTI.
There’s one other little problem: destructors in D aren’t inherently virtual or recursive (in normal D code, the runtime emulates virtualness and recursiveness using RTTI). Making our own virtual and recursive destructors is easy (but a minor nuisance). You could use a template mixin to reduce the boilerplate, but I just wrote out the definitions explicitly for this example.
Here’s a high-level view of the workaround:
- Avoid the compiler error by putting the class definitions and related stuff into a separate file that’s compiled
without
-betterC
. - Reimplement class construction and destruction without RTTI.
- Do linker hacking to make up for the loss of
-betterC
. - Link with code that’s compiled normally with
-betterC
.
The Class Code
Here’s the code for the classes. It’s a silly example that demonstrates
- A base class and a class that derives from it
- Constructors/destructors with arguments and side effects
- Virtual and non-virtual methods
- Member variables
Next there’s some templated factory code that does the construction, with a corresponding templated convenience
function for destruction. This code is @nogc
, so memory is
managed using standard C malloc()
and free()
.
The tricky part is correctly initialising the raw memory before calling the constructor. D’s ABI for class layout is specced out, but I dodged the issue
completely with a little hack. For the purposes of this PoC, I made each class constructable with a side-effect-free
default constructor. The factory function contains a static
immutable
instance of the class. The raw memory of this “model” instance gets blitted into the newly allocated
instance memory, and we’ll know that we’ll have all the right vtable pointers, etc, in the right place. After that, we
can safely call the normal class constructor on the new instance. Thanks to D’s CTFE, the model instance gets
constructed at compile time, so any runtime library dependencies can be ripped out before linking with -betterC
code. Effectively, we temporarily borrow the standard class
construction code during compilation.
By the way, C++ classes have value semantics, but extern(C++)
classes in D still have reference semantics, and a class
instance reference is identical to a pointer at the binary level.
There were two things I had to do to get this code to link with -betterC
code. First, DMD inserted a reference to a TypeInfo
vtable from the runtime in an inconvenient place in the compiled
code. Disassembly showed it wasn’t truly needed, so I just made the linker happy by inventing a dummy
implementation:
Other D runtime references could be ripped out like it’s 2016 again:
The User Code
Here’s the code that uses the classes. Again, it’s just a silly example that uses a bunch of class functionality.
However, this time it’s hackless code that compiles cleanly with -betterC
.
And it works!