Dynamic Linking

From EDM2
Jump to: navigation, search

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

This content is part of the Control Program Programming Guide and Reference.

This chapter describes:

  • Static versus dynamic linking
  • Dynamic link library (DLL) data
  • DLL initialization and termination
  • Building a DLL
  • Use of protected memory by DLLs

Static versus dynamic linking

Most programmers are familiar with static linking; an application calls a routine or procedure whose code is not found in the application's source file. The routine is external to your source file and is declared as such. When the source file is compiled, the compiler places an external reference for the routine in the application's object (.OBJ) file. To create the executable file (.EXE) for the application, the application's object file is linked with an object file that contains the code for the routine. The result is an EXE file that contains the application code, as well as a copy of the code for the routine. The following figure illustrates the process of building a statically linked application.

  My_Application.OBJ
 ┌────────────────────────┐
 │ EXTERNAL               │
 │     Your_Routine       │
 ├────────────────────────┤
 │ .                      │                  My_Application.EXE
 │ .                      │                 ┌──────────────────┐
 │ CALL ???; Your_Routine ├──┐              │ .                │
 │                        │  │  ┌──────┐    │ .                │
 └────────────────────────┘  ├─►│ LINK ├───►│ CALL xxx         │
                             │  └──────┘    │ .                │
  Your_Library.OBJ           │              │                  │
 ┌────────────────────────┐  │         xxx: │ Your_Routine:    │
 │ PUBLIC                 │  │              │                  │
 │     Your_Routine       │  │              └──────────────────┘
 ├────────────────────────┤  │
 │                        │  │
 │ Your_Routine:          │  │
 │                        ├──┘
 └────────────────────────┘

Static Linking

When OS/2 loads a statically linked program, all the code and data are contained in a single EXE file and the system can load it into memory all at once.

The advantages and disadvantages of static linking are summarized in the following table.

Advantages Disadvantages
Compile in pieces External routines built into
EXE (making EXEs larger)
Can create libraries of routines that can be linked with applications. EXE cannot be changed without relinking.
External routines cannot be shared (duplicate copies of libraries).

Dynamic linking permits several applications to use a single copy of an executable module. The executable module is completely separate from the applications that use it. Several functions can be built into a DLL, and applications can use these functions as though they were part of the application's executable code. You can change the dynamically-linked functions without recompiling or relinking the application.

The advantages of dynamic linking are:

Reduced memory requirements
Many applications can use a single DLL simultaneously. Since only one copy of the DLL is in memory, this saves memory space and, in general, the code is discardable.
Simplified application modification
An application can be enhanced or modified simply by changing a DLL. For example, if an application uses a DLL to support video output, several displays can be supported by different DLLs. The application can use the DLL that supports the appropriate display.
Flexible software support
DLLs can be used for after-market support. In the display-driver example, a new DLL can be provided to support a display that was not available when the application was shipped. Similarly, a database application can support a new data-file format with a new DLL.
Transparent migration of function
The DLL functions can be used by applications without any understanding of how the functions actually do their work. Future changes to the DLL are transparent to the application, as long as the input and output parameters remain the same.
Multiple programming language support
A function in a DLL can be used by an application written in any programming language, as long as the application understands the function's calling convention.
Application-controlled memory usage
Applications can make decisions about which DLL routines they want to load into memory and use. If a DLL is not used, it does not have to be loaded.

DLLs can be used to implement subroutine packages, subsystems, and interfaces to other processes. In OS/2:

  • Some DLLs are interfaces to the kernel.
The worker routines for the OS/2 API reside in the OS/2 kernel. Applications, which run at privilege level 3, usually can make direct calls to the kernel, which runs at privilege level 0. Some calls, however, must be linked through a DLL. For example, an application that calls DosOpen is linked to the DLL DOSCALL1.DLL. This library contains the definitions for some system functions. When a system function is called, OS/2 makes the call to the kernel on behalf of the application.
  • Some DLLs are interfaces to devices.
DLL subsystems can virtualize devices and manage the device for its clients.
  • Some DLLs provide an open system architecture for software.
Add-ons to OS/2 can be provided easily and by anyone.

OS/2 provides two varieties of dynamic linking: load-time and run-time. In load-time dynamic linking, references are resolved when an application is loaded. In run-time dynamic linking, references are resolved when the application runs.

Load-Time Dynamic Linking

Load-time dynamic linking is similar to static linking in that an application can call a routine that is not found in the application's source file. In load-time dynamic linking, however, an application is linked with a library file that contains a record that describes where the routine can be found instead of with a file that contains the code for the routine. The resulting application executable file contains this record and not a copy of the routine's code. The following figure illustrates the process of building a load-time dynamically linked application.

