This article contains the following executables: SFEDIT.ARC
Paul is a software engineer at Claris and is the coauthor of the Turbo C++ DiskTutor (Osborne/McGraw-Hill). He can be reached through the DDJ office.
Under a true protected-mode system, an ill-behaved application cannot corrupt the rest of the system. Windows, however, is not a pure protected-mode environment. Windows lives on top of real-mode MS-DOS. So, how does Windows communicate with DOS, the BIOS, network drivers, and other real mode services that are unaware of protected mode? That--and how you go about making undocumented DOS calls--is the focus of this article.
To examine these concepts, I used Microsoft's QuickC for Windows to write a DLL that provides standard file dialog boxes similar to those found in Windows 3.1. I chose QuickC/Win largely because it is a Windows-hosted environment that provides the essentials of the Windows SDK.
Extending Windows
Hidden within Windows is a DOS Protected-Mode Interface (DPMI) compliant DOS extender. The DOS extender is an "invisible" layer between protected-mode applications and DOS. Protected-mode Windows applications can use the same real-mode services that DOS applications use. The Windows DOS extender accomplishes this by trapping each call to the system. When a Windows program calls DOS, Windows handles the call, switches the processor to real mode, and relays the call to DOS. When the call is complete, the extender code switches back to protected mode and returns control to the application. (See the accompanying text box, "DPMI and the Windows DOS Extender.")
Transparent to the application, the Windows DOS extender must also translate protected-mode buffers to real-mode buffers. For example, to call the DOS Open File function (INT 21h, Function 3Dh), the DOS extender first copies the protected-mode filename to a real-mode addressable buffer before issuing the DOS call. This transparency allows unmodified standard C library functions such as fopen to work in Windows applications.
Standard File Dialog Boxes
As Windows users know, a file dialog box allows the user to select a file from a list of available files, enter a filename directly into an edit box, and to switch drives and directories. Windows' lack of a standardized interface element for selecting files has always been a mystery to me. Unlike the Macintosh, which includes a Standard File Package, each Windows 3.0 application must implement its own version of a file dialog box. (Note that Windows 3.1 will include standard file dialog boxes in COMMDLG.DLL.)
One benefit of standard file dialog boxes is shared code. The Open File and File Save As dialog boxes are common to a large class of applications, so it is natural to put them in a Dynamic Link Library. In a DLL, these dialog boxes are not statically bound to any one application, but are available to any application that wants to provide a user interface for specifying filenames.
SFILEDLG
SFILEDLG is a DLL containing a File Open and a File Save As dialog box. (Because of space considerations, SFILEDLG and other listings are provided electronically. See "Availability" on page 3 for details.) It uses an owner-draw list box to draw bitmaps representing different drive types. The exported function, SFGetFileDlg, is the interface to these dialog boxes. SFileDlgProc, the callback function, processes the Windows messages. When the DLL is loaded, LibMain creates the resources used by the dialog boxes. These resources are discarded when the DLL is removed from the system.
Undocumented DOS Calls
The LibMain initialization routine in SFILEDLG calls GetDriveTypeX to determine the type of drive for each possible drive in the system. The Windows API function GetDriveType recognizes floppy, hard, and network drives. GetDriveTypeX extends GetDriveType by also recognizing RAM, CD-ROM, and SUBST drives. Unfortunately, neither Windows nor DOS provides a one-stop solution to recognize all types of drives. This solution requires a combination of documented and undocumented DOS.
Recall that the Windows DOS extender transparently reflects each DOS call for protected-mode Windows applications. The DOS extender traps many documented system calls. To make undocumented DOS calls, however, Windows applications need to employ their own protected- to real-mode interface.
To make undocumented DOS calls, GetDriveTypeX uses the DPMI Simulate Real-Mode Interrupt facility (INT 31h, Function 0300h), The function DPMI_RealModeInt in Listing One (page 102) works similar to the int86 function supplied by C libraries. The comments in the header of DPMI_RealModeInt document the protocol for DPMI's Simulate Real-Mode Interrupt facility.
CD-ROM Drives
The CD-ROM extensions, MSCDEX, are documented extensions of DOS. These extensions are translated by the Windows DOS extender, so there is no need to provide a real-mode translation. To detect a CD-ROM drive, isCDROM calls INT 2Fh, Function 15OBh. The CX register specifies the drive number, which is 0 for drive A:, 1 for drive B:, 2 for drive C:, and so on. If a CD-ROM drive is detected, the function returns ABABh in register BX.
SUBST
The DOS command SUBST is a TSR that associates a drive letter with a directory. Issuing the DOS command SUBST W: C:\WINDOWS maps all requests for drive W: to the C:\WINDOWS directory. Drives created by SUBST can be recognized using the Current Directory Structure (CDS), an undocumented DOS structure. (See the textbox entitled "The DOS Lists of Lists.") This structure contains a bit flag that indicates whether the drive is a SUBST drive.
In Listing One, isSubstDrive calls the function GetCDS to obtain a real-mode pointer to the CDS array. GetCDS, calls the undocumented DOS function INT 21h, Function 5200h (Get DOS List of lists). Because INT 21h, Function 5200h is not serviced by the Windows DOS extender a Windows application must never call it directly. GetCDS uses DMI_RealModeInt as the interface to this undocumented DOS function. On return, the register pair ES:BX holds a real-mode address of the lists of lists. This address must be converted to a protected-mode address with PMODE_ADDR. PMODE_ADDR takes a selector created by the Windows API function AllocSelector, a real-mode address, and returns a protected-mode address.
From the protected-mode address of the List of Lists, GetCDS finds the real-mode address of the CDS for the specified drive. Before this address can be used, isSubstDrive calls on PMODE_ADDR again to convert it to a protected-mode address. Finally, the flag field in the CDS is checked to determine if the drive is a SUBST drive.
RAM Drives
Like SUBST drives, there is no official method for detecting RAM drives. The function isRamDrive uses the assumption that RAM drives have only one File Allocation Table (FAT) instead of the usual two. If a drive is not recognized by any of the above drive types, GetDriveTypeX tries isRamDrive to see if it has only one FAT. The CDS contains a pointer to the drive's Drive Parameter Block (DPB), which holds the number of FATs used by the drive. Like the List of Lists, the CDS is an internal DOS structure and thus contains real-mode pointers. Again, a conversion to protected-mode pointer using PMODE-ADDR is required before the DPB can be dereferenced. (Note: The RAM drive detection method used by the Windows File Manager appears to check the disk volume label for the string "MS-RAMDRIVE.")
True Pathnames
Network and SUBST drives are aliases to another device or pathname. LibMain calls GetCanonicalPath to resolve the true paths of each drive. GetCanonicalPath calls the undocumented DOS INT 21h Function 60h for this purpose. DPMI_RealModeInt is called to invoke INT 21h Function 60h, which asks for a pointer to a relative path string in DS:SI and returns the canonical fully qualified string in ES:DI. These pointers must be real-mode addressable. GetCanonicalPath uses the Windows API function GlobalDOSAlloc to create this buffer. GlobalDOSAlloc returns a protected-mode selector in the high word and a real-mode segment in the low word of a buffer in the first megabyte of RAM. This buffer is used in GetCanonicalPath to transfer area for the real-mode interrupt. The transfer buffer is returned to the precious real-mode memory pool using GlobalDOSFree.
Real-Mode Buffers
SFILEDLG demonstrates two cases of a real- to protected-mode buffer transfer. These solutions can be applied more generally to accessing any absolute real-mode address such as TSRs, network areas, BIOS data area, and so on. In GetDriveTypeX, the protected-mode application must access existing buffers (DOS List of Lists, Current Directory Structure, Drive Parameter Block) created by real-mode programs. GetDriveTypeX calls AllocSelector and SetSelectorBase to map these buffers into the protected-mode address space. In Get CanonicalPath, the interrupt asks the calling routine for a buffer to fill. GlobalDOSAlloc provides the real-mode addressable buffer and a selector the protected-mode program uses to access it.
Also, the file dialog boxes in SFILEDLG keeps track of the current filename and file type. Typically, this data would be kept in static or global variables. However, precautions must be taken with static and global data in DLLs. At most, one instance of a DLL is loaded by Windows, even if many applications are using it. Consequently, global and static data are stored in a DLL's single data segment, which is shared among all applications that call it.
To associate a separate data buffer with each instance of a file dialog box, SFILEDLG uses window properties. A property list contains a list of words, indexed by strings. Each window may have its own property list. Unlike window extra bytes, properties may be assigned to a window even if that window was not registered by the application. SFILEDLG allocates a global SFDlg structure containing the dialog state. The handle to this structure is then placed in the dialog box's property list.
Using SFILEDLG
SFEDIT (provided electronically) is a text editor that uses SFILEDLG.DLL for selecting filenames for opening and saving. The application itself is not particularly interesting; the editor is a multiline EDIT control which provides no additional functionality over Windows' own NOTEPAD. But SFEDIT shows how easily SFIELDLG.DLL can be incorporated into any application needing file dialog boxes.
Also provided electronically is SFILEDLG.MAK, a QuickC for Windows 1.0 project file for making SFILEDLG.DLL and the bitmap resource files referenced in SFILEDLG.DLL. QuickC/Win will automatically create an import library, SFILEDLG.LIB, which is used by SFEDIT.MAK and linked with the application SFEDIT.EXE. (With minor modifications, SFILEDLG.DLL and SFEDIT.EXE can be created with Microsoft C or Borland C++.)
A Word About Tools
QuickC for Windows offers a less painful entry into the world of Windows development than Microsoft C 6.0 and the Windows Software Development Kit. QuickC/Win offers an integrated editor, compiler, and debugger in a completely Windows-hosted environment. This is undoubtedly a more convenient environment than the DOS-hosted Microsoft C 6.x Programmer's WorkBench. Furthermore, QuickC/Win is packaged with the essential elements of the Windows SDK. A dialog editor, image editor, and CASE tool is included. Code-View for Windows is not included, but I found the integrated Windows-hosted debugger to be generally very capable. However, when debugging user-interface code, QuickC/Win has a tendency to grab input focus from the application being debugged. Two other SDK utilities that I missed in QuickC/Win are SPY and HEAPWALKER, which Microsoft is now selling separately.
QuickC/Win is an evolutionary tool for Windows software development. It is not Visual Basic for C programmers and it doesn't compile C++ code. Microsoft has revised its QuickC for DOS compiler and packaged it with existing SDK tools and documentation. It's a good package, and will entice many users to delete Microsoft C 6.0 and the Windows SDK from their hard drives.
References
Petzold, Charles. Programming Windows. Redmond, Wash.: Microsoft Press, 1990.
Schulman, Andrew, Raymond J. Michels, Jim Kyle, Tim Paterson, David Maxey, and Ralf Brown. Undocumented DOS. Reading, Mass.: Addison-Wesley, 1990.
DOS Protected Mode Interface Specification, Santa Clara, Calif.: Intel, 1991.
Windows INT 21h and NetBios Support for DPMI, Redmond, Wash.: Microsoft Press, 1990.
DPMI and the Windows DOS Extender
The DOS Protected-Mode Interface (DPMI) allows protected-mode programs to access DOS by specifying a set of low-level functions that manage extended memory and call real-mode programs. The services provided by DPMI are not transparent as in the case of DOS extenders. In fact, DPMI is not principally designed to be called by applications. The DPMI "client" is intended to be DOS extenders, such as the one in Windows. The DOS extender is responsible for translating protected-mode system calls for the application.
Generally, Windows will translate any software interrupts that are register based (that is, don't pass pointers, stack parameters, and use segment registers). The reason is that, in protected-mode Windows, segment registers are "selectors", which contain offsets into the Local Descriptor Table (LDT). The actual linear address of the data segment is stored in the LDT.
Most DOS functions work under Windows 3.0; even those that use pointers or segment registers. Windows provides the necessary protected-mode pointer translations for DOS functions. However, the Windows DOS extender either does not support or supports in limited fashion the DOS interrupts shown in Figure 1.
Figure 1: The Windows DOS Extender either does not support or supports in limited fashion these DOS interrupts.
INT Description ------------------------------------- 20h Terminate Program 25h Absolute Disk Read 26h Absolute Disk Write 27h Terminate And Stay Resident INT 21h functions not supported Function Description ------------------------------------- 00h Terminate Process 0Fh Open File with FCB 10h Close File with FCB 14h Sequential Read 15h Sequential Write 16h Create File with FCB 21h Random Read 22h Random Write 23h Get File Size 24h Set Relative Record 27h Random Block Read 28h Random block Write INT 21h functions supported with restrictions Function Description ----------------------------------------------------------------------- 21h, 35h Set and Get Interrupt Vector Software interrupts issued in real mode are not reflected to the protected mode interrupt handlers, except for INT 23h, INT 24h, and INT 1Ch. 44h IOCTL Subfunctions 02h, 03h, 04h,, and 05h, used to receive data from and send data to a device; the transfer buffer must be less than 4K unless it is below the 1-Mbyte boundary. Subfunction OCh minor code 4Ah, 4Ch, 4Dh, 6Ah, and 6Bh; the code-page functions are not supported. 38h, 65h Get Country Data and Extended Country DWORD pointers returned by these functions are real-mode addresses.
Unlike extenders, which try to be fully DOS compatible, Windows does not translate many undocumented DOS functions. If necessary, a combination of direct DPMI calls and Windows segment functions may be used. Windows 3.0 supports version 0.9 of the DPMI specification. (The DPMI 1.0 specification can be obtained from the Intel Literature Center, P.O. Box 58065, Santa Clara, CA 95052, or by calling for the Intel Reference Literature Packet # JP 26, 800-548-4725.)
Microsoft recommends that Windows applications not call any DPMI functions other than those listed in Figure 2. The Windows kernel contains a number of functions that may be used instead of calling DPMI directly. Not all of these functions are documented. Some of the more useful ones, along with their equivalent DPMI functions, are listed in Figure 3.
Figure 2: DPMI functions that may be safely called by a Windows application
DPMI (INT 31h) Functions Description ------------------------------------------------------------- 0200h Get Real-Mode Interrupt Vector 0201h Set Real-Mode Interrupt Vector 0300h Simulate Real-Mode Interrupt 0301h Call Real-Mode Procedure with Far Return Frame 0302h Call Real-Mode Procedure with IRET Frame 0303h Allocate Real-Mode Call-Back Address 0304h Free Real-Mode Call-Back Address
Figure 3: Subset of DPMI functions supported within the Windows API
DMPI (INT 31h) Functions Decription Windows API Function --------------------------------------------------------------- 0000h Allocate Local Descriptor AllocSelector 0001h Free Local Descriptor FreeSelector 0006h Get Segment Base Address GetSelectorBase 0007h Set Segment Base Address SetSelectorBase 0008h Set Segment Limit SetSelectorLimit 0100h Allocate DOS Memory Block GlobalDOSAlloc 0101h Free DOS Memory Block GlobalDOSFree
The DOS List of Lists
MS-DOS maintains a list of system variables near the beginning of its kernel's data segment. This undocumented area, coined the "DOS List of Lists," is a gateway for accessing many other undocumented structures of DOS. Among other secrets, the List of Lists contains: the address of the first memory control block; information on the BUFFERS and LASTDRIVE commands in CONFIG.SYS; and the address of the System File Tables, which maintain the state of open files in the system. A pointer to the List of Lists can be obtained by calling INT 21h, Function 52h. The address of the List of Lists is returned in the ES:BX register pair.
Among the undocumented structures in the List of Lists is the address for the Current Directory Structure (CDS), located at offset 23. The CDS was introduced as part of the network enhancements made in DOS 3.0. In addition to working with network files, the CDS is used for manipulating foreign file systems. There is one CDS for each possible drive on the system. For example, if LASDRIVE = Z is set in the CONFIG.SYS file,then DOS will maintain an array of 26 Current Directory Structures. The size of each CDS varies depending on the version of DOS. In DOS 3.x, the size of the CDS is 81 bytes. From DOS 4.0 and up, the size of the CDS is 88 bytes. But this is an undocumented internal data structure, so the size and the contents of the CDS are not guaranteed.
--P.C.
_MAKING UNDOCUMENTED DOS CALLS FROM WINDOWS 3_ by Paul Chui[LISTING ONE]
<a name="006c_0018"> #define NOCOMM #include <windows.h> #include <dos.h> #include <memory.h> #include "getdrvx.h" #define CDS_SUBST (0x1000) #define DOS3_CDS_SIZE 81 #define DOS4_CDS_SIZE 88 #define CDS_PATH 0x00 #define CDS_FLAGS 0x43 #define CDS_DPB 0x45 #define DPB_FATS 8 #define MK_FP(seg,ofs) ((void far *) (((unsigned long)(seg) << 16) | (unsigned)(ofs))) #define _DS GetDS() /* Documented Windows functions not declared in WINDOWS.H */ DWORD FAR PASCAL GlobalDosAlloc(DWORD dwBytes); WORD FAR PASCAL GlobalDosFree(WORD wSelector); /* Undocumented Windows functions */ VOID FAR PASCAL SetSelectorBase(WORD wSelector, DWORD dwBase); VOID FAR PASCAL SetSelectorLimit(WORD wSelector, DWORD dwLimit); /* Global Variables */ WORD _wSelector; // a data selector /* Types */ // DOS list of lists structure (for DOS 3.1 or better) typedef struct { BYTE x[22]; // (don't care) BYTE far* cds; BYTE xx[7]; // (don't care) BYTE lastdrive; // ... the rest of DOS List of Lists } DOSLISTS; // DPMI real-mode call structure typedef struct { DWORD edi, esi, ebp, reserved, ebx, edx, ecx, eax; WORD flags, es, ds, fs, gs, ip, cs, sp, ss; } REALMODECALL; int GetDosVersionMajor(void) { int dosver; _asm mov ah, 0x30; _asm int 0x21; _asm mov dosver, ax; return (BYTE)dosver; } unsigned GetDS(void) { _asm mov ax, ds; return; // return value in AX } /************************************************************************* BOOL DPMI_RealModeInt(int intno, REALMODECALL far* r) PURPOSE: DPMI simulate real-mode interrupt function. The following is an excerpt from the DPMI Specification: PARAMETERS: int intno real-mode interrupt to simulate REALMODECALL far* r RETURNS: TRUE if interrupt is successful, FALSE on error NOTES: The following is an excerpt from INTEL's DPMI specs: To Call AX = 0300h BL = Interrupt number BH = Flags Bit 0 = 1 resets the interrupt controller and A20 line Other flags reserved and must be 0 CX = Number of words to copy from protected mode to real-mode stack ES:(E)DI = Selector:Offset of real-mode call structure Returns If function was successful: Carry flag is clear. ES:(E)DI = Selector:Offset of modified real-mode call structure If function was not successful: Carry flag is set. *************************************************************************/ BOOL DPMI_RealModeInt(int intno, REALMODECALL far* r) { intno &= 0x00FF; // reset high byte (flags) _asm { push di mov bx, intno // flags | real-mode interrupt to call mov cx, 0 // don't copy anything from protected mode stack les di, r // es:di -> real-mode call structure mov ax, 0x0300 // DPMI simulate real-mode interrupt function int 0x31 pop di jc error } return TRUE; error: return FALSE; } /************************************************************************** BYTE far* PMODE_ADDR(WORD wSel, BYTE far* RMODE_ADDR) PURPOSE: Get the Current Directory Structure PARAMETERS: WORD wSel A valid selector BYTE far* RMODE_ADDR A real mode address RETURNS: a protected-mode address of RMODE_ADDR NOTES: The base address of selector wSel is set to the segment of RMODE_ADDR. **************************************************************************/ BYTE far* PMODE_ADDR(WORD wSel, BYTE far* RMODE_ADDR) { SetSelectorBase(wSel, (DWORD) FP_SEG(RMODE_ADDR) << 4); return MK_FP(wSel, FP_OFF(RMODE_ADDR)); } /************************************************************************** BYTE far* GetCDS(int nDrive) PURPOSE: Get the Current Directory Structure PARAMETERS: int nDrive The drive to retrieve RETURNS: a real-mode pointer to the Current Directory Structure of drive nDrive. ***************************************************************************/ BYTE far* GetCDS(int nDrive) { DOSLISTS far* doslists; REALMODECALL r; // Get DOS list of lists (INT 21h, Function 52h) _fmemset(&r, 0, sizeof(REALMODECALL)); r.eax = 0x5200; if (!DPMI_RealModeInt(0x21, &r)) return NULL; // Pointer to DOS list of lists is returned in ES:BX doslists = (DOSLISTS far*) PMODE_ADDR( _wSelector, MK_FP(r.es, LOWORD(r.ebx)) ); if (GetDosVersionMajor() < 4) return doslists->cds + (nDrive * DOS3_CDS_SIZE); else return doslists->cds + (nDrive * DOS4_CDS_SIZE); } /************************************************************************* BOOL isCDROM(int nDrive) PURPOSE: Tests if nDrive is a CD ROM drive PARAMETERS: int nDrive The drive to test RETURNS: TRUE if nDrive is a CD ROM **************************************************************************/ BOOL isCDROM(int nDrive) { WORD saveAX, saveBX; _asm mov cx, nDrive; _asm mov ax, 0x150B; _asm int 0x2F; _asm mov saveAX, ax; _asm mov saveBX, bx; return saveBX == 0xADAD && saveAX != 0; } /************************************************************************** BOOL isSubstDrive(int nDrive) PURPOSE: Tests for drives created using the DOS SUBST command PARAMETERS: int nDrive The drive to test RETURNS: TRUE if nDrive is a SUBST drive ***************************************************************************/ BOOL isSubstDrive(int nDrive) { BYTE far* cds; // Current Directory Structure WORD cds_flags; cds = PMODE_ADDR(_wSelector, GetCDS(nDrive)); cds_flags = *(WORD far*)(cds+CDS_FLAGS); if (cds_flags & (CDS_SUBST)) return TRUE; return FALSE; } /************************************************************************** BOOL isRamDrive(int nDrive) PURPOSE: Tests for Ram drives PARAMETERS: int nDrive The drive to test RETURNS: TRUE if nDrive is a ram drive NOTES: This function tests to see if the drive has only one FAT. It is assumed that if this is true, then the drive must be a RAM drive. ***************************************************************************/ BOOL isRamDrive(int nDrive) { BYTE far* cds; // Current Directory Structure BYTE far* dpb; // Drive Parameter Block cds = PMODE_ADDR(_wSelector, GetCDS(nDrive)); dpb = *(BYTE far* far*)(cds+CDS_DPB); dpb = PMODE_ADDR(_wSelector, dpb); if (*(dpb+DPB_FATS) == 1) return TRUE; return FALSE; } /*************************************************************************** WORD FAR PASCAL GetDriveTypeX(int nDrive) PURPOSE: Determines drive type PARAMETERS: int nDrive The drive number. 0 = A:, 1 = B:, 2 = C:, 3 = D:, ... RETURNS: DRIVE_UNKNOWN Unknown drive type DRIVE_NOTEXIST Drive does not exist DRIVE_REMOVE Removable (floppy) drive DRIVE_FIXED Fixed (hard) drive DRIVE_REMOTE Remote (network) drive DRIVE_CDROM CD ROM drive DRIVE_RAM Ram drive DRIVE_SUBST SUBST drive ***************************************************************************/ WORD FAR PASCAL GetDriveTypeX(int nDrive) { WORD wDriveType; // Get a new selector, using the current DS as the prototype _wSelector = AllocSelector(_DS); SetSelectorLimit(_wSelector, 0xFFFF); wDriveType = GetDriveType(nDrive); if (isCDROM(nDrive)) wDriveType = DRIVE_CDROM; else if (wDriveType != DRIVE_REMOTE && isSubstDrive(nDrive)) wDriveType = DRIVE_SUBST; else if (isRamDrive(nDrive)) wDriveType = DRIVE_RAM; FreeSelector(_wSelector); return wDriveType; } /*************************************************************************** BOOL FAR PASCAL GetCanonicalPath(LPSTR lpszRelPath, LPSTR lpszTruePath) PURPOSE: Resolve path string to canonical path string PARAMETERS: LPSTR lpszRelPath Relative path string or directory name LPSTR lpszTruePath Destination for canonical fully qualified path RETURNS: TRUE if successful ***************************************************************************/ BOOL FAR PASCAL GetCanonicalPath(LPSTR lpszRelPath, LPSTR lpszTruePath) { BOOL retval; DWORD dw; WORD wSelector; WORD wSegment; LPSTR lpszDosBuf; REALMODECALL r; dw = GlobalDosAlloc(128); if (dw == NULL) return FALSE; wSelector = LOWORD(dw); wSegment = HIWORD(dw); lpszDosBuf = MK_FP(wSelector, 0); _fmemcpy(lpszDosBuf, lpszRelPath, 128); r.eax = 0x6000; r.ds = wSegment; r.esi = 0; r.es = wSegment; r.edi = 0; retval = DPMI_RealModeInt(0x21, &r); _fmemcpy(lpszTruePath, lpszDosBuf, 128); GlobalDosFree(wSelector); return retval; }
Copyright © 1992, Dr. Dobb's Journal