New Tricks for Dynamic Linking on the OS/2 2.x Operating System

From EDM2
Revision as of 20:51, 3 December 2018 by Ak120 (Talk | contribs)

Jump to: navigation, search

by John Keenleyside

Dynamic linking is not a new concept. Now, IBM has added some new features for 32-bit dynamic link libraries (DLLs). It is to build and use 32-bit DLLs on the OS/2 2.x operating system, especially if you use the IBM C Set++ compiler.

Building 32-bit DLLs

Building DLLs is straightforward in OS/2 once you understand the constraints. You build from source files containing the data or functions you want to include in the DLL. Each function or data item you want to export from the DLL must have external scope.

You also need a module definition file. The OS/2 linker uses this file while linking the compiled objects to create a DLL. Basically, it's a plain text file that describes the name, attributes of segments, exported names or ordinals, imported names or ordinals, and other characteristics for the DLL. (Ordinals are numbers you can assign to exported functions.)

Using 32-bit DLLs

Dynamic linking comes in two flavors:

  • Load-time dynamic linking, which resolves external references within a segment at the time the segment is loaded.
  • Runtime dynamic linking, which does not resolve references within a code segment until the actual code is executed.

To use a DLL, write the source file for the program or DLL that will use it. Refer to the functions or variables as if they were going to be statically linked into the program or DLL. Then, at link-edit time, tell the linker that some function or variable reference is a dynamic reference to a DLL and will be resolved at runtime.

OS/2 2.x lets you communicate this information to the linker as follows:

  • Create a module definition file for the program or DLL that is going to use the DLL. Specify the imported names or ordinals under the keyword IMPORTS.
  • Use the IMPLIB utility from the OS/2 Developer's Toolkit to create an import library for the linker. It will contain a list of the exported names or ordinals for the DLL. Create the import library from the module definition file or the DLL itself. This is the preferred approach, because it eliminates the need to create a module definition file. It also keeps the DLL independent from your application programs, so you can use it again.
  • Even easier, IBM C Set++ will do it all automatically. Like many C/C++ compilers, the IBM C Set++ compiler supports the _Export keyword. But it also supports the import and export pragmas. This lets you specify exports and imports within the source code itself - especially useful for C++ programmers who want to export classes from their DLLs but don't want the hassle of figuring out all the mangled names to export.

The following shows a bit-counting function you can build into a DLL. It also provides a small application source (Main.C) file that shows you how to call the bitcount() function. Using the C Set++ compiler, the command file builds the DLL and the application program.

/* From BITS.C */
/* This function will count the number of 1 bits in n. */

unsigned int bitcount(unsigned int n)
{
  unsigned int count;
   for (count = 0; n; n >>= 1)
     if (n & 1)
        count++;
  return count;
}

/* From BITS.DEF */
LIBRARY BITS INITINSTANCE TERMINSTANCE
DATA MULTIPLE NONSHARED
EXPORTS
  bitcount

/* From MAIN.C */
#include <stdio.h>
unsigned int bitcount(unsigned int n);
int main(void)
{
  printf("The number of 1 bits in %u is %u\n",
         123, bitcount(123));
  return 0;
}

/* From BUILD.CMD */
/* This is a REXX command file. */

/* Compile and link-edit BITS.C to create BITS.DLL */
'ICC /Ge- /O+ BITS.C BITS.DEF'

/* Create the import library for BITS.DLL */
'IMPLIB BITS.LIB BITS.DEF'

/* Compile and link-edit MAIN.C to create MAIN.EXE */
'ICC /O+ MAIN.C BITS.LIB'

Sample Program 1. Simple Bit-Counting Function

Initialization and Termination

When you load a DLL in OS/2 2.x, you might need to allocate memory and initialize data and other resources before you can call code within the DLL. Then, after an application is finished using a DLL, you might need to free up resources such as memory.

The C Set++ libraries provide functions to initialize and terminate the C/C++ runtime environment. The function _CRT_init() initializes the C runtime environment, and the function __ctordtorInit calls any C++ constructors that are required. The __ctordtorTerm function calls the C++ destructors, and the _CRT_term function terminates the C runtime environment.

You can export the initialization and termination functions from the DLL, so each application can call them. The initialization functions are called before any other functions within the DLL. The termination functions are called just before the application is finished with the DLL. This makes the DLL dependent on the applications that use it.