In the example in the following figure, the LIB file contains a record that describes where the code for a set of functions can be found. In this case, the code for the function Your_Routine can be found in the file, or module, Your_Routines.DLL under the entry point name Your_Routine. (The entry point name does not have to match the function name.) The function code also can be referenced by its ordinal value.

  My_Application.OBJ
 ┌────────────────────────┐
 │ EXTERNAL               │
 │     Your_Routine       │
 ├────────────────────────┤
 │ .                      │                  My_Application.EXE
 │ .                      │                 ┌──────────────────┐
 │ CALL ???; Your_Routine ├──┐              │ .                │
 │                        │  │  ┌──────┐    │ .                │
 └────────────────────────┘  ├─►│ LINK ├───►│ CALL ???         │
                             │  └──────┘    │ .                ├◄─┐
  Your_Library.LIB           │              │                  │  │
 ┌────────────────────────┐  │              ├──────────────────┤  │
 │                        │  │              │ Reference to     │  │
 ├────────────────────────┤  │              │                  │  │
 │ function name:         │  │              │ Your_Routine.1   │  │
 │       Your_Routine     │  │              │ Your_Routine.    ├──┘
 │                        │  │              │     Your_Routine │
 │ module name:           │  │              │                  │
 │       Your_Routines    ├──┘              └──────────────────┘
 │                        │
 │ entry point            │
 │       ordinal value: 1 │
 │       entry point name │
 │           Your_Routine │
 ├────────────────────────┤
 │                        │
 └────────────────────────┘

Dynamic Linking

When the application is loaded, the system resolves the dynamic-link references, as shown in the following figure.

My_Application.EXE               Your_Routine.DLL
┌─────────────────┐              ┌─────────────────┐
│  .              │              │   Entry Table   │
│  .              │              │       1    ─────┼──┐
│  .              │              │.................│  │
│ Call ???        │◄─┐           │                 │  │
│  .              │  │           │                 │  │
│                 │  │   yyy:    │  Your_Routine:  │◄─┘
│                 │  │           │                 │
│.................│  │           │                 │
│Reference to     │  │           │                 │
│                 │  │           │                 │
│Your_Routine.1   ├──┘           └────────┬────────┘
│Your_Routine.    │                       │
│     Your Routine│                       │
└───────┬─────────┘                       │
        │                                 │
        └─────────────────┬───────────────┘
                          │
                       ┌──┴──┐
                       │LOAD │
                       └──┬──┘
                          │
        ┌─────────────────┴───────────────┐
        │                                 │
My_Application code              Other code
┌─────────────────┐              ┌─────────────────┐
│   Call yyy  ────┼─────► yyy:   │  Your_Routine   │
└─────────────────┘              └─────────────────┘

Resolving Dynamic Link References

If a program contains dynamically linked references, the system must process the information in the references. If the DLL already is in memory, the system adds information to the executable code so that the code can use the DLL functions. If the required DLLs are not already in memory, OS/2 searches the path specified by the LIBPATH environment variable. If the system cannot find the DLLs, it stops loading the application and reports the error. If the system finds the DLLs, it loads them. When DLLs are loaded into memory, OS/2 notifies the application where the DLL functions can be found.

When a DLL is loaded into memory is determined by how the DLL was built. A DLL is built like an application, using a module-definition (.DEF) file. The CODE statement in a DEF file describes the attributes of application or DLL code. The load option for the CODE statement determines when application or DLL code is loaded:

PRELOAD Code is loaded as soon as a process accesses the DLL. This leads to increased performance (in terms of accessing the DLL functions) while decreasing available memory. This option might be preferable if only one program is running.
LOADONCALL Code is loaded when needed. This is the recommended choice and the default.

Run-Time Dynamic Linking

When an application contains a reference to a DLL, the DLL is loaded into memory when the application is loaded (unless the DLL already is in memory). If the application uses functions in several DLLs, all of those DLLs are loaded into memory. Some applications might use functions in several DLLs but use only a few of them at any one time. For example, an application that supports many different printers or plotters might use functions in many DLLs but need only one of them at a time, depending on the printer or plotter the application is supporting. If the user switches to a different printer or plotter, another DLL will be used, but the first will remain in memory. Loading DLLs this way can be very wasteful.

To avoid this problem, applications can take advantage of run-time dynamic linking and load and unload DLLs as they are required. The process of building a run-time dynamically linked application is similar to the process of building a load-time dynamically linked application. However, the EXE file for a run-time dynamically linked application does not contain a record describing where the external routines can be found. Instead, the application explicitly tells OS/2 when to load and free the dynamic link module.

Applications load and unload DLLs and call functions whose code resides in those DLLs by:

  1. Calling DosLoadModule to get a handle to the DLL module.
    This function also loads the DLL into memory or attaches to it, if it already is loaded.
  2. Calling DosQueryProcAddr to get a pointer to a function within the DLL.
  3. Calling the function indirectly through the pointer returned by DosQueryProcAddr.
  4. Calling DosFreeModule to free the handle to the DLL module.
    When all handles to the DLL module have been freed, the DLL is unloaded from memory.

