18.3.2 Defining Procedures and Data

Procedures and data in DLLs can be either global (available to the client process) or local (used only by the DLL). To create a global data item, make sure that it is public:

EXTERNDEF dllvar

.DATA

dllvar WORD 0

The variable must then be exported in a module-definition file, as shown in Section 18.4.1, “Writing the Module-Definition File.” When executable files other than the DLL access the variable, they must treat it as far data, as in the following example:

mov ax, SEG dllvar

mov es, ax

mov bx, es:dllvar

An exported procedure (often called a dynamic-link procedure) must follow these rules:

It must be declared far and public. The MASM keyword EXPORT does both of these.

The procedure should initialize DS upon entry (unless you are not going to be accessing any static near data).

Data pointers in the parameter list should be far.

The easiest way to realize most of these requirements is to use the EXPORT keyword and LOADDS in the procedure's prologuearg list (see Section 7.3.8). LOADDS generates instructions to save DS and load it with the value of the DLL's data segment. The EXPORT keyword makes the procedure FAR and PUBLIC, overriding the memory model. You may also need to use FORCEFRAME, which instructs the assembler to generate a stack frame even if there are no parameters or locals.

The example DLL used in the chapter, CSTR.DLL, illustrates how DLLs can be shared by several processes. The procedures in the DLL write a string and keep track of the number of times the string is written. When more than one process uses the DLL, they all increment the global variable GCount, but each process increments its own private instance of the PCount variable.

The only initialization code this DLL needs is code to set up the exit code. The next section shows how to write a module-definition file to create an import library and how to create a DLL from this code.

The code for the CSTR.DLL example looks like this:

.MODEL small, pascal, os_os2, farstack

.286

INCL_NOCOMMON EQU 1

INCL_DOSPROCESS EQU 1

INCL_VIO EQU 1

INCLUDE OS2.INC

INCLUDELIB OS2.LIB

.DOSSEG

VioWrtCStr PROTO FAR PASCAL, pchString:PCH, hv:HVIO

GetGCount PROTO PASCAL

GetPCount PROTO PASCAL

CStrExit PROTO FAR

.STACK

.DATA ; Default segment is SHARED

GCount WORD 0 ; Count of all calls

@CurSeg ENDS

PRIVDAT SEGMENT WORD ; Private segment is NONSHARED

PCount WORD 0 ; Count of all this process

; calls to VioWrtCStr

PRIVDAT ENDS

.CODE

.STARTUP

pusha

; Initialization goes here. In this case, the only

; initialization is setting up the exit behavior.

INVOKE DosExitList, EXLST_ADD, CStrExit

INVOKE DosExitList, EXLST_EXIT,0

popa

retf

VioWrtCStr PROC FAR PASCAL EXPORT <LOADDS> USES cx di si,

pchString:PCH,

hv:HVIO

sub al, al ; Search for zero

mov cx, 0FFFFh ; Set maximum length

les di, pchString ; Load pointer

mov si, di ; Copy it

repne scasb ; Find null

.IF zero? ; Continue if found

sub di, si ; Calculate length

xchg di, si ; Restore address and save length

INVOKE VioWrtTTy, ; Let OS/2 do output

es:di, ; Address of string

si, ; Calculated length

hv ; Video handle

inc GCount ; Count as one of total calls

ASSUME DS:PRIVDAT

mov ax, PRIVDAT

mov ds, ax

inc PCount ; Count as one of process calls

ASSUME DS:DGROUP

sub ax, ax ; Success

.ELSE

mov ax, 1 ; Error

.ENDIF

ret

VioWrtCStr ENDP

GetGCount PROC FAR PASCAL EXPORT <LOADDS, FORCEFRAME>

mov ax, GCount

ret

GetGCount ENDP

GetPCount PROC FAR PASCAL EXPORT <LOADDS, FORCEFRAME> USES ds

ASSUME DS:PRIVDAT

mov ax, PRIVDAT

mov ds, ax

mov ax, PCount

ASSUME DS:NOTHING

ret

GetPCount ENDP

.DATA

szOut BYTE 13, 10, "Exiting DLL...", 13, 10, 0

.CODE

CStrExit PROC FAR <LOADDS, FORCEFRAME>

INVOKE VioWrtCStr,

ADDR szOut,

0

INVOKE DosExitList, EXLST_EXIT, 0

CStrExit ENDP

END

These generated code for the VIOWrtCStr procedure follows. The code marked with asterisks is generated by the assembler.

VioWrtCStr PROC FAR PASCAL EXPORT <LOADDS> USES cx di si,

pchString:PCH,

hv:HVIO

0000 55 * push bp

0001 8B EC * mov bp, sp

0003 1E * push ds

0004 B8 ---- R * mov ax, DGROUP

0007 8E D8 * mov ds, ax

0009 51 * push cx

000A 57 * push di

000B 56 * push si

.

. ; Procedure code here

.

ret

000C 5E * pop si

000D 5F * pop di

000E 59 * pop cx

000F 1F * pop ds

0010 C9 * leave

0011 CA 0006 * ret 00006h

0014 VioWrtCStr ENDP

Summary: The DLL should establish its own data segment.

The DLL should change DS in this manner because each client program has its own private version of DGROUP. When a program calls your dynamic-link procedure, DS points to the program's data area, not yours. The solution is to initialize DS so that it points to your own default data area.

However, one side effect of this approach is that it alters DS so that it no longer is equal to SS. Consequently, all data pointers in the parameter list must be far pointers, even if the data was stack data or near data.