So static linking is evil and dynamic linking solves our
deployment problems! Well, it's not quite that easy.
| Problem 1 : C++ method name mangling
|
First of all, in order to support function overloading, compilers
"mangle" the human-readable names of class methods into a format
that encodes parameter types. This "mangling" is done order to make
the names unique for the purposes of the linker. Unfortunately,
different C++ compilers mangle names in different ways. This means
if we try and use our DLL-based C++ class from an executable client
that was compiled and linked with a different C++ compiler, we have
a problem. DLL references won't fix-up because of the naming
mismatch. If loadtime dynamic linking is used, the executable client
won't link against the DLL import library at build time, because it
contains alternatively mangled import records. If runtime dynamic
linking is used, the executable client won't be able to find the DLL
entry points it expects at run-time because they are exported with
alternatively mangled names.
Look at the mangled names exported by the Microsoft Visual C++
5.0 compiler.
??0CMyString@@QAE@PBD@Z
//CMyString::CMyString(char
const *) ??1CMyString@@QAE@XZ
//CMyString::~CMyString(void) ?Find@CMyString@@QAEPBDPBD@Z
//CMyString::Find(char const *) ?Length@CMyString@@QAEHXZ
//CMyString::Length(void)
Now look at the mangled names expected by the Borland C++ 4.02
compiler.
@CMyString@$bctr$qpxc
//CMyString::CMyString(char const *) @CMyString@$bdtr$qv
//CMyString::~CMyString(void) @CMyString@Find$qpxc
//CMyString::Find(char const *) @CMyString@Length$qv
//CMyString::Length(void)
Oh dear, things were going so well! Is there any solution to this
problem?
| Solutions to Problem 1 : C++ method name
mangling |
We could try a technique of aliasing the class method names.
Aliasing with names
This involves aliasing the class method names exported by the DLL
to be the same as those mangled names expected by the executable
client. Because there is no way of aliasing method names as part of
the class description, to do this we must amend (or create) a module
definition file that describes the exporting DLL and the C++ class
it is exporting. Here's how we do it.
The module definition file for the DLL, mystring.def:
LIBRARY
MYSTRING EXPORTS
@CMyString@$bctr$qpxc=??0CMyString@@QAE@PBD@Z
@CMyString@$bdtr$qv=??1CMyString@@QAE@XZ
@CMyString@Find$qpxc=?Find@CMyString@@QAEPBDPBD@Z
@CMyString@Length$qv=?Length@CMyString@@QAEHXZ
On the face of it, we need a separate module definition file,
import library file, and DLL file for each name mangling convention
in use in today's C++ compilers. However, it is possible to provide
multiple exported aliases for the same internal DLL routine name, so
we can get away with one module definition file, import library
file, and DLL file to cover all name-mangling possibilities.
Aliasing with ordinal numbers
This involves aliasing the class method names exported by the DLL
to be ordinal numbers and to have the executable client link using
these. Linking against numbers seems to be a much better idea
because it is not prone to the name mangling problem. Again, because
there is no way of aliasing method names as part of the class
description, to do this we must amend (or create) multiple module
definition files that describe the exporting DLL and the C++ class
it is exporting. Here's what we do.
Consider the module definition file for the DLL,
mystring.def.
LIBRARY
MYSTRING EXPORTS ??0CMyString@@QAE@PBD@Z
@1 ??1CMyString@@QAE@XZ
@2 ?Find@CMyString@@QAEPBDPBD@Z
@3 ?Length@CMyString@@QAEHXZ @4
This would yield an import library file that could be linked
against by Visual C++ clients.
Here is another module definition file for the DLL,
myborlandstring.def.
LIBRARY
MYSTRING EXPORTS @CMyString@$bctr$qpxc
@1 @CMyString@$bdtr$qv
@2 @CMyString@Find$qpxc
@3 @CMyString@Length$qv @4
This would yield an import library file that could be linked
against by Borland C++ clients.
Even though we need only one DLL file (all class methods are
exported by ordinal number), we still need a separate module
definition file and import library file for each name mangling
convention in use in popular C++ compilers.
The last two techniques mean that it is very difficult to get
things right. There has to be a better solution.
Aliasing with vtables
There is a third technique that also involves using numbers to
access the DLL-based class methods. We don't export any details of
the implementation class from the DLL, but instead provide a shared
abstract base class definition that the executable client programs
against and the DLL-based class implements. If the client and the
DLL-based class agree on the functions in the vtable (e.g., order of
appearance, return types, parameters, meaning, etc.), the function
names used are irrelevant. This is because all function names used
by the client to access the DLL-based C++ implementation class will
be resolved into offsets into a vtable. As long as the following is
true we should be OK:
- The "hidden" DLL-based C++ implementation class must derive
from, and implement all, the methods of the shared abstract base
class
- The DLL must provide an out-of-band technique for creating an
instance of the DLL-based C++ implementation class (i.e., not a
C++ technique, e.g., not new) and returning a reference to the shared
abstract base class
Hmmm....this sounds interesting! We may explore this further.
So, as long as we can get the name-mangling problem sorted out,
then dynamic linking solves our deployment problems! Well, it's
still not quite that easy.
It turns out that even when we solve the name-mangling problem,
the code crashes when we run a Borland C++ client against a class
exported from a Visual C++ DLL. Even though the two environments
share the same class description, the binary layout of the object in
each environment is different enough to cause us problems at
runtime. For instance, the way that new and delete
work in the client are very much tied into the Borland C++
environment. This means the Visual C++ object implementation is
expected to manipulate memory that was laid out by the Borland C++
client. Oops! Let's look at this problem in more detail.
| Problem 2 : Binary layout of C++
objects |
Binary Encapsulation
Encapsulation gives us the ability to hide data and therefore
gives us much flexibility in our class implementation. C++ provides
us with the syntax to provide encapsulation. Consider the following
change to our class.
Header file, mystring.h:
class EXPORTORIMPORT
CMyString { private: char
*m_psz; int
m_nLength; public: CMyString(const char *
psz); ~CMyString();
const char*Find(const char *psz); int
Length(); };
Source file changes, mystring.cpp:
CMystring::CMyString(const char *
psz) { m_nLength = psz ? strlen(psz) :
0; m_psz = new
char[m_nLength+1]; if
(m_nLength)
strcpy(m_psz,psz);
else m_psz[0] =
0; } int CMyString::Length() { return
m_nLength; }
All we have done here is add a private data member to cache the
length of the (immutable) string, which is hidden from any client
user of this class. Sure, this change is hidden from the client user
of the class, but is this change hidden from the compiler? The
compiler needs to know about all aspects of the class in order to
generate code for it - its size for instance. The new and delete operators needs to know how much memory to
allocate for each object, and they get this information from the
class definition at compile time. OK, so the class definition has
changed - we just recompile. But wait a minute! We said that one of
the great advantages of dynamic linking was that because of the
delayed linkage technique, we could overwrite our DLL with a newer
version, provided none of the class method signatures changed (and
they haven't) and clients would automatically reap the benefits. But
there are old clients out there who's already compiled new and delete calls are allocating and deleting 4 bytes for
instances of our old class. The upgraded class in the new version of
our DLL expects 8 bytes for the object. You can see that things will
quickly go bad. C++ gives us syntactical encapsulation and not
binary encapsulation.
If you want to write some code to test this problem out, then click here.
Absence of a binary standard for C++ objects
The binary encapsulation problem turns out to be worse than we
thought. We've just seen that we can break the binary compatibility
between a client and the class that it is using, even when both
client and class are compiled with a single compiler. In fact, there
are many C++ features that are implemented differently from compiler
to compiler and they also drastically affect the binary layout of
the object. Some examples are the provision of Runtime Type
Information (RTTI), the vtable layout and virtual function prologues
for classes using multiple inheritance, how virtual destructors are
implemented, and how exceptions are thrown and caught. Discussion of
these more esoteric issues is largely beyond the scope of this
tutorial.
| Solutions to Problem 2 : Binary layout of C++
objects |
Versioning
We just provide a different DLL for each new version of our DLL.
When a DLL changes its implementation (regardless of whether the DLL
public exported interface has changed or not) we don't overwrite the
old DLL with the new one. We just rename each new version of the DLL
to be encoded with its version number to make it unique. This allows
old clients to use the old DLL version and new clients to link
against, and use, the new one. So we end up with MYSTRING_1_0.DLL,
MYSTRING_1_1.DLL, MYSTRING_2_0.DLL, and so on. Our machine fills up
with many versions of the same DLL, which probably only differ in
minor ways. We have a similar problem to the one we had with static
linking, but infinitely more confusing. It is not a very elegant
solution.
There has got to be a better runtime solution that will allow
clients built with one C++ compiler to use classes built with
another, and allow classes to evolve and change over time to
accommodate the expectations of new clients without breaking old
clients.
A better solution
So, in the absence of any standard binary layout for a C++ object
across all compilers, we have to find a pragmatic solution. A
sensible approach would seem to hinge around the following:
- Minimizing the point of contact between a client and a class.
The smaller the interface between client and class and the less
dependencies between them, the less can go wrong when the class
changes implementation details.
- Hiding the implementation details of the class behind a level
of indirection. The client deals with the implementation through
some abstract interface that doesn't ever change. Some indirection
glue binds the abstract interface to the class implementation
code.
- Concentrating on those parts of the binary layout for a C++
class or object that we know to be common across all existing
compilers for a given platform.
We present two different solutions along these lines: handle
classes and pure abstract base classes.
Handle classes
Let's redefine our string class like this:
Header file, shared by client and DLL implementation,
imystring.h:
#ifndef CMyString class
CMyString; #endif
class EXPORTORIMPORT
IMyString { private: CMyString
*m_pthis; public: IMyString(const char *
psz); ~IMyString();
const char*Find(const char *psz); int
Length(); };
Header file, not seen by client but used by DLL implementation,
mystring.h:
class
CMyString { private: char
*m_psz; int
m_nLength; public: CMyString(const char *
psz); ~CMyString();
const char*Find(const char *psz); int
Length(); };
Source file, not seen by client, imystring.cpp:
IMystring::IMyString(const char *
psz) : m_pthis( new CMyString(psz))
{ } IMystring::~IMyString())) { delete
m_pthis; } int IMyString::Length() { if
(m_pthis) return
m_pthis->Length();
else return
0; } const char*IMyString::Find(const char *psz)
{ if
(m_pthis) return
m_pthis->Find(psz);
else return
NULL; }
This technique has some advantages.
- The implementation class
is never publicly exposed. Only the handle class, IMyString, is
exposed. The handle class must not change else we are back in the
same trouble! The handle class only contains a reference to the
implementation class and the new and delete is contained totally
within the DLL. So the implementation class can change with no
adverse effect on the client.
- There is no loss of type
safety, as there is with other handle-based schemes like
handle-based functions (such as the Windows API ).
- C++ programmers feel
compelled to wrap up handle-based functions (such as the Windows
API) but will not bother to wrap handle classes.
There are also some disadvantages.
- Manual forwarding for each method is
labor-intensive.
- We still haven't solved the name-mangling
problem. N methods implies N compiler-dependent exported
symbols.
- This technique is very much C++ specific
and is virtually impossible to use from other languages.
If you would like to write some code to try out this technique,click here.
Pure abstract base classes
The third solution to the name-mangling problem, mentioned
earlier, seems most compelling. That is, to provide a shared
abstract base class definition that the executable clients program
against and the DLL-based class implements. There is no access by
name, rather access by vtable offset. That approach will only work,
however, if a single definition of an abstract base class yields the
same binary vtable layout in memory. It turns out that the vtable
for a class has the same binary layout in memory across all known
C++ compilers on a given platform, as long as a single inheritance
hierarchy is assumed. It also turns out that a given method calling
convention will be predominant across all C++ compilers on a given
platform, so as long as we restrict ourselves to this convention we
will be OK.
Remember that on each different platform we are going to have to
re-compile anyway. So, for each different platform we could
abstract away the details of things like calling convention by
having an intermediate way of defining an interface and providing a
tool that spits out the right C++ language mappings for a given
platform. This is an interesting thought!
For cross-platform access you could even get a tool to take the
intermediate interface definition and spit out code for each
platform to map the interface from one context to another across the
network. That might even work on the same machine across different
processes. Another interesting thought!
Next
we will look at the pure abstract base class solution in more
detail.

|