An application also can request information about a DLL from the system. The application can use the DosQueryModuleHandle function to determine whether a DLL has been loaded into memory already. The DosQueryModuleName function returns full path information for the DLL file.

Following are the advantages of run-time dynamic linking:

Memory is consumed as needed.
DLLs can be loaded and unloaded as they are used. Unused DLLs do not have to be loaded into memory, and memory can be released when the application has finished using the DLL.
Applications can recover from DLL NOT FOUND.
Applications can make decisions. If a load-time DLL cannot be found, the application terminates immediately. If a run-time DLL cannot be found, the application receives an error value from the DosLoadModule function, and it can use another DLL or specify a full path for the DLL. If a function is not available, the DosQueryProcAddr function returns an error value, and the application can take appropriate action.
DLL and function names can be variable.
An application programmer does not have to know the names of the DLLs the application will be using or the names of the functions within the DLL. The application can read the names of the DLL or the functions from a configuration file or obtain the names from user-supplied input.
DLLs can be anywhere.
The application can specify the full path for the DLL. Load-time DLLs must be in a directory in the path specified by the LIBPATH environment variable, but run-time DLLs can be in other directories.

Dynamic link library (DLL) data

Most DLLs will contain some data. Whereas DLL code is shared by all processes that use it, DLL data can be shared or not shared by all processes that use it. Data that is specific to each process that uses the DLL (that is, to each instance of the DLL) is called instance data. Data that is shared by all processes that use the DLL is called shared or global data.

The first time a process references a DLL (and it is loaded or its usage count is incremented because it is already loaded), a separate copy of the DLL's instance data is created. Modifications to the instance data for one process do not affect the instance data for any other process. The system, however, maintains only one copy of a DLL's shared data for all processes that use the DLL. Every process that uses the DLL can access the same data. If one process modifies the data (increments a count, for example), the data will be modified for all processes that use the DLL.

Because changes to shared DLL data by one process are visible to the DLL code when called by another process, shared data provides DLLs with a mechanism for tracking processes that use it. This is crucial to subsystems, which are DLLs that manage resources (for example, devices, queues, and so forth).

There usually is only one data and one code object, or segment, defined in a C source file. This means that a DLL that has instance and shared data is built from more than one C source file, with a default automatic data segment and with named data segments. How data is defined is determined by how the DLL is built. A DLL is built like an application, using a DEF file. The DATA statement in a DEF file describes the attributes of application or DLL data. Following is a list of the available options for the DATA statement:

Options Result
MULTIPLE OS/2 creates a unique copy of the automatic data segment for each process that uses the DLL. Modifications made by one process do not affect any other process. This is the default.
SINGLE OS/2 creates only one automatic data segment for all processes that use the DLL. If one process modifies the data, the data will be modified for all processes that use the DLL.
READWRITE Enables you to read from or write to the automatic data segment. This is the default.
READONLY Enables you to read only from the automatic data segment.
LOADONCALL The automatic data segment is loaded into memory as needed. This is the default.
PRELOAD The automatic data segment is loaded as soon as a process accesses a DLL.
┌───────────┐                   ┌───────────┐
│ Process A │                   │ Process B │
└──┬─────┬──┘                   └──┬─────┬──┘
   │     │                         │     │
   │     │                         │     │
┌──┼─────┼─────────────────────────┼─────┼──┐
│  │     │       Dynamic Link      │     │  │
│  │     │        Functions        │     │  │
└──┼─────┼─────────────────────────┼─────┼──┘
   │     │                         │     │
   │     │      ┌───────┐          │     │
   │     │      │Shared │          │     │
   │     └─────►│ Data  │◄─────────┘     │
   │            └───────┘                │
   │              ┌─────────┐            │
   │              │Process B│            │
   │            ┌─┴───────┐ │◄───────────┘
   │            │Process A│ │
   └───────────►│Instance ├─┘
                │  Data   │
                └─────────┘

DLL Data

You also can create a DLL with both shared and instance data. For more information, see Using Shared and Instance Data

DLL initialization and termination

A DLL is initialized and terminated by the default _DLL_InitTerm function. When a process gains access to the DLL, this function initializes the necessary environment for the DLL, including storage, semaphores, and variables. When the process frees its access to the DLL, the _DLL_InitTerm function terminates the DLL environment created for that process. The _DLL_InitTerm function is called automatically when you link to the DLL.

The _DLL_InitTerm function can be executed once when the DLL is first loaded into memory, or it can be executed each time a new process first accesses the DLL. The LIBRARY statement in the module-definition file is used to specify when the _DLL_InitTerm function is to be executed. Following is a list of of the available options for the LIBRARY statement.

