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

by John Keenleyside

The last volume of The Developer Connection News showed why OS/2 APIs are provided through DLLs, and how you can build 32-bit DLLs easily using the C Set++ compiler. In this volume we answer such questions as should the data in my DLL be private or shared?, how do I perform exception handling in my DLL?, and what is a resource DLL and how do I use it?

Private or Shared Data
You must always mark the code objects in a DLL with the READONLY and SHARED attributes. That is, applications using the DLL share one copy of the code in that DLL. However, data in a DLL can either be shared or private to each application using the DLL. In fact, some data can be shared, while other data remains private. However, the granularity for data sharing is at the object level. That is, to make some data shared and other data private, the data must be in two separate segments.

Most PC compilers usually create DGROUP (the automatic data segment). This group can contain initialized and uninitialized data, as well as constants. It's the default location of data declared in your source files. In the module definition file for a DLL, you can specify whether DGROUP should be shared or a new instance created for every process using the DLL.

The DATA keyword followed by SINGLE or MULTIPLE specifies how DGROUP is to be treated for every process that uses the DLL. An additional keyword (SHARED or NONSHARED) on the line containing the DATA keyword specifies the default sharing characteristics of all data segments.

If you are using the C Set++ compiler to build a DLL where the runtime libraries are going to be statically linked to it, the module definition file for the DLL must contain the lines LIBRARY INITINSTANCE TERMINSTANCE and DATA MULTIPLE NONSHARED. This tells the linker to build a DLL where the code is shared but a separate copy of the data exists for each process using the DLL. This method uses a different library environment for each process.

To add shared data to a DLL where the default for data is nonshared, put the data to be shared into a separate segment, so it will not end up in the DGROUP object within the DLL module. The C Set++ compiler supports #pragma data_seg. This statement tells the compiler to put any data declared after this pragma into the named segment.

Listing 1 shows how to declare a counter variable to be put into a data segment called SharedData. It also provides a small application source file that you can compile using C++. It calls the IncandPrint function in the DLL whenever a 1 is entered. If you run this application program in multiple OS/2 windows simultaneously, the multiple processes using the DLL share the counter variable.

/* From SHAREIT.DEF */ ''Listing 5. Declaring a Counter Variable''

Basing Your DLLs
The OS/2 2.x linker provides the /BASE:n option to increase the loading of large executable modules and DLLs. (n represents the preferred load address for a module). The linker applies internal relocation records, assuming that the first object of the module will be loaded at the preferred address and that each additional object will be loaded at the next available multiple of 64KB. If all the module's objects can be loaded at the preferred address, the loader does not have to apply any internal relocation records, resulting in the module being loaded faster.

If the module is an executable, the linker can discard the internal relocation records after it has applied them, because an executable is always loaded at 64KB. However, if the module is a DLL, no guarantee exists that the preferred address for the DLL will be available. The linker must leave the relocation records in the DLL. The loader reapplies the relocation records based on the actual address at which the DLL is loaded. The load time of the DLL is the same as if the DLL had been linked without the BASE option.

Use the following steps to determine the DLL's preferred address: /* From MYADDR.DEF */ ''Listing 2. Sample Function that Prints Its Address''
 * 1) Link your DLL with the /MAP option. This provides a road map of your DLL that tells you where external functions are in the DLL. relative to the start of the object.
 * 2) Write a small function, as shown in listing 6, that will print out the address of itself. If the application calls this function, it can determine where this function is in memory.

Using this address and the map file, you can determine the address at which your DLL was loaded; this is the preferred address.

Exception Handling
Remember exception handling when developing DLLs!

You have two choices: provide support within your DLL to handle any exceptions generated while code is being executed in the DLL or leave exception handling up to the application. The former is the preferred method; it allows libraries of functions to be independent from the applications that use them.

An exception handler is a function that performs specific actions based on the type of exception. These actions include doing nothing and passing on the exception to the next exception handler in the chain, terminating the process, or trying to repair the cause of the exception and continuing execution. OS/2 2.x maintains a last-inserted, first-called chain of exception handlers that are called when an exception occurs. Therefore, the exception handler in a DLL must be the last one registered (inserted in to the chain) at the time an exception occurs, while code is being executed within the DLL.

To do this, register the exception handler on the entrance to every function that is exported from the DLL and deregister it just before exiting. The OS/2 APIs to register and deregister exception handlers are DosSetExceptionHandler and DosUnsetExceptionHandler.

You should also consider exception handling with respect to your runtime library. If you are statically linking a compiler's runtime library to your DLL, you should program your DLL's exported functions so that the statically linked library handles any exceptions that occur within the DLL.

With C Set++, use a #pragma handler statement for each exported function. This causes an exception handler within the statically linked runtime library to be registered on entrance to these functions. On exit from these functions, the exception handler is deregistered. This method guarantees that the correct exception handler deals with any exceptions generated within your DLL.

Resource DLLs
PM programs considers resources to be such items as menus, messages, icons, fonts, bitmaps, accelerator tables, help tables, dialog templates, and window templates. PM lets you define resources externally to the code that uses them. The resources either can be bound to the application, or they can be incorporated into a DLL.

If you define and store resources in a DLL, more than one application can use them. You also can make modifications to the resources without affecting the code that uses them.

National language standard requirements also can be handled effectively through the use of resource DLLs. All the text in menus, messages, and help panels are external to the application. This makes it easy to build a resource DLL for each supported language. The installation program for the application can ask the user which language is to be used and installs the correct DLL.

The resource identifiers within each DLL must be identical. DBCS and bidirectional language support might require code changes, but the use of resource DLLs still makes it easier for the application developer to provide multi-language support.

Add resources to a DLL by using the resource compiler (RC.EXE) that comes with the OS/2 2.x operating system. That is, the DLL must exist first; the design of the OS/2 linker requires at least one compiled object to produce a module. This object need not contain any executable code, so an almost empty source file can be used to create the object.

With most compilers, you put a simple declaration into the source file. Otherwise the compiler generates a reference that causes runtime routines to be linked into the DLL. But the C Set++ compiler lets you create an object from an empty source file.

Of course, it's possible to have code in a resource DLL. This makes sense in the case of standard dialog or window procedures that can be shared by multiple applications. Each application can still override or enhance the function provided by the standard procedures by subclassing the window or dialog.

You also need a module definition file to build a resource DLL. All it takes is one line containing LIBRARY. Using the linker, you can build the empty DLL from the object and the module definition file. Then, add one or more resources to the DLL by invoking the resource compiler. Listing 3 shows an example. /* From RESOURCE.DEF */ ''Listing 3. Sample source file, module definition file, and command file used to build an empty DLL''

If your resource DLL contains only resources, you cannot load the DLL automatically at application load time because no reference to the DLL exists for the loader to resolve. You first have to load the DLL containing the resources by using the DosLoadModule API, which returns a handle for the DLL module. After the DLL is loaded, use the DosGetModuleHandle API to obtain a module handle. The handle is then passed as the resource file parameter to APIs such as WinLoadString, which access resources from the DLL.

Conclusion
As with anything, the more you work with DLLs, the faster and easier it will be to build and use them. OS/2 Version 2.x provides advantages of Version 1.x, including a new feature for the termination of 32-bit DLLs. The C Set++ compiler offers further advantages, including features that make initialization, termination and entry point coding automatic.

That's good news whether you're trying to get up to speed on DLLs or looking for new tricks to make it all easier!

References:
Notes:
 * 1) OS/2 Version 2.0 - Volume 4: Application Development GG24-3774-00
 * 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.
 * 2) The referenced book can be found on your accompanying CD-ROM.