NT-Style Threads for MS-DOS

Phar Lap's TNT 386/DOS-Extender makes it possible

Al Williams

Al is the author of DOS and Windows Protected Mode and Commando Windows Programming, both published by Addison-Wesley. Al can be contacted at 310 Ivy Glen Court, League City, TX 77573.


Mention features such as threads, DLLs, and 32-bit memory allocation, and you'll probably think about operating systems such as Windows NT or OS/2 2.1. By using special tools, however, you can take advantage of features such as these in an MS-DOS program. Better still, your source code can be compatible with Windows NT or Win32s.

In this article, I'll examine Phar Lap's TNT 386|DOS-Extender, which provides a subset of the Win32 programming API. You can use many features from the Win32-base API specification, including multiple threads, DLLs, and 32-bit memory allocation.

TNT also allows you to mix standard DOS and BIOS interrupts (and C library functions that use them) with Win32 functions in your programs. Of course, doing so prevents you from compiling your program for native Windows NT or Win32s. However, a mixed program can still run in a DOS box under Windows or NT.

About DOS Extenders

DOS extenders let you write programs for MS-DOS that take advantage of protected-mode features; a typical 386 extender supports access to four gigabytes of virtual memory, for example. Most extenders supply functions that stand in for the common DOS and BIOS interrupts. Of course, you can't use an ordinary 16-bit compiler to generate 32-bit programs. Instead, you need a special 32-bit compiler.

Traditionally, programmers have turned to extenders when the need arose for additional memory. However, the new hybrid extenders (like TNT) offer much more than simple extended-memory management. They mimic other operating systems and provide modern operating-system features for MS-DOS programs.

In this article, I'll develop a DOS program that removes a directory tree. Instead of recursing down the tree, I'll use NT-style threads to process the directories in parallel. The resulting program, XPRUNE, will work with TNT or Windows NT.

About Multithreading

In simple terms, a thread is a piece of code that can execute in parallel with other threads. Threads share global variables, but not local (stack-based) ones. For example, if your program is performing a complex calculation, it might spawn a thread to check for keyboard input. If this thread detects a key press, it aborts the calculation. Ordinarily, you would have to code the calculation to occasionally scan for input. With the thread method, your calculation continues unimpeded until the keyboard-scanning thread aborts it.

Multiple threads can share the same code. That is, a game program might have a function that draws an asteroid on the screen. This function tracks the asteroid's position, color, and spin rate. Although this is one function, it could run in multiple threads to create several asteroids at once.

Windows NT contains a call for creating new threads (CreateThread()), but you won't use it often from inside C/C++ programs. Instead, you should use the C function _beginthread() (declared in PROCESS.H), because only by starting a thread this way can you call C library functions. Figure 1 shows the details of the _beginthread() call.

The _beginthread() function returns a thread handle. You can use this handle to query the thread's status or control the thread. For example, you can use WaitForSingleObject() with a thread handle to wait for the thread to terminate. Under NT, you can also use WaitForMultipleObjects() to wait for multiple threads, but TNT doesn't implement this call; see Table 1.

Using Threads

XPRUNE (see Listing One, page 88) is a TNT program that uses threads. XPRUNE accepts a directory name and removes the directory and all the files and directories under it.

Traditionally, programs like XPRUNE would use simple recursion, deleting each directory tree in sequence. With threads, XPRUNE can delete all the trees in parallel. Of course, you can't remove a directory until all of its contents are gone, so XPRUNE needs some synchronization.

Under DOS, deleting directories in parallel won't help the XPRUNE program go faster--DOS is inherently single-tasking. Still, when the program runs on an NT system (perhaps even a multiprocessor system), things should speed up considerably.

The erase_all() function is the heart of XPRUNE. This function removes a directory and its contents. It sends each file that it finds in the initial directory to del(). When del() detects a subdirectory, it creates a new thread using erase_all().

Since you can only pass one pointer to a thread function, erase_all() accepts a pointer to structure (struct pkt) as an argument. Callers must allocate this pointer using malloc(). In general, the pointer you pass to the thread should not point to a local or global variable; the local variable may go out of scope, and the global variable's value may get changed unexpectedly.

Synchronization

XPRUNE can't remove a directory until it deletes all the files in it. Therefore, erase_all() must wait for all the del() calls (and their threads) to complete before removing the directory.