Options Result
INITINSTANCE The _DLL_InitTerm function is called the first time the DLL is loaded for each process that accesses the DLL.
INITGLOBAL The _DLL_InitTerm function is called only the very first time the DLL is loaded. This is the default.
TERMINSTANCE The _DLL_InitTerm function is called the last time the DLL is freed for each process that accesses the DLL.

As an example, the following statement, identifies the executable file as a DLL and specifies that SAMPLE03 is the name of the DLL:

LIBRARY SAMPLE03 INITINSTANCE TERMINSTANCE

It also specifies that the _DLL_InitTerm function is to be called the first time the DLL is loaded for each process that calls the DLL and the last time the DLL is freed for each process that calls the DLL.

When OS/2 starts executing a DLL, it sets the CPU registers to known values, but only for 16-bit DLLs. All 32-bit DLLs are called with a stack frame, like all other API calls.

Initialization/termination functions can be written in a high-level language. For more information on writing your own initialization/termination function, see #Creating an Initialization/Termination Function.

Building a DLL

Building a DLL is not very different from building a conventional static library. The following sections show how you can use OS/2 tools and functions to create, manage, and use DLLs.

Use of protected memory by DLLs

External Function References

When you compile or assemble an application, the compiler or assembler generates an object module for the code in the application. If you use any functions that are external to your application (their code is in another object module), the compiler or assembler adds an external function reference to your application's object module.

The Linker resolves these external references. If the Linker finds the external function in a library called an import library or in an IMPORTS statement in the application's module-definition file, the code for the external function is in a DLL. To resolve external references to DLLs, the Linker simply adds information to the executable file that tells the loader where to find the DLL code when the application is loaded.

Module-Definition Files

The module-definition file is an important tool for building DLLs. This file contains information that tells OS/2 the name of the DLL, when to load the DLL, how to manage memory for the DLL, and when to initialize the DLL.

When you create a DLL, the module-definition file must contain a list of all the functions in the DLL that can be called by an application (or by another DLL). You specify these external functions by using an EXPORTS statement in the module-definition file.

You also must tell the Linker where to find the external functions in your application. If the functions are in a DLL, you can use an IMPORTS statement in the module-definition file for the application to tell the Linker where to find the DLL functions. You also can use an import library to tell the Linker where to find your DLL functions.

Import Libraries

A conventional library contains object modules for a number of functions. The library is a convenient way to manage a large number of modules and use them in your executable code by linking to the library. The Linker uses the external references in your object module to determine which modules must be pulled out of the library.

An import library does not contain any object modules. Instead, the import library contains information that tells the Linker what DLLs are used by your application and the location of the functions your application uses within each DLL.

Like a conventional library, an import library primarily is a convenience. Instead of specifying all the functions your application imports in its module-definition file, you can link with the import library and let the Linker resolve the external references in your object module.

You use import libraries every time you compile and link a program that uses the OS/2 API. All the OS/2 functions are implemented in DLLs, and OS2386.LIB is an import library that tells the Linker where to find each OS/2 function.

For more information about module-definition files and import libraries, see the online Tools Reference in the OS/2 Warp Developer's Toolkit.

Creating a Simple DLL

DLLs typically are used to provide common functions that can be used by a number of applications. The following figure shows a C source file, MYPUTS.C, for a DLL that contains a simple string-printing function.

#include <os2.h>

#define HF_STDOUT 1  /* standard-output handle               */

VOID EXPENTRY myPuts(PSZ pszMsg)
{
    ULONG ulWritten;

    while(*pszMsg) {
        DosWrite(HF_STDOUT,
                 pszMsg,
                 1,
                 &ulWritten);

        pszMsg++;
    }
}

The following figure shows the module-definition file, MYPUTS.DEF, for this DLL.

LIBRARY myputs
DATA SINGLE SHARED
EXPORTS
    myPuts

The LIBRARY statement names the DLL (MYPUTS.DLL). The DATA statement tells the system that this DLL will share all data with each process that uses the DLL. The EXPORTS statement indicates that the function myPuts can be used by applications and DLLs.

The DLL is compiled and linked like any application. You can use IBM's ICC and LINK386, as shown below, to create MYPUTS.DLL.

icc  /C+ /Ge- myputs.c

link386 /noi myputs, , nul, OS2386, myputs

When the DLL has been created, you must copy it to one of the directories indicated by the LIBPATH environment variable in your CONFIG.SYS file.

Importing DLL Functions

After you create a DLL, you can use it in an application. The following figure shows a C source file, USEPUTS.C, that uses the myPuts function contained in the DLL MYPUTS.DLL.

#include <os2.h>

VOID EXPENTRY myPuts(PSZ);

VOID main(VOID)
{
   myPuts("Testing, 1,2,3");
}