Fortunately, another way exists for OS/2 to call a function within the DLL when it is loaded and when an application tells the operating system that it is finished with a DLL.

A DLL can have an entry point just like an executable. This entry point is called when the DLL is loaded and when it is unloaded. This process is called global initialization and termination. The entry point also can be called each time a new process accesses a DLL or is finished with the DLL. This process is called instance initialization and termination. The whole initialization and termination routine becomes automatic.

A New Way to Use Entry Points

The OS/2 2.x operating system also provides a new feature for the termination of 32-bit DLLs. In OS/2 1.x, the entry point was called only for initialization. But if the entry-point routine is in a 32-bit code segment (as in OS/2 2.x), it also will be called for termination.

For the DLL entry point function to determine whether it is being called for initialization or termination, the loader passes a flag value to it. If the flag is 0, it is being called for initialization. If the flag is 1, it is being called for termination.

The module handle for the DLL also is passed to the entry-point function. The following describes the contents of the stack and the values of the 80x86 registers when the entry-point function is called.

Register values at LX format DLL initialization and termination:

EIP = DLL entry point address
ESP = Current stack pointer for thread that is loading the DLL
EAX = EBX = ECX = EDX = ESI = EDI = EBP = 0
CS = Code selector for base of linear address space
DS = ES = SS = Data selector for base of linear address space
FS = Data selector of base of the Thread Information Block (TIB) for the thread that is loading the DLL
GS = 0

Stack contents at DLL initialization:

[ESP+0] = Return address to system where the value of EAX is the return code
[ESP+4] = Module handle for the DLL
[ESP+8] = 0 which means that the DLL entry point is being called for initialization

Stack contents at DLL termination:

[ESP+0] = Return address to system where the value of EAX is the return code
[ESP+4] = Module handle for the DLL
[ESP+8] = 1 which means that the DLL entry point is being called for termination

Note: The stack contents at DLL initialization and termination follows the system calling convention except that AL is not set to the number of DWORDS of parameters passed.

The entry-point function can be written in a high-level language like C, but it must have system linkage, because it is called from the operating system. Also, because the return value of this function is returned in the EAX register, an unsigned long return type is appropriate.

If you are using the C Set++ compiler, the prototype for this function is simple:

unsigned long _System entry(unsigned long hModule, unsigned long ulFlag)

OS/2 automatically confirms your results. A non-zero return value tells the loader that the DLL initialization was successful. A zero return value tells the loader that an error occurred.

A New Way to Specify and Set Entry Points

The entry point is typically specified by the module end record within an object module that is linked into the DLL. With most compilers, you generate the module end record using a small assembler module, as follows.

        TITLE   DLLSTUB.ASM

        .386
        .387

CODE32   SEGMENT DWORD USE32 PUBLIC 'CODE'
CODE32   ENDS
DATA32   SEGMENT DWORD USE32 PUBLIC 'DATA'
DATA32   ENDS
CONST32  SEGMENT DWORD USE32 PUBLIC 'CONST'
CONST32  ENDS
BSS32    SEGMENT DWORD USE32 PUBLIC 'BSS'
BSS32    ENDS

DGROUP   GROUP  CONST32, BSS32, DATA32
         ASSUME CS:FLAT, DS:FLAT, SS:FLAT, ES:FLAT

         EXTRN  entry:PROC

         END    entry

C Set++ makes it easier to specify the entry point. It gives you the object module to set the entry point in its runtime libraries. All you do is name the entry-point function _DLL_InitTerm. A default _DLL_InitTerm() function calls the initialization and termination functions.

C Set++ also provides the pragma, #pragma entry. This new feature lets you set the entry point for a module using the C language rather than assembler, so you can pick the entry-point function name. It also means that you don't have to use the DLL generation option (/Ge-) when compiling the source files for the DLL. You decide at link-edit time, rather than at compile time, whether to put the objects into a DLL or into an application.

The following shows you how to write a DLL entry-point function using the features of C Set++. The application program provided in the listing uses runtime dynamic linking.

/* From DLLENTRY.C */
#pragma strings(readonly)

#define INCL_DOSFILEMGR
#define INCL_DOSMODULEMGR
#include <os2.h>
#include <string.h>

