If you have an existing C++ program and want to start using D, it probably isn't practical to rewrite the entire application. However, it may be possible to start writing new components of the application in D. Let's look at how this can be done.
Review how D interfaces with C. Any extern
functions of C work exactly the same way in C++. You'll also need to get the appropriate C++ compiler. On 32-bit Windows, you'll need the Digital Mars C compiler to pair with DMD. On 64-bit Windows, the Microsoft Visual C++ compiler will work. On Linux, use g++.
Let's interface D with C++ by executing the following steps:
extern(C++)
; otherwise, use them in the same way as you use C functions. You can also write a D function with the extern(C++)
linkage and use it from C++ by writing the prototype.extern(C++)
to access objects or to implement objects. Any virtual function in the C++ class should have a corresponding method in the D interface, appearing in exactly the same order. If possible, use pure virtual functions in an abstract class in C++; this will exactly match the D interface. If not, you can make it work with a careful listing of all virtual functions.extern(C++)
. If you had to define unusable entries to make the virtual table match (for example, a virtual destructor), implement these as do-nothing methods.The following is some example code. First, we'll look at the C++ code. We won't use header files for brevity:
#include<stdio.h> #include<stdlib.h> // our C++ class class Animal { public: // ideally, we would prefer to use classes // with no member variables and no additional // functions. D and C++ don't understand each // other's constructors and destructors and will // have different ideas as to where members will be found. // But, perfection may not be realistic with // existing code, so we'll see how to possibly work with it. int member; // we must not use this on a D object *at all* virtual ~Animal() {} // this is what we really want: abstract virtual // functions we can implement with an interface. virtual void speak() = 0; }; // A concrete C++ class we'll use in D via the interface class Dog : public Animal { void speak() { printf("Woof "); } }; // Our D functions extern "C++" void useAnimal(Animal* t); extern "C++" Animal* getCat(); extern "C++" void freeCat(Animal* cat); // D Runtime functions from the library extern "C" int rt_init(); extern "C" void rt_term(); // RAII struct for D runtime initialization and termination struct DRuntime { DRuntime() { if(!rt_init()) { // you could also use an exception fprintf(stderr, "D Initialization failed"); exit(1); } } ~DRuntime() { rt_term(); } }; void main() { // be sure to initialize the D runtime before using it DRuntime druntime; Dog dog; // use a C++ class in a D function useAnimal(&dog); // use a D class from C++ // you may use a smart pointer or RAII struct here too // so the resource is managed automatically. Animal* cat = getCat(); // use a factory function cat->speak(); // call the function! // it was created in D, so it needs to be destroyed by D too freeCat(cat); }
Now, let's have a look at the code at the D side, which is as follows:
import std.stdio; import core.stdc.stdlib; // for malloc extern(C++) interface Animal { // since the C++ class had a virtual destructor // we must define an entry, but we don't want to use it void _destructorDoNotUse(); void speak(); } class Cat : Animal { // Note that we did *not* copy the C++ member variable. // Doing so would be futile; the layouts will not match. // Creating D child classes of a C++ class with member // variables should be avoided if at all possible. extern(C++) void _destructorDoNotUse() {} extern(C++) void speak() { try {writeln("Meow!");}catch(Throwable) {} } } // We'll implement a factory function for getting Cats extern(C++) Animal getCat() { // Manage the memory with malloc and free for full control import std.conv; enum size = __traits(classInstanceSize, Cat); auto memory = malloc(size)[0 .. size]; return emplace!Cat(memory); } extern(C++) void freeCat(Animal animal) { auto cat = cast(Cat) animal; if(cat !is null) { destroy(cat); free(cast(void*) cat); } } // This can also use an object from C++ extern(C++) void useAnimal(Animal t) { t.speak(); }
Compile and run the program separately, just like with C, remembering to link it in Phobos. It will print the result of the Dog
and Cat
objects speaking to each other.
Always create and destroy objects using methods from the same language where it was created, and always use them through pointers (in C++) or interfaces (in D). Remember to keep a reference to D objects somewhere in D, or manage the memory manually so that the garbage collector doesn't reap it prematurely.
To create and destroy the D class instances with malloc
, we must not use the built-in new operator. Instead, we will allocate memory and initialize the object separately. We learn the number of bytes needed by using __traits(classInstanceSize, ClassName)
. Then, we allocate the memory to the block using malloc
and immediately slice it to the appropriate size. This lets D functions know the size of the block (which they would not know with a pointer).
Finally, the std.conv.emplace
function constructs the given argument in the specified memory buffer, returning the reference to it. After the memory
block argument, emplace
will also take constructor arguments for the class, if any.
Complementing destruction is the free
function. Similar to construction, this is done in two steps: first destroying the object, then freeing the memory.
Memory management can be improved by also using other C++ techniques such as a smart pointer, and/or the D functions could provide reference counting. Use C++ practices consistent with the rest of your project for best integration.
The preceding example neglected a C++ member variable. If it's possible, do not implement child classes of a C++ class with members in D. You may use a C++ class with members, but inheriting from it is potentially risky because if the member is used by any C++ function on the D object, it may corrupt your memory. It is strongly recommended to refactor the C++ code to use abstract interface-style classes and inherit from them in D, instead of trying to match it.
All the rules that apply to the writing part of a C program in D also apply here, with the biggest difference being that you can use some C++ or D classes. In particular, despite C++ supporting exceptions, you should not throw exceptions across language boundaries, since the two language's exception models may not be compatible. This is why our speak
method swallows any exception thrown by writeln
.
Keeping two sets of definitions in sync is bug-prone work. The dtoh
and dstep
tools that work with the C code also work with C++; try to use them when you can.
18.119.160.181