The module-definition file for USEPUTS.C tells OS/2 where to find the myPuts function. This module-definition file (USEPUTS.DEF) contains the information shown in the following figure.

NAME useputs
IMPORTS
  myputs.myPuts

The module-definition file tells OS/2 that USEPUTS imports the myPuts function from MYPUTS.DLL. USEPUTS.C is compiled and linked as shown below.

icc  /C+ useputs.c

link386 /noi useputs, , nul, , useputs

Using an Import Library

You also can create an import library for your DLL. If you do this, you can link applications with your DLL without explicitly declaring the imports for each application. OS/2 uses this technique for the application programming interface (API). When you link your applications with OS2386.LIB, you are using an import library.

To create the import library STRINGS.LIB from MYPUTS.DLL, you use the Import Library Manager (IMPLIB), as shown below.

implib strings.lib myputs.def

You then can link your applications with STRINGS.LIB to resolve references to the myPuts function, as shown below.

link386 /noi useputs, , nul, strings;

A module-definition file for USEPUTS.C is optional in this example because we are linking with an import library.

Using Shared and Instance Data

When you create a DLL, you can use the DATA statement in the module-definition file to define the default attributes for data segments within the DLL. The default condition is for the DLL to have a unique copy of the automatic data segment for each process. You can specify DATA MULTIPLE READWRITE in the module-definition file to cause OS/2 to create a separate copy of all the DLL data for each process that uses the DLL (instance data). Modifications made by one process do not affect other processes.

You also can specify different attributes for different sets of data by using the #pragma data_seg and #pragma alloc_text directives to define your own data and code segments. You can list the segments in the module-definition file under the heading SEGMENTS and specify attributes for each.

   SEGMENTS
      mydata SINGLE READONLY
      mycode PRELOAD

Any segments that you do not specify under SEGMENTS are given the attributes specified by the DATA or CODE statement, depending on the type of segment.

Creating an Initialization/Termination Function

It might be necessary for a DLL to perform some tasks before an application accesses a DLL or after an application finishes accessing a DLL. For example, the library might need to allocate a heap or open a device prior to using a DLL, or deallocate a heap or close a device after using a DLL. You can handle these tasks in an initialization/termination function. The initialization/termination function can be called to perform initialization tasks when the DLL is first loaded or each time a new process accesses the DLL, depending on the LIBRARY statement in the module-definition file. If you specify INITGLOBAL in the LIBRARY statement, the initialization/termination function is called only once, when the DLL is first loaded into memory. This is the default setting. If you specify INITINSTANCE, the library function is called each time the DLL is accessed by a new process. In the same way, the initialization/termination function can be called on to perform termination tasks. If you specify TERMINSTANCE, the library function is called each time the DLL is freed for each process that accesses the DLL.

When a thread calls DosLoadModule to load a DLL, the initialization routines of the loaded DLL (and the initialization routines of the DLLs that it loads) will run on the thread that called DosLoadModule. This initialization will complete before DosLoadModule returns.

The prototype for the _DLL_InitTerm function is:

unsigned long _DLL_InitTerm (unsigned long modhandle, unsigned long flag);

If the value of the flag parameter is 0, the DLL environment is initialized. If the value of the flag parameter is 1, the DLL environment is ended. The modhandle parameter is the module handle assigned by OS/2 for this DLL. The module handle can be used as a parameter to various OS/2 API calls. For example, DosQueryModuleName can be used to return the fully-qualified path name of the DLL, which tells you where the DLL was loaded from.

The return code from _DLL_InitTerm tells the loader whether the initialization or termination was performed successfully. If the call was successful, _DLL_InitTerm returns a nonzero value. A return code of 0 indicates that the function failed. If a failure is indicated, the loader will not load the program that is accessing the DLL.

Before you can call any C library functions, you must first initialize the C run-time environment. To initialize the environment, use the function _CRT_init. The prototype for this function is:

int _CRT_init (void);

If the run-time environment is successfully initialized, _CRT_init returns 0. A return code of -1 indicates an error. If an error occurs, an error message is written to file handle 2, which is the usual destination of stderr.

To properly terminate the C run-time environment, use the function, _CRT_term. The prototype for this function is:

void _CRT_term (unsigned long)

Because _DLL_InitTerm is called by OS/2, it must be compiled using the system linkage. In the IBM C Set/2 compiler, the following #pragma directive is used to specify the system linkage:

#pragma linkage (_DLL_InitTerm, system)

The initialization/termination function must have a specific entry point. You cannot create a function with a specific entry point in the C programming language, so the initialization function must be written in assembly language. However, you can write a very simple initialization function in assembly language and have it immediately jump to a C function. The following figure shows an Assembler language initialization function entry point.

PAGE     ,132
TITLE    DLLSTUB
NAME     DLLSTUB

.386
.387

EXTERN   _DLL_InitTerm:NEAR