int _dllentry = 1;  /* just in case an object is compiled with /Ge- */
char name[CCHMAXPATH];

#pragma entry(entry)
unsigned long _System entry(unsigned long hModule, unsigned long
ulFlag)
{
   APIRET rc;
   unsigned long ulBytesWritten;

   rc = DosQueryModuleName(hModule, CCHMAXPATH, name);

   if (!rc)
   {
      if (ulFlag == 0)
      {
         rc = DosWrite(1, name, strlen(name), &ulBytesWritten);
         rc = DosWrite(1, " initialized.\r\n", 15, &ulBytesWritten);
      }
      else
      {
         rc = DosWrite(1, name, strlen(name), &ulBytesWritten);
         rc = DosWrite(1, " terminated.\r\n", 14, &ulBytesWritten);
      }
   }

   return !rc;   /* non-zero means DLL init/term was successful */
}

void hello(void)
{
   unsigned long ulBytesWritten;

   DosWrite(1, "Hello there\r\n", 13, &ulBytesWritten);

   return;
}
/* From SIMPLE.DEF */
LIBRARY SIMPLE INITINSTANCE TERMINSTANCE

EXPORTS
   hello
/* From RUNTIME.C */
#pragma strings(readonly)

#define INCL_DOSMODULEMGR
#define INCL_DOSPROCESS
#include <os2.h>

char pszErrorBuf[CCHMAXPATH];

void hello(void);

int main(void)
{
   APIRET rc;
   HMODULE hDLL;
   PFN pHello;

   rc = DosLoadModule(pszErrorBuf, CCHMAXPATH, "SIMPLE", &hDLL);

   if (!rc)
   {
      rc = DosQueryProcAddr(hDLL, 0, "hello", &pHello);

      if (!rc)
         pHello();

      rc = DosFreeModule(hDLL);
   }

   return rc;
}
/* From BUILD.CMD */
/* Build a simple DLL that shows how the DLL entry point function works. */
'ICC /C /Rn /O+ DLLENTRY.C'
'ICC /Rn /Ge- /FeSIMPLE.DLL DLLENTRY SIMPLE.DEF'
'ICC /Rn /O+ RUNTIME.C'

Ordering Issues

Sometimes the initialization part of the entry-point function for DLL A depends on DLL B being initialized first. For instance, DLL B could be a C runtime DLL, and the entry point function in DLL A could be using some C runtime functions. In this case, you should initialize the C runtime DLL before DLL A.

To handle this, DLL B exports a function that performs the initialization required. This function can be called by the entry-point function in DLL B or the entry-point function in DLL A. Use the _CRT_init() and __ctordtorInit() functions that are provided in the C Set++ libraries.

Ordering constraints also can be a problem at termination. There is no way to determine whether a DLL has been unloaded or not, you can't solve the problem by calling the termination function of another DLL. If you call a function in an unloaded DLL, you'll get an access violation.

You can use the DosExitList() API to register a termination function within the DLL that will be called when a process terminates. DosExitList() takes a function-order parameter, allowing termination functions to be called in a specific order. If you register more than one termination function using the same order number, these functions are called in last-in first-out order. When a process terminates, OS/2 calls all the termination functions registered with DosExitList() before any of the DLL entry-point functions are called for termination processing.

A different termination problem exists when DLLs are unloaded using the DosFreeModule() API. In this case, the termination function registered by the DLL with DosExitList() is not called since a process is not terminating. OS/2 cannot unload the DLL since an exit list contains a reference to a function within the DLL.

Fortunately, OS/2 2.x provides a new, improved solution to this problem. It calls a 32-bit entry-point function in a DLL whenever the DLL is about to be unloaded. The entry-point function can call DosExitList() to remove the registered termination function and instead perform the termination processing itself. The DLL can then be unloaded, and all of the resources it was using will have been freed.

Summary

In the next issue of The Developer Connection News, the discussion of DLLs will continue. Topics such private or shared databased DLLs, exception handling within DLLs, and resource DLLs will be covered.

References

  1. OS/2 Version 2.0 - Volume 4: Application Development GG24-3774-00

Notes

  1. All sample code has been compiled using the GA version of IBM C Set++ compiler. It has been tested on the GA level of OS/2 2.0.

Reprint Courtesy of International Business Machines Corporation, © International Business Machines Corporation