Creating a list of thread handles and using WaitForSingleObject() is one way to wait for the multiple del() threads to complete, but this is awkward. A better method is to use semaphores (see the accompanying text box entitled, "About Semaphores").

Many multitasking operating systems (including Windows NT) support semaphores. Since TNT does not, I wrote TNTSEM (see Listings Two and Three, page 88). TNTSEM implements semaphores using events--an NT feature that TNT does support. Of course, TNTSEM will work fine under Windows NT, too.

Compiling XPRUNE

You can compile XPRUNE with any TNT-supported compiler. I used Microsoft Visual C++ 32-bit edition (the version that runs under DOS and generates WIN32s code). From the command line, enter:

cl /MT /Zi xprune.c tntsem.c.

To make the resulting executable work with TNT, enter:

rebind xprune.

Other TNT Capabilities and Limitations

As you can see in Table 1, TNT supports numerous NT features. For example, you can create and use DLLs. You can also call DOS and BIOS functions directly. Of course, doing so prevents your program from running under Windows NT (except as a DOS program in the NT DOS box). If you program in C, you won't use many of these functions anyway--the compiler's run-time library uses them. You continue to use fopen(), malloc(), and other familiar calls.

TNT can still produce files compatible with previous versions of Phar Lap's 386 extenders. However, you can't make NT-style calls from these programs. The NT-style programs allow you to mix old and new API calls along with DOS and BIOS function calls--the best of both worlds.

TNT can help you write DOS-extended programs that may also run under Windows NT. However, if you are an experienced NT programmer, you'll quickly be frustrated with some omissions in the TNT API (semaphores, for example). Phar Lap promises to continue adding API functions in subsequent releases.

Also, TNT does not support Unicode. Only the ANSI character-set functions are available. Still, for most programmers this isn't a problem.

If you are coming from a DOS or Windows environment, you'll find TNT's functions quite rich. You can allocate large amounts of memory and perform high-level file operations by making simple TNT calls.

You may find that using features like threading can slow down a program under DOS (this is a problem with DOS, not TNT). However, the time difference is usually not very large. If you compile XPRUNE with the /DSINGLE option, it will not use threads. Then you can compare the time difference for yourself.

Summary

TNT is worth looking at if you need to write DOS-extended programs using widely available tools or programs that run under DOS, Windows, and Windows NT.

If you've tried DOS extenders before, you'll find that 32-bit tools are finally in the mainstream. Using NT development tools means not having to compromise--you'll have full-featured debuggers and libraries.

If you need to support code for DOS and Windows NT, TNT can simplify your life considerably. You may have to roll some of your own API functions for now, but that is easier than trying to do it all from scratch.

References

Win32 Applications Programming Interface. Redmond, WA: Microsoft, 1992.

Williams, Al. DOS and Windows Protected Mode. Reading, MA: Addison-Wesley, 1992.

--. "Your Own Disk Duplication Program." Dr. Dobb's Journal (January, 1992).

--. "Programming with Phar Lap's 286|DOS-Extender." Dr. Dobb's Journal (February, 1992).

--. "Roll Your Own DOS Extender." Dr. Dobb's Journal (October/November, 1990).

Figure 1: (a) _beginthread() call; (b) return value; (c) arguments.

(a)
     unsigned long _beginthread(void (*f)(void *), unsigned stksiz, void * arg);
(b)
     Thread handle (cast to type HANDLE). If an error occurs, the return value is --1.
(c)
     f, pointer to thread function. Function's prototype is void f(void *arg).
     stksiz, stack size in bytes. If 0, use default size.
     arg, void pointer to pass to thread function.

Table 1: NT API functions available for use with TNT.

