MFC .DLL TUTORIAL, PART 2

Environment: Visual C++

As I discussed in the last article, .DLLs are a useful tool for any MFC
programmer. They are subject to a number of important limitations,
however, and anyone who is making .DLLs should be aware of these.

MFC Issues

This was discussed in the last article, but is worth mentioning again
briefly. An MFC extension .DLL can only be used if the client application
dynamically links to the same version of MFC, and the correct MFC code
library .DLL is available on the client computer. A regular .DLL which
dynamically links to MFC will only work if the correct MFC code library
.DLL is available.

Compiler Incompatibility Issues

One of the biggest problems with C++ based .DLLs arises when a .DLL is
built on one brand of compiler and called by an application built on
another brand of compiler. Often it won’t work without a great deal of
effort.

ANSI sets the standards for the C and C++ languages. That is, it
specifies the C and C++ functions and data types which should be
supported by a compiler. It does not, however, provide a complete
standard as to how these functions and data types should be implemented
on a binary level. As a result, compiler vendors are free to implement
language features in their own proprietary ways.

Most C / C++ programmers know that different compilers handle
data types differently. One compiler will allocate 2 bytes for an int
and another will allocate 4 bytes. One will use 4 byte doubles and another
will use 8 bytes. An even bigger difference with C++ compilers arises
from their implementation of function and operator overloading.
The differences between compilers go far beyond this, however. The same C or
C++ code may be compiled very differently by different compilers. These
differences may keep your .DLL from running with someone else’s application.

Of course, if you are building an MFC extension .DLL, this is not an
issue for you. MFC extension .DLLs are made with a Microsoft compiler.
As discussed in the previous article, they can only be used by applications
that are dynamically linked to MFC. These applications are also made with
a Microsoft compiler.

Compiler incompatibility problems can be fixed by
inserting pragmas and other precompile instructions into your code, but this is
hard to do and unreliable. There will always be the chance that someone
else is using a compiler that’s still incompatible.

Recompiling

Let’s say you built a .DLL that exports a class called CMyClass.
You provide a copy of the header file for CMyClass to be used by the
client application. Suppose that a CMyClass object is 30 bytes in size.

Now let’s suppose you modify the .DLL to change CMyClass. It still has
the same public functions and member variables, but now CMyClass has an
additional private member variable, an int. So now, when you create an
object of type CMyClass, it’s 34 bytes in size. You send this new .DLL
to your users and tell them to replace the old .DLL. Now you have a
problem. The client application is expecting a 30 byte object, but your
new .DLL is creating a 34 byte object. The client application is going to
get an error.

Here’s a similar problem. Suppose that instead of exporting CMyClass
your .DLL exports several functions which use CMyClass references or
pointers in their parameters or return values. You have provided a
CMyClass header file which now resides in the client application.
Again, if you change the size of the CMyClass object without rebuilding
the client application, you will have problems.

At this point, the only way to fix the problem is to replace
the CMyClass header file in the client application and recompile
it. Once recompiled, the client application will start looking
for a 34 byte object.

This is a serious problem. One of the goals of having a .DLL is
to be able to modify and replace the .DLL without modifying the
client application. However, if your .DLL is exporting classes or
class objects, this may not be possible. You may have to recompile
the client application. If you don’t have the source code for the
client application, you simply can’t use the new .DLL.

Solutions

If there were a perfect fix for these problems, we might not have COM.
Here are a few suggestions:

MFC extension .DLLs don’t have compiler incompatibility problems.
They simply can’t be used by applications built on non-Microsoft
compilers. As for regular .DLLs, you can avoid many compiler problems
by only exporting C-style functions and using the extern “C” specifier.
Exporting C++ classes and overloaded C++ functions leaves you much more
vulnerable to compiler incompatibility.

As for having to recompile your client application when you modify
the .DLL, there are relatively easy ways to avoid the problem. I will
describe two of them: (1) an interface class and (2) static functions
used to create and destroy your exported class.

Using an Interface Class

The goal of an interface class is to separate the class you want to export
and the interface to that class. The way to do this is to create
a second class which will serve as the interface for the class you want
to export. Then, even when the export class changes, you will not need
to recompile the client application, because the interface class remains
the same.

