Using C++ In Dynamic Link Libraries

From EDM2
Jump to: navigation, search

Written by Gordon Zeglinski

Introduction

Using C++ In Dynamic Link Libraries

Just when things start moving along, a new set of setbacks arise. If they weren't happening to me, they'd be quite comical. At any rate, we'll take a break from SOM/DSOM in this issue, and look at using C++ code from Dynamic Link Libraries (DLLs). Before looking at C++ specific issues, we start by looking at some DLL basics.

DLL Basics

A DLL is a library of code and or resources that can be loaded and unloaded dynamically and shared between processes. Dynamic loading means that the library is loaded at run time rather than being statically bound to the application. When a DLL is loaded or unloaded, the operating system calls the function _DLL_InitTerm(). The following snippet is a skeleton _DLL_InitTerm() function:

unsigned long _System _DLL_InitTerm(unsigned long hModule, unsigned long
ulFlag){

   switch (ulFlag) {
      case 0 :
	 // do load initialization here
	 break;
      case 1 :
	 // do unload termination here
	 break;
   }

return 1UL;       //return 0 if we want to indicate that the initialization
code has failed

}

The above function (in its current form) is only useful for a resource DLL or a DLL that contains only assembly language functions. High level languages will require initialization and termination routines to be called. If we use C Set++ as an example, the minimal _DLL_InitTerm() function would be as follows:

unsigned long _System _DLL_InitTerm(unsigned long hModule, unsigned long
ulFlag){

   switch (ulFlag) {
      case 0:
	 //initialize the C Run Time library
	 if (_CRT_init() == -1)
	    return 0UL;

	 //for C++ code we call __ctordtorInit to create the global/static instances
 
	 __ctordtorInit();

	 // do user load initialization here
	 break;
      case 1:
	 // do user unload termination here

	 //for C++ code we call __ctordtorTerm to destroy static/global instances

	 __ctordtorTerm();

	 //if the C Run Time library is STATICALLY linked, we call _CRT_term to free up the
	 //resources used by the CRTL
	 _CRT_term();
	 break;
   }

return 1UL;       //return 0 if we want to indicate that the initialization code has failed

}

Note: for compilers other than C Set++, different run time library initialization/termination calls have to be made.

Functions are exported from a DLL by either name or numerical offset. The standard way of exporting a function is to specify its name in the "EXPORTS" section of the definition file for the DLL. Most compilers also allow a short cut method of exporting functions. This is to declare them with a special keyword that causes the compiler to generate code that the linker will automatically export for you. In C Set++ one uses the "_Export" keyword.

This seems simple enough. However, there are numerous pitfalls as we shall soon see.

DLLs and C++

C++ mangles function names. This can increase the difficulty in exporting C++ functions. C Set++ provides a utility called CPPFILT to extract mangled names so that they may be placed in the .DEF file. Alternatively, one can use the _Export keyword to have the compiler and linker work together to export the function. Let's define the class Foo so that it's member functions will be exported. The code to do this is presented below:

struct FooBar{
  int Y;
}
class _Export Foo{
  public:
    Foo();
    ~Foo();
    int GetVar(){
      return FooVar;
    }
void SetVar(int);
FooBar* CreateFooBar();
protected: int FooVar;
};

Note: Borland C++ allows one to use the _export keyword in a similar manner as one uses _Export in C Set++.

By placing the keyword _Export after the keyword class, the compiler automatically exports all non-inline functions. In this example Foo::Foo(), Foo::~Foo(), Foo::SetVar(int), and ,Foo::CreateFooBar() will all be exported. Foo::GetVar() will not be exported because it is implicitly defined as inline.

Let's say that we have coded Foo's member functions so they can be exported. We compiled the code and linked it statically to the C run time library to form a DLL. We then link the resulting DLL to the following code snippet to produce an executable file.

void main(){
   Foo      FooInst;
   FooBar   *Bar;

   Bar=FooInst->CreateFooBar();

   delete Bar;
}

Let's assume that Foo::CreateFooBar() is contained in the Foo DLL and is defined as follows:

FooBar*  Foo::CreateFooBar(){
      return new FooBar;
}

We've now compiled and linked our code. Everything should be fine until we actually try running the code. In this scenario, we have 2 active C run time libraries. The first is bound to the Foo DLL and the second is bound to the executable. When the delete operator is evoked, the memory is freed from the local C RTL. This is where the problem will occur. The memory was allocated in the DLL's C RTL and an attempt to free it was made in the executable's CRTL. There are two ways of solving this problem. First, one can use another DLL that holds the C RTL and dynamically link this DLL to FOO DLL and the executable. Alternatively, one can create and export a new function in FOO DLL that is used to delete instances of FooBar.

Creating a separate DLL for the C RTL is not without its pitfalls. The biggest pitfall is that OS/2 doesn't unload DLLs in reverse order. In fact, the order in which they are unloaded is somewhat unpredictable. This is a serious problem. What if the C RTL DLL is unloaded first? If this happens, it's most likely that when subsequent DLLs are unloaded they will trap or hang the process if they have any static or global object instances in them. One way around this is to use exit handlers at different priorities. These exit handlers call the __ctordtorTerm() function and keep track of the DLL's initialization state. Another method could be to use a counter variable in the C RTL DLL that keeps track of how many other DLLs are using it. This method would require the C RTL DLL to export a function for incrementing and decrementing this counter. These functions would have to be called each time a dependent DLL is loaded and unloaded. When the counter reaches 0, the C RTL is shutdown by calling _CRT_term. Both the counter method and the exit handler method require each DLL to use a custom _DLL_InitTerm function.

Using C++ DLLs Across Different Languages or Compilers

Because the name mangling scheme is not standardized, nor is the calling convention for that matter, it is impossible to use C++ functions "raw" across different compilers. To do this, one has to define C to C++ wrapper functions. The following snippet shows how to wrap the Foo class in C Set++.

extern "C"{
   void* _System CreateFoo();
   void  _System DestroyFoo(void *_Foo);

   int   _System FooGetVar(void *_Foo);
   void  _System FooSetVar(void *_Foo, int V);
   void* _System FooCreateFooBar(void *_Foo);

   void  _System DestroyFooBar(void *_FooBar);
};

void* _System CreateFoo(){
   return new Foo;
}

void  _System DestroyFoo(void *_Foo){
   delete ((Foo*)_Foo);
}

int   _System FooGetVar(void *_Foo){
   return ((Foo*)_Foo)->GetVar();
}

void  _System FooSetVar(void *_Foo, int V){
   ((Foo*)_Foo)->SetVar(V);
}

void* _System FooCreateFooBar(void *_Foo){
   return ((Foo*)_Foo)->>CreateFooBar();
}

void  _System DestroyFooBar(void *_FooBar){
   delete ((FooBar*)_FooBar);
}

The function names CreateFoo(), DestroyFoo(), etc. can be exported by placing them in the EXPORTS section of the DLLS module definition file. The following snippet illustrates how these wrapper functions can be used.

void main(){
   void  *FooInst=CreateFoo();

   FooSetVar(FooInst, 10);

   DestroyFoo(FooInst);
}

The above snippet can be the main routine for either a C or a C++ program. The problem of multiple C run time libraries is avoided in this case because all memory allocation and deallocation is handled in the DLL's wrapper functions.

Wrapping Things Up

That's it for another issue. We have seen several ways of using C++ code contained in a DLL. The problem of having multiple C runtime libraries within a single process along with the problem of DLL unloading has been examined. Several solutions to each of these problems have been presented here.