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.