top header
com as a better c++term

 

home contents introduction com as a better C++ interfaces classes apartments labs lab setup quizzes
 
previousnext

 
Cross-Compiler Problems

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.


Top of document