Skip to content

DLLBehaviour

djewsbury edited this page Feb 13, 2015 · 1 revision

#DLL Behaviour

How to deal with dynamic linking? I'm seeing a few good reasons for using DLLs for targeted purposes. But to get DLLs working correctly, we need to decide on solutions to a few problems!

Why use DLLs at all? There are a few immediate practical reasons why they would be useful:

  • OpenCollada linking takes too long in Profile/Release (because of insane code generation tasks)
  • it must be put into it's own dll, otherwise very code change will result in huge link times
  • File format plugins and other optional plugins
  • it's convenient that certain optional behaviour be treated with a "plugin" type of model. This makes it a lot easier to mix in some closed source behaviour (eg for proprietary file formats)

There are some other goals as well:

  • mix old and new code
  • eg, if the "BufferUploads" part is not working in today's build, but we need some new feature in the "Render" part, then why not just drop back to yesterday's BufferUploads
  • this is also useful for testing, because it allows us to isolate problems
  • DLLs can also add options for cross-language compatibility.
  • Since the DLL linking and function calling method is quite flexible, it provides a convenient way to hook into higher level languages and scripting languages (eg, another way to link lua to C). But, that said, the C++/CLI layer solves the same problem more elegantly.

##Problems to solve

###Maintaining DLL interface compatibility

There are 2 common patterns for the DLL interface. We can use the virtual table pointer-to-method behavior to abstract us from the DLL details:

class IObject
{
public:
    virtual void SomeMethod() const = 0;
    virtual ~IObject();
};
dll_export std::unique_ptr<IObject> CreateObject();

Here, IObject and derived classes don't need to know anything about DLLs. This class should work equally well (and identically) as a statically linked class, or a DLL linked class. This is pattern is sometimes used even if there is only a single implementation of IObject (think about the way DirectX and other COM libraries work).

Another option is to mark the class or class members as dll_import:

class dll_import Object
{
public:
    Object();
    ~Object();
    void SomeMethod() const;
};

In the top example, if we looked at table of symbols that are dynamically linked, we would see only "CreateObject()". The function address of the implementations of SomeMethod() are stored in a virtual table, not in the dynamic linking table.

But if we looked the dynamic linking table for the section object, we would see all of the methods of Object: Object::Object, Object::~Object and Object::SomeMethod.

Let's call the executable that loads the DLL the "host module."

In both cases, if the host module attempts to call a method of Object, we should get a call-via-function-pointer. In the top example, it's using a virtual table. In the second it's using the dynamic linking table. But it should end up executing similar instruction.

But what happens if we use Object from within the DLL itself? In the second case, we should always get a normal function call. But in the first case, the we will frequently get a call-via-function-pointer (because the methods are all virtual). There are some cases where we might get a normal function call, but those should be very rare.

But there's a more important difference between the two.

####Interface robustness

One of the useful aspects to DLLs is the ability to mix and match different versions. Sometimes we want to use today's version of library A, but yesterday's version of library B. Or maybe last week's library B. Or even last month's library B. This type of thing can be really useful for tracking down problems and other typical daily tasks.

However, to achieve this, we need to try to avoid things that invalidate the DLL interface. We can only mix and match if the interface remains compatible.

In the top example, the COM style interface is not very robust. Both the DLL and the host module must agree on the layout of the virtual table of IObject exactly. If there any differences at all, the two will no longer be compatible. This happens frequently when adding a method, or removing a method (or even just reordering the virtual table). Even if those methods aren't used at all, when they change the virtual table, they break DLL compatibility.

Worse still, the operating system doesn't recognise that the DLL has become incompatible. Nothing will verify that both the DLL and the host module agree about the virtual table. This means that DLL incompatibility problems don't result in a clear error message -- they just cause a crash.

However, the second example is more robust. There is a separate DLL linking symbol for each method. DLL linking will succeed as long as all of the required symbols can be found. So if new methods are added to the DLL -- it should still work. Or if methods that we don't call are removed; it should also continue to work. And if there is a compatibility problem, we can get a concise error message.

That suggests that the second method should be much better for maintaining a compatible interface over a long term.

###Dealing with globals

Each DLL has it's own copy of all globals and class/function-scope statics. However, frequently we want both the host module and the DLL to use the same value of certain globals. For example, the Assets::CompileAndAsyncManager instance is truly global, and should ideally be shared by everyone. If we create this object in the host module, we must have some way to initialise the global instance pointer for all DLLs. We have to manually get the value from the host module and apply it to the DLL module. And this has to be done early in the start-up process, before any code needs to use those globals!

It's a confusing and frustrating problem! Not every programmer recognises this problem, and it can be particularly confusing when the watch window in the debugger shows an unexpected instance of a duplicated global.

It can be worst when using foreign libraries. If a foreign library relies on globals, it must explicitly have a solution for this issue. Otherwise, it can be very difficult to use in a multi-DLL environment.

I'm not sure if this problem can be solved cleanly. It might necessitate an awkward and hacky solution.

###Dealing with heap problems

###DLL unload

###Platform independence

This page has been all about DLLs on Windows. Not all platforms have access to similar technology, but some do. Sometimes the way it works is radically different between platforms, however. This is the hardest type of platform independence, because ideally we want to be forward-compatible -- we want to be able to support dynamic linking models that exist now, and we also want to make it possible to support some future model for some unknown platform (like a desktop version of Android).