Console I/O
FlushConsoleInputBuffer
GetCommandLine
GetConsoleCP
GetConsoleMode
PeekConsoleInput
ReadConsole
ScrollConsoleScreenBuffer
SetConsoleCtrlHandler
SetConsoleCursorPosition
SetConsoleModeSetConsoleTitle
WriteConsole
Debugging
ContinueDebugEvent
DebugBreak
OutputDebugString
QueryPerformanceCounter
QueryPerformanceFrequency
ReadProcessMemory
UnhandledExceptionFilter
WaitForDebugEvent
WriteProcessMemory
File Manipulation
CopyFile
CreateDirectory
CreateFile
DeleteFile
DosDateTimeToFileTime
FileTimeToDosDateTime
FileTimeToLocalFileTime
FileTimeToSystemTime
FindClose
FindFirstFile
FindNextFile
FlushFileBuffers
GetCurrentDirectory
GetDiskFreeSpace
GetDriveType
GetFileAttributes
GetFileInformationByHandle
GetFileSize
GetFileTime
GetFileType
GetFullPathName
GetLogicalDrives
GetStdHandle
GetSystemDirectory
GetVolumeInformation
_lclose
LockFile
MoveFile
OpenFile
ReadFile
RemoveDirectory
SearchPath
SetCurrentDirectory
SetEndOfFile
SetFileAttributes
SetFilePointer
SetFileTime
SetHandleCount
SetStdHandle
SystemTimeToFileTIme
UnlockFile
WriteFile
Memory Management
CreateFileMapping
GlobalAlloc
FlobalFree
HeapAlloc
HeapFree
HeapReAlloc
HeapSize
LocalAlloc
LocalFree
LocalReAlloc
LocalSize
MapViewOfFile
UnmapViewOfFile
VirtualAlloc
VirtualFree
VirtualQuery
Miscellaneous
Beep
CloseHandle
DuplicateHandle
GetCPInfo
GetEnvironmentStrings
GetEnvrionmentVariable
GetLastError
GetLocalTime
GetSystemTime
GetTimeZoneInformation
GetVersion
IsDBCSLeadByte
RaiseException
SetEnvrionmentVariable
SetErrorMode
SetLocalTime
SetSystemTime
Sleep
Module Management
FreeLibrary
GetModuleFIleName
GetModuleHandle
GetProcAddress
LoadLibrary
Process/Thread Management
CreateEvent
CreateProcess
CreateThread
DeleteCriticalSection
EnterCriticalSection
ExitProcess
ExitThread
GetCurrentProcess
GetCurrentThread
GetCurrentThreadID
GetExitCodeProcess
GetPriorityClass
GetProcessHeap
GetStartupInfo
GetThreadContext
GetThreadPriority
GetThreadSelectorEntry
InitializeCriticalSection
LeaveCriticalSection
OpenEvent
OpenThread
PulseEvent
ResetEvent
ResumeThread
SetEvent
SetPriorityClass
SetThreadContext
SetThreadPriority
TerminateProcess
WaitForSingleObject

About Semaphores

Semaphores are a way to allow threads to wait for several events to occur. The TNTSEM implementation differs from true NT semaphores, but the ideas are the same. Each semaphore has an associated count. A thread blocking on the semaphore will not execute until the count is 0.

When you create a semaphore (using sem_create()), you specify its initial count. You can modify the count using sem_signal(). When you want to wait for the semaphore to return to 0, call sem_wait().

XPRUNE creates a TNTSEM for each directory it wants to delete. Every subdirectory increments its parent's semaphore. When the semaphore returns to 0, XPRUNE can remove the directory. Without the semaphore, XPRUNE could try to remove the directory before its child threads have erased all of the files in it.

--A.W.

Products Mentioned

TNT 386|DOS-Extender
Phar Lap Software Inc.
60 Aberdeen Avenue
Cambridge, MA 02138
617-661-1510

[LISTING ONE]


/* XPRUNE.C -- (T)NT directory removal -- Williams */

#include <windows.h>
#include <stdlib.h>
#include <process.h>
#include <string.h>
#include "tntsem.h"

/* Arguments to thread function */
struct pkt
  {
  char *dir;
  TNT_SEM sem;
  };
void erase_all(struct pkt *);
void del(char *,WIN32_FIND_DATA *,TNT_SEM);
/* Delete file of directory */
void del(char *dir,WIN32_FIND_DATA *fd,TNT_SEM wait)
  {
  char path[MAX_PATH];
  strcpy(path,dir);
  strcat(path,fd->cFileName);
  if (fd->dwFileAttributes!=FILE_ATTRIBUTE_DIRECTORY)
    {
    if (!DeleteFile(path))
      {
      printf("Failed to delete file: %s\n",path);
      }
    }
  else
    {
/* Build arguments to new thread */
    struct pkt *packet=malloc(sizeof(struct pkt));
    if (fd->cFileName[0]=='.') return; /* skip . and .. */
    strcat(path,"\\");
    packet->dir=strdup(path);
    packet->sem=wait;
/* Bump semaphore count up by 1 */
    sem_signal(wait,1);
/* Launch new thread to delete subdirectory */
#ifndef SINGLE
    _beginthread(erase_all,8192,packet);
#else
    erase_all(packet);
#endif
    }
  }
