Tags C & C++

Because of ABI incompatibilities between compilers and even different versions of the same compiler, exporting C++ classes from DLLs is a tricky business. Luckily, with some care it is possible to do this safely, by employing abstract interfaces.

In this post I will show a code sample of a DLL and an application using it. The DLL exports a class by means of a factory function that creates new objects that adhere to a known abstract interface. The main application loads this DLL explicitly (with LoadLibrary) and uses the objects created by it. The code shown here is Windows-specific, but the same method should work for Linux and other platforms. Also, the same export technique will work for implicit DLL loading as well.

First, we define an abstract interface (by means of a class with pure virtual methods, and no data), in a file named generic_interface.h:

class IKlass {
public:
    virtual void destroy() = 0;
    virtual int do_stuff(int param) = 0;
    virtual void do_something_else(double f) = 0;
};

Note that this interface has an explicit destroy method, for reasons I will explain later. Now, the DLL code, contained in a single C++ file:

#include "generic_interface.h"
#include <iostream>
#include <windows.h>

using namespace std;


class MyKlass : public IKlass {
public:
    MyKlass()
        : m_data(0)
    {
        cerr << "MyKlass constructor\n";
    }

    ~MyKlass()
    {
        cerr << "MyKlass destructor\n";
    }

    void destroy()
    {
        delete this;
    }

    int do_stuff(int param)
    {
        m_data += param;
        return m_data;
    }

    void do_something_else(double f)
    {
        int intpart = static_cast<int>(f);
        m_data += intpart;
    }
private:
    int m_data;
};

extern "C" __declspec(dllexport) IKlass* __cdecl create_klass()
{
    return new MyKlass;
}

There are two interesting entities here:

  1. MyKlass - a simplistic implementation of the IKlass interface.
  2. A factory function for creating new instances of MyKlass.

And here is a simple application (also contained in a single C++ file) that uses this library by loading the DLL explicitly, creating a new object and doing some work with it:

#include "generic_interface.h"
#include <iostream>
#include <windows.h>

using namespace std;

// A factory of IKlass-implementing objects looks thus
typedef IKlass* (__cdecl *iklass_factory)();


int main()
{
    // Load the DLL
    HINSTANCE dll_handle = ::LoadLibrary(TEXT("mylib.dll"));
    if (!dll_handle) {
        cerr << "Unable to load DLL!\n";
        return 1;
    }

    // Get the function from the DLL
    iklass_factory factory_func = reinterpret_cast<iklass_factory>(
        ::GetProcAddress(dll_handle, "create_klass"));
    if (!factory_func) {
        cerr << "Unable to load create_klass from DLL!\n";
        ::FreeLibrary(dll_handle);
        return 1;
    }

    // Ask the factory for a new object implementing the IKlass
    // interface
    IKlass* instance = factory_func();

    // Play with the object
    int t = instance->do_stuff(5);
    cout << "t = " << t << endl;
    instance->do_something_else(100.3);
    int t2 = instance->do_stuff(0);
    cout << "t2 = " << t2 << endl;

    // Destroy it explicitly
    instance->destroy();
    ::FreeLibrary(dll_handle);

    return 0;
}

Alright, I raced through the code, but there are a lot of interesting details hiding in it. Let's go through them one by one.

Clean separation

There are other methods of exporting C++ classes from DLLs (here's one good discussion of the subject). The one presented here is the cleanest - the least amount of information is shared between the DLL and the application using it - just the generic interface header defining IKlass and an implicit agreement about the signature of the factory function.

The actual MyKlass can now use whatever it wants to implement its functionality, without exposing any additional details to the application.

Additionally, this code can easily serve as a basis for an even more generic plugin architecture. DLL files can be auto-discoverable from a known location, and a known function can be exposed from each that defines the exported factories.

Memory management

Memory management between DLLs can be a real pain, especially if each DLL links the MSVC C runtime statically (which tends to be common on Windows). Memory allocated in one DLL must not be released in another in such cases.

The solution presented here neatly overcomes this issue by leaving all memory management to the DLL. This is done by providing an explicit destroy function in the interface, that must be called when the object is no longer needed. Naturally, the application can wrap these objects by a smart pointer of some kind to implement RAII.

Note that destroy is implemented with delete this. This may raise an eyebrow or two, but it's actually valid C++ that occasionally makes sense if used judiciously.

It's time for a pop quiz: why doesn't IKlass need a virtual destructor?

Name mangling and calling convention

You've surely noticed that the signature of create_klass is rather intricate:

extern "C" __declspec(dllexport) IKlass* __cdecl create_klass()

Let's see what each part means, in order:

  • extern "C" - tells the C++ compiler that the linker should use the C calling convention and name mangling for this function. The name itself is exported from the DLL unmangled (create_klass)
  • __declspec(dllexport) - tells the linker to export the create_klass symbol from the DLL. Alternatively, the name create_klass can be placed in a .def file given to the linker.
  • __cdecl - repeats that the C calling convention is to be used. It's not strictly necessary here, but I include it for completeness (in the typedef for iklass_factory in the application code as well).

There is a variation on this theme, which I'll mention because it's a common problem people run into.

One can declare the function with the __stdcall calling convention instead of __cdecl. What this will do is cause GetProcAddress to not find the function in the DLL. A peek inside the DLL (with dumpbin /exports or another tool) reveals why - __stdcall causes the name to be mangled to something like _create_klass@0. To overcome this, either place the plain name create_klass in the exports section of the linker .def file, or use the full, mangled name in GetProcAddress. The latter might be required if you don't actually control the source code for the DLL.


Comments

comments powered by Disqus