END      _DLL_InitTerm

The following figure shows a sample initialization/termination function written in C. This code was written using the IBM C Set/2 compiler. If you use another compiler, some of the #pragmas or keywords might need to be changed.

/*+-------------------------------------------------------------------+*/
/*| Sample Program 03 : INITTERM.C                                    |*/
/*|                                                                   |*/
/*| COPYRIGHT:                                                        |*/
/*| ----------                                                        |*/
/*| Copyright (C) International Business Machines Corp., 1995         |*/
/*|                                                                   |*/
/*+-------------------------------------------------------------------+*/

#include <stdlib.h>
#include <stdio.h>

/*+-------------------------------------------------------------------+*/
/*| _CRT_init is the C run-time environment initialization function.  |*/
/*|It will return 0 to indicate success and -1 to indicate failure.   |*/
/*+-------------------------------------------------------------------+*/

int _CRT_init (void);

/*+-------------------------------------------------------------------+*/
/*| _CRT_term is the C run-time environment termination function.     |*/
/*+-------------------------------------------------------------------+*/

void _CRT_term (unsigned long);

size_t nSize;
int *pArray;

/*+-------------------------------------------------------------------+*/
/*| _DLL_InitTerm is the function that gets called by the operating   |*/
/*| system loader when it loads and frees this DLL for each process   |*/
/*| that accesses this DLL.  However, it only gets called the first   |*/
/*| time the DLL is loaded and the last time it is freed for a        |*/
/*| particular process.  The system linkage convention must be used   |*/
/*| because OS/2 loader is calling this function.     |*/
/*+-------------------------------------------------------------------+*/

#pragma linkage (_DLL_InitTerm, system)

unsigned long _DLL_InitTerm (unsigned long modhandle, unsigned long flag)
   {
   size_t i;

   /* If flag is zero then the DLL is being loaded so initialization  */
   /* should be performed.  If flag is 1 then the DLL is being freed  */
   /* so termination should be performed.                             */

   switch (flag)
      {
      case 0:
         /* The C run-time environment initialization function must   */
         /* be called before any calls to C run-time functions that   */
         /* are not inlined.                                          */

         if (_CRT_init () == -1)
            return 0UL;

         srand (17);

         nSize = (rand() % 128) + 32;

         printf ("The array size for this process is %u\n",nSize);

         if ((pArray=malloc (nSize * sizeof(int)))==NULL)
            {
            printf("Could not allocate space for unsorted array.\n");
            return 0UL;
            }

         for (i=0; i<nSize; ++i)
            pArray[i] = rand();

         break;

      case 1:
         printf("The array will now be freed.\n");

         free(pArray);

         _CRT_term(0UL);

         break;

      default:
         printf("flag = %lu\n",flag);
         return 0UL;

      }

   /* A nonzero value must be returned to indicate success. */
   return 1UL;

   }

You also can write the initialization/termination function entirely in assembly language, without jumping to a C function. For this case, the library initialization registers are defined as follows:

EIP
    Library entry address ESP
    User program stack CS
    Code selector for base of linear address space DS=ES=SS
    Data selector for base of linear address space

Note: All 32-bit protected memory library modules will be given a GDT selector in the DS and ES registers (ProtDS) that addresses the full linear address space available to an application. This selector should be saved by the initialization routine. Non-protected memory library modules will receive a selector (FlatDS) that addresses the same amount of linear address space as an application's EXE file.

FS
    Data selector of the base of the Thread Information Block (TIB) GS
    Is equal to 0 EAX=EBX
    Is equal to 0 ECX=EDX
    Is equal to 0 ESI=EDI
    Is equal to 0 EBP
    Is equal to 0 [ESP+0]
    Return address to system, and EAX = return code [ESP+4]
    Module handle for the library module [ESP+8]
    Is equal to 0 (for initialization)

A 32-bit library can specify that its entry point address is the 16-bit object code. In this case, the entry registers are the same as for entry to a library using the segmented EXE format. This means that a 16-bit library can be relinked to take advantage of the benefits of the linear EXE format (such as more efficient paging).

The library termination registers are defined as follows:

EIP
    Library entry address ESP
    User program stack CS
    Code selector for the base of the linear address space DS=ES=SS
    Data selector for the base of the linear address space FS
    Data selector of the base of the Thread Information Block (TIB) GS
    Is equal to 0 EAX=EBX
    Is equal to 0 ECX=EDX
    Is equal to 0 ESI=EDI
    Is equal to 0 EBP
    Is equal to 0 [ESP+0]
    Return address to the system [ESP+4]
    Module handle for the library module [ESP+8]
    Is equal to 1 (for termination)

Note: Library termination is not permitted for libraries with 16-bit entries.

Linking at Runtime