void erase_all(struct pkt *packet)
  {
  char path[MAX_PATH];
  WIN32_FIND_DATA fd;
  HANDLE findhandle;
  BOOL found=TRUE;
  TNT_SEM wait;
/* Wait on subthreads before deleting directory */
  wait=sem_create(0);
  strcpy(path,packet->dir);
  strcat(path,"*.*");
/* Find all files and call del() */
  for (findhandle=FindFirstFile(path,&fd);
       found;
       found=FindNextFile(findhandle,&fd))
    {
    del(packet->dir,&fd,wait);
    }
/* Wait */
  sem_wait(wait,-1);
  sem_delete(wait);
/* Remove backslash */
  packet->dir[strlen(packet->dir)-1]='\0';
  if (!RemoveDirectory(packet->dir))
    {
    printf("Failed to remove directory: %s\n",packet->dir);
    if (GetLastError()==5)
      printf("Directory probably contains hidden or read only files.\n");
    };
/* Clean up malloc'd pointers */
  free(packet->dir);
  free(packet);
/* Signal parent thread that we are done */
  sem_signal(packet->sem,-1);
  }
main(int argc,char *argv[])
  {
  int i;
  struct pkt *p;
  TNT_SEM sem;
  char dir[MAX_PATH];
  if (argc>=2)
    {
    strcpy(dir,argv[1]);
    if (dir[strlen(dir)-1]!='\\') strcat(dir,"\\");
    p=malloc(sizeof(struct pkt));
    p->dir=strdup(dir);
    p->sem=sem=sem_create(1);
    erase_all(p);
    sem_wait(sem,-1);
    sem_delete(sem);
    }
  else
    {
    printf("XPRUNE by Al Williams\nUsage:\n"
           "XPRUNE directory_name\n\nRemoves directory and "
           "all files within it.");
    }
  }




[LISTING TWO]



/* TNT semaphores -- usable with NT, too.   Al Williams */
#ifndef _TNT_SEM
#define _TNT_SEM

typedef struct _tnt_sem
  {
  int count;             /* count */
  HANDLE event;          /* semaphore wait event */
  CRITICAL_SECTION cs;   /* controls access to count */
  } *TNT_SEM;
TNT_SEM sem_create(int count);
void sem_delete(TNT_SEM p);
int sem_signal(TNT_SEM p,int how);
int sem_wait(TNT_SEM p,int timeout);

#endif


[LISTING THREE]



/* TNT semaphores -- usable with NT, too.   Al Williams */

#include <windows.h>
#include <stdlib.h>
#include "tntsem.h"

/* Create a semaphore */
TNT_SEM sem_create(int count)
  {
  TNT_SEM p=(TNT_SEM)malloc(sizeof(struct _tnt_sem));
  if (p)
    {
    p->count=count;
    p->event=CreateEvent(NULL,TRUE,count==0?TRUE:FALSE,NULL);
    if (!p->event)
      {
      free(p);
      p=NULL;
      }
    }
  InitializeCriticalSection(&p->cs);
  return p;
  }
/* Destroy semaphore */
void sem_delete(TNT_SEM p)
  {
  if (p&&p->event) CloseHandle(p->event);
  if (p) DeleteCriticalSection(&p->cs);
  free(p);
  }
/* Signal a semaphore
   how == -x ; decrement by x
   how == 0  ; read semaphore count
   how == x  ; increment by x
*/
int sem_signal(TNT_SEM p,int how)
  {
  if (!p) return 0;
  EnterCriticalSection(&p->cs);
  p->count+=how;
  if (p->count)
    ResetEvent(p->event);
  else
    SetEvent(p->event);
  LeaveCriticalSection(&p->cs);
  return p->count;
  }
/* Wait for semaphore to reach zero count */
int sem_wait(TNT_SEM p,int timeout)
  {
  return WaitForSingleObject(p->event,timeout);
  }

Copyright © 1994, Dr. Dobb's Journal