Interfacing with C++

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.

Getting ready

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++.

How to do it …

Let's interface D with C++ by executing the following steps:

  1. Use C++ functions by marking them 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.
  2. Use interfaces marked 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.
  3. If you implement a C++ object in D, inherit it from the interface and mark each method 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.
  4. Pass object references between the languages and access the methods. In addition, create factory and destruction functions that deal with the interface. Manage all memory that is stored across language boundaries manually. Failure to do this may cause an object to get accidentally garbage collected while being used by the C++ code.

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.

How to do it…

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.

Note

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.

See also

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.160.181