So far, the examples in this chapter have used load-time dynamic linking. With load-time linking, OS/2 loads the DLL containing the imported functions when it loads the EXE file. If it cannot find the necessary DLL, it terminates the application and reports the error.

Run-time dynamic linking permits an application to load a DLL into memory when it is required, and to remove the DLL when it is no longer needed. The application uses the DosLoadModule function to load the DLL into memory (if it is not loaded already). If the system cannot find the DLL, the application receives an error value and can take appropriate action. For example, the application might use another DLL or search another directory.

Once the application has loaded the DLL, it can use the DosQueryProcAddr function to obtain a pointer to the required function (or functions). The application then can use the function. When the DLL is no longer required, the application can use the DosFreeModule function to remove the DLL from memory. If there are other applications using the DLL, it remains in memory until the last application frees the DLL.

An application can specify a full path for the run-time DLL. If you specify the full path name, you can have two DLLs with the same name loaded at the same time, as in C:\OS2\DLLFILE.DLL and C:\OS2\DLL\DLLFILE.DLL. If the path is not specified, OS/2 assumes the DLL has the extension .DLL and looks for the file in the directories specified by the LIBPATH environment variable.

The following figure uses the run-time dynamic-linking functions to access the myPuts function in the MYPUTS.DLL dynamic link library.

 #define INCL_DOSMODULEMGR
 #include <os2.h>
 
 VOID (*EXPENTRY myPuts) (PSZ);
 
 VOID main(VOID)
 {
      HMODULE hmod;
      ULONG ulErr;
      UCHAR szFailName[CCHMAXPATH];
 
      ulErr = DosLoadModule(szFailName,          /* failed module name */
          sizeof(szFailName),                    /* size of buffer     */
          "myputs",                              /* name of DLL        */
          &hmod);                                /* module handle here */
 
      if (ulErr)
          DosExit(EXIT_PROCESS, 1);
 
      ulErr = DosQueryProcAddr(hmod,    /* DLL module handle           */
          0,                            /* function ordinal value      */
          "myPuts",                     /* function name               */
          (PFN *) &myPuts);             /* address of function pointer */
 
      if (!ulErr) {
 
          /* We can use the function now. */
 
          myPuts("does it work?");
 
          DosFreeModule(hmod);                 /* frees the DLL module */
     }
 }

Protected Memory Use

OS/2 provides shared library support in the form of 32-bit DLLs. All 32-bit dynamic links or APIs are called using near CALL or RET instructions, so the cost of making dynamic-link calls should be significantly less than the cost of making the comparable calls in the 16-bit version of OS/2, where a far CALL is required. The DLLs execute in the context of the caller.

All 32-bit DLLs are mapped into the appropriate shared memory region of the requesting processes at load time and execute at ring 3 without IOPL. This model's protection characteristics correspond closest to the ring 3 dynamic-linking model in the 16-bit version of OS/2. The following figure shows how 32-bit DLLs are implemented in the linear memory model of OS/2.

  4G┌──────────────┐
    │ System Area  │
512M├──────────────┤
    │              │
    ├──────────────┤
    │  32 Bit DLL  │
    ├────────┬─────┤
    │     ▲  │     │
    │     │  │     │
    │ Call│  │Ret  │
    │Near │  │Near │
    │     │  │     │
    │     │  │     │
    │     │  ▼     │
    ├─────┴────────┤
    │  32 Bit EXE  │
    ├──────────────┤
    │              │
   0└──────────────┘

A 32-Bit DLL

However, since 32-bit EXE programs can address the entire address space with a 32-bit offset, it is easier for a 32-bit application programmer to potentially cast a bad pointer to data in the shared region than in the 16-bit segmented addressing scheme. Since many subsystems have semaphores and other shared data structures in the shared region, the potential for an inadvertently errant application to affect another process sharing a subsystem becomes an issue in the flat environment. Therefore, OS/2 provides a mechanism for DLLs to protect their critical shared global data regions from 32-bit EXEs. This mechanism prevents a thread in one process from potentially affecting other processes using the same resources (subsystems), or potentially taking down the entire workstation if the compromised subsystem is critical (such as PM).

OS/2 enables existing 16-bit DLLs and new 32-bit DLLs to get their shared global data allocated into a single protected region that is not accessible by 32-bit EXEs, thereby achieving a level of protection. There is no provision for protecting DLLs from each other or from threads executing 16-bit EXE modules. The MEMMAN CONFIG.SYS line supports a "PROTECT/NOPROTECT" option, as follows, for enabling or disabling memory protection:

MEMMAN=SWAP,PROTECT

If neither PROTECT nor NOPROTECT is specified, the default is protection enabled (PROTECT).

When protection is enabled, the memory manager reserves a 64M region of the linear address space below the 512MB line; this is called the protected region. Protected objects are allocated within the protected region. The following types of memory are considered protected:

DLL Global Data
Global data that is part of the DLL image when loaded. This is only global shared data, not instance data. Although DLL code is shared, it is not allocated in the protected region since it is read-only.
DLL Run-Time Shared Data
Global data that is allocated at runtime by a thread executing in DLL code that is a protected API. This includes 16- and 32-bit, named and unnamed, shared memory, and shared memory allocated with DosAllocSeg with the share flag set.

The DS value that is used for the user address space (FlatDS) no longer references a descriptor with a 512MB limit. Instead the system exports another DS value for the user address space called the ProtDS that does have the 512MB limit-the FlatDS limit is reduced by the size of the protected region. When a 32-bit EXE is executing, it runs with the FlatDS and is unable to access protected objects created by 16-bit, 32-bit, or 16- and 32-bit DLLs. If the thread calls a 16-bit DLL API entry point, the DLL will have addressability to the protected region through the LDT. If the thread calls a 32-bit DLL entry point that is protected, the 32-bit DLL entry point contains code to switch to the ProtDS so that the protected region is accessible-the 32-bit DLL switches back to the FlatDS before completing service. A switch on the C compiler is used to generate the code sequence as shown in the following figure.

DLLAPI  proc    push    ds
   push    es
   mov     dx, seg FLAT:DGROUP
   mov     ds, dx
   mov     es, dx

   .....

   pop     es
   pop     ds
   ret
DLLAPI  endp

Although SS is not loaded with the ProtDS, a subsystem that switches stacks to a protected stack must write some assembler code to change ESP-thus the subsystem also should set up SS to be the ProtDS when performing the stack switch.

When protection is not enabled, FlatDS=ProtDS and the code still works the same.

Note: The system currently is not sensitive to whether parameters are being validated relative to the FlatDS or the ProtDS when ring 0 kernel APIs are called. Also the 32-16 thunks do not probe 32-bit parameters before converting them and passing them to a 16-bit DLL.

The grouping of protected allocations can be enabled or disabled on a per DLL basis. For 32-bit DLLs, the Linker uses the PROTECT parameter in the DEF file to provide protection information in the DLL's module flags to the loader. All 16-bit modules requiring protection must be specified with the new PROTECT16 CONFIG.SYS parameter.

PROTECT16=DLLNAME1,DLLNAME2,...,DLLNAMEX

Notice that the DLL suffix is not required. Only DLL files can get the protection.

DLL Side Effects

Dynamic link routines are not processes. They run on the thread of the calling process and therefore do not own resources. Any resource that they obtain or use is owned by the calling process. Authors of DLLs should be careful not to needlessly allocate resources until the resource is required by the calling process to perform the requested function. They also should free the resource as soon as that resource is no longer required.

A dynamic link routine that obtains and uses resources should attempt to minimize the use of a process's resources. For example, stack space should be conserved. If an application redirects file handle 5 and calls a DLL entry that expects file handle 5 to be an open handle to an associated device driver, unpredictable results can occur.

If the routine opens an abundance of file handles, it might consider increasing the maximum number of file handles, so that the process maximum is not exceeded. However, increasing the maximum number of file handles for a process also increases the maximum number of file handles for all processes created by the current process. This will cause additional memory to be consumed and could cause problems for an application that assumes a limit of 20 file handles. Also, it should be noted that applications have the ability to redirect file handles.

Dynamic link routines also should not make system calls that affect the calling process environment. If a DLL changes a process's current directory, another thread running under the same process could fail a file I/O call if it assumes a given working directory.

Applications and DLLs should not make calls to other DLLs, including system DLLs, within a critical section. Since DLLs can use semaphores to synchronize threads within a process or between processes, calling a DLL within a critical section could cause application deadlocks. This would occur if the DLL requests a semaphore on behalf of the calling thread and another thread within the process owns the semaphore. Because the calling thread is in a critical section and is the only thread within the process that is permitted to execute, the semaphore will never be freed, causing a deadlock.

Summary

There are two types of linking: static and dynamic. Static linking enables a program's code and data to be contained in a single executable file, enabling the system to load it all into memory at once. Dynamic linking permits several applications to use a single copy of an executable module, since the executable module is completely separate from the applications that use it.

The advantages of dynamic linking are:

  • Reduced memory requirements
  • Simplified application modification
  • Flexible software support
  • Transparent migration of functions
  • Multiple programming language support
  • Application controlled memory usage.

OS/2 provides two types of dynamic linking: load-time and run-time. In load-time dynamic linking, an application is linked with a library file that contains a record that describes where the routine can be found instead of a file that contains the code for the routine. The DLL can be loaded as soon as a process accesses the DLL or when needed. In run-time dynamic linking, the EXE for an application does not contain a record describing where the external routines can be found. Instead, the application explicitly tells OS/2 when to load and free the dynamic link module.

DLL data can be shared or not shared by all processes that use it.