Here’s an example of how that would work. Suppose you want to export
CMyClass. CMyClass has two public functions, int FunctionA(int) and int
FunctionB(int). If I simply export CMyClass, I’ll have to recompile the
client application every time I add a new variable. Instead,
I’ll create and export an interface class, CMyInterface. CMyInterface will
have a pointer to a CMyClass object. Here’s the header file for CMyInterface
as it looks inside the .DLL:



#include “MyClass.h”

class __declspec(dllexport) CMyInterface
{
//private pointer to CMyClass object
CMyClass *m_pMyClass;

CMyInterface( );
~CMyInterface( );

public:
int FunctionA(int);
int FunctionB(int);
};


Inside the client application, the header file will look slightly
different. The #include will be gone. After all, you can’t include
MyClass.h, because the client application doesn’t have a copy of it.
Instead, you will use a forward declaration of CMyClass. This will
allow you to compile even without the CMyClass header file:



class __declspec(dllimport) CMyInterface
{
//Forward declaration of CMyClass
class CMyClass;

CMyClass *m_pMyClass;

CMyInterface( );
~CMyInterface( );

public:
int FunctionA(int);
int FunctionB(int);
};


Inside the .DLL, you implement CMyInterface as follows:



CMyInterface::CMyInterface( )
{
m_pMyClass = new CMyClass;
}

CMyInterface::~CMyInterface( )
{
delete m_pMyClass;
}

int CMyInterface::FunctionA( )
{
return m_pMyClass->FunctionA( );
}

int CMyInterface::FunctionB( )
{
return m_pMyClass->FunctionB( );
}


Thus, for every public function in CMyClass, CMyInterface will provide
its own corresponding function. The client application has no contact
with CMyClass. If it wants to call CMyClass::FunctionA, it instead calls
CMyInterface::FunctionA. The interface class then uses its pointer
to call CMyClass. With this arrangement, you’re free to modify with
CMyClass. It doesn’t matter if the size of the CMyClass object changes.
The size of the CMyInterface object will remain the same. If you add a
private member variable to CMyClass, the size of CMyInterface will remain
unchanged. If you add a public member variable to CMyClass, you can add
“getter” and “setter” functions for the new variable in CMyInterface
without fear. Adding new functions to CMyInterface will not cause
recompile problems.

Creating a separate interface class avoids some compiler incompatibility
problems and most recompile problems. As long as the interface class
doesn’t change, there should be no need to recompile. There are still
two relatively minor problems with this solution. First, for every public
function and member variable in CMyClass, you must create a corresponding
function or variable in CMyInterface. In the example there are only two
functions, so that’s easy. If CMyClass had hundreds of functions and
variables, this would be a more tedious, error-prone process.
Second, you are increasing the amount of processing that must be done.
The client application no longer calls CMyClass directly. Instead, it
calls a CMyInterface function which calls CMyClass. If this is a function
that will be called thousands of times by the client application, the
extra processing time may begin to add up.

Static Functions

A different way to avoid having to recompile uses static functions to
create and destroy the exported class. This solution was sent to me by
Ran Wainstain in a comment to my previous article.

When you are creating a class which you intend to export, you add two
public, static functions, CreateMe( ) and DestroyMe( ):



class __declspec(dllexport) CMyClass
{
CMyClass( );
~CMyClass( );

public:
static CMyClass* CreateMe( );
static void DestroyMe(CMyClass *ptr);
}


CreateMe( ) and DestroyMe( ) are implemented as follows:



CMyClass* CMyClass::CreateMe( )
{
return new CMyClass;
}

void CMyClass::DestroyMe(CMyClass *ptr)
{
delete ptr;
}


You export CMyClass as you would any other class. In the client
application, you must be sure to use the CreateMe( ) and DestroyMe( )
functions. When you want to create a CMyClass object, you don’t
declare it in the usual fashion, i.e.:



CMyClass x;

Instead, you do this:



CMyClass *ptr = CMyClass::CreateMe( );

When you are done, you must remember to delete the object:



CMyClass::DeleteMe(ptr);

Using this technique, you can modify the size of CMyClass without
having to recompile the client application.

Conclusion

This article is not a complete review of every issue concerning .DLLs, nor
does it cover every possible solution. Good discussions of these issues
can be found in Inside COM by Dale Rogerson and Essential COM
by Don Box. For a more detailed understanding of the issue, that’s where
I would go. This article should at least serve to make you aware of the
biggest issues and possible fixes and get you started on your way.

More by Author

Must Read