Figure 1   KillThrd Library Functions

KillThrd_CreateThread

Creates a worker thread that can be killed at any time

KillThrd_Kill

Kills the worker thread

KillThrd_Close

Frees the resources allocated by KillThrd_CreateThread

KillThrd_DelayDeath

Delays the death of the worker thread until a known "safe" time

Figure 2   KILLTHRD

KILLTHRD.H

 /******************************************************************************
Module name: KillThrd.H
Notices: By Jeffrey Richter
******************************************************************************/


///////////////////////////////////////////////////////////////////////////////
// The kill thread software exception code


// Useful macro for creating our own software exception codes
#define MAKESOFTWAREEXCEPTION(Severity, Facility, Exception) \
   ((DWORD) ( \
   /* Severity code */     (Severity  <<  0) |     \
   /* MS(0) or Cust(1) */  (1         << 29) |     \
   /* Reserved(0) */       (0         << 28) |     \
   /* Facility code */     (Facility  << 16) |     \
   /* Exception code */    (Exception <<  0)))


// Our very own software exception. This exception is raised
// when a thread is being killed.
#define SE_KILLTHREAD   \
   MAKESOFTWAREEXCEPTION(ERROR_SEVERITY_ERROR, FACILITY_NULL, 1)


///////////////////////////////////////////////////////////////////////////////
// Functions called by the control thread


// Data structure returned to the control thread.
// The control thread should only ever manipulate the
// m_hThread member directly.  Never touch the other members.
typedef struct {
   HANDLE   m_hThread;     // Handle of worker thread
   HANDLE   m_hmtxControl; // Used to coordinate access the other objects
   HANDLE   m_hmtxDelay;   // Queue killing when owned
   HANDLE   m_heventEnd;   // The killing was queued
} KILLTHRD, *PKILLTHRD;


// Creates a worker thread that can be killed at any time
PKILLTHRD WINAPI KillThrd_CreateThread (
                    LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, 
                    LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, 
                    DWORD dwCreationFlags, LPDWORD lpThreadId);

// Kills the worker thread
void WINAPI KillThrd_Kill (PKILLTHRD pkt);

// Frees the resources allocated by KillThrd_CreateThread
VOID WINAPI KillThrd_Close (PKILLTHRD pkt);


///////////////////////////////////////////////////////////////////////////////
// Functions called by the worker thread


// Delays the death of the worker until a known "safe" time
void WINAPI KillThrd_DelayDeath (BOOL fBlock);


///////////////////////////////// End of File /////////////////////////////////

KILLTHRD.CPP

 /******************************************************************************
Module name: KillThrd.CPP
Notices:     By Jeffrey Richter
Purpose:     Functions to kill a worker thread cleanly.
******************************************************************************/


#define STRICT
#include <windows.h>
#include "Process.h"    // For _beginthreadex
#include "KillThrd.h"


///////////////////////////////////////////////////////////////////////////////


// Used to store a pointer to the worker thread's internal data structure
// Allocated in KillThrd_CreateThread
static int gs_nTlsIndex = TLS_OUT_OF_INDEXES;


// Internal data structure used by the worker thread. 
typedef struct {
   HANDLE                 m_hmtxControl;     // Used to coordinate access to
                                             // the other objects
   HANDLE                 m_hmtxDelay;       // Delay death when owned
   DWORD                  m_dwDelayCount;    // # of times to delay death
   HANDLE                 m_heventEnd;       // The killing was queued
   LPTHREAD_START_ROUTINE m_lpStartAddress;  // Worker thread function
   LPVOID                 m_lpParameter;     // Worker thread parameter
} KILLTHRD_WORKERINFO, *PKILLTHRD_WORKERINFO;


///////////////////////////////////////////////////////////////////////////////


// Wraper function for the worker thread. The new thread starts here because
// we need to wrap the call to the actual worker thread function in an SEH
// __try block and to perform cleanup just before the thread dies.  The 
// address of the KILLTHRD_WORKERINFO structure is saved in 
// thread local storage.
static UINT WINAPI KillThrd_ThreadFunc (PVOID pvParam) {

   PKILLTHRD_WORKERINFO pktwi = (PKILLTHRD_WORKERINFO) pvParam;
   DWORD dwExitCode = 0;

   __try {
      __try {
         // The index is allocated in KillThrd_CreateThread
         TlsSetValue(gs_nTlsIndex, pktwi);
         dwExitCode = pktwi->m_lpStartAddress(pktwi->m_lpParameter);
      }

      // If the exception occurs because our thread is forcibly being
      // killed, execute our handler (the system does a global unwind first).
      __except ((GetExceptionCode() == SE_KILLTHREAD) ? 
        EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {

         // Nothing to do in here
      }
   }
   __finally {
      // This executes even if the thread is dying.
      CloseHandle(pktwi->m_hmtxDelay);
      CloseHandle(pktwi->m_heventEnd);
      CloseHandle(pktwi->m_hmtxControl);
      free(pktwi);
   }
   return(dwExitCode);
}


///////////////////////////////////////////////////////////////////////////////


// Use this function instead of CreateThread to start a killable thread.
// The parameters to this function match CreateThread.  The caller is 
// responsible for calling KillThrd_Close to free the allocated resources.
PKILLTHRD WINAPI KillThrd_CreateThread (
                  LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, 
                  LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, 
                  DWORD dwCreationFlags, LPDWORD lpThreadId) {

   PKILLTHRD pkt = NULL;
   PKILLTHRD_WORKERINFO pktwi = NULL;

   // If this is the first time this function is called, allocate
   // a thread local storage slot.
   if (gs_nTlsIndex == TLS_OUT_OF_INDEXES)
      gs_nTlsIndex = TlsAlloc();

   // Use calloc instead of malloc because it zeros the memory block
   // Note: Error check should be done here!
   pkt = (PKILLTHRD) calloc(1, sizeof(KILLTHRD));
   pktwi = (PKILLTHRD_WORKERINFO) calloc(1, sizeof(KILLTHRD_WORKERINFO));

   pktwi->m_lpStartAddress = lpStartAddress;
   pktwi->m_lpParameter = lpParameter;
   pktwi->m_dwDelayCount = 0;

   pktwi->m_hmtxControl = CreateMutex(NULL, FALSE, NULL);
   pktwi->m_hmtxDelay = CreateMutex(NULL, FALSE, NULL);
   pktwi->m_heventEnd = CreateEvent(NULL, TRUE, FALSE, NULL);


   // Duplicate the handles so the control thread and the worker
   // thread each have their own set of process-relative handles which
   // they are responsible for closing.  The actual kernel objects will
   // not be destroyed until both handles are closed.
   DuplicateHandle(GetCurrentProcess(), pktwi->m_hmtxControl,
                   GetCurrentProcess(), &pkt->m_hmtxControl, 0, FALSE, 
                   DUPLICATE_SAME_ACCESS);

   DuplicateHandle(GetCurrentProcess(), pktwi->m_hmtxDelay,
                   GetCurrentProcess(), &pkt->m_hmtxDelay, 0, FALSE,
                   DUPLICATE_SAME_ACCESS);

   DuplicateHandle(GetCurrentProcess(), pktwi->m_heventEnd,
                   GetCurrentProcess(), &pkt->m_heventEnd, 0, FALSE,
                   DUPLICATE_SAME_ACCESS);

   // Start the thread at our wrapper function, KillThrd_ThreadFunc,
   // which then calls lpStartAddress
   pkt->m_hThread = (HANDLE) _beginthreadex(lpThreadAttributes,
                     dwStackSize, KillThrd_ThreadFunc, pktwi, dwCreationFlags,
                     (PUINT) lpThreadId);

   // The control thread uses pkt to kill the worker thread using
   // KillThrd_Kill and must free this resouce by calling KillThrd_Close.
   return(pkt);
}


///////////////////////////////////////////////////////////////////////////////


// When the control thread is done with pkt, it must call this function to
// close the handles and free the memory.
VOID WINAPI KillThrd_Close (PKILLTHRD pkt) {

   if (pkt != NULL) {
      if (pkt->m_hThread     != NULL) CloseHandle(pkt->m_hThread);
      if (pkt->m_hmtxDelay   != NULL) CloseHandle(pkt->m_hmtxDelay);
      if (pkt->m_heventEnd   != NULL) CloseHandle(pkt->m_heventEnd);
      if (pkt->m_hmtxControl != NULL) CloseHandle(pkt->m_hmtxControl);
      free(pkt);
   }
}


///////////////////////////////////////////////////////////////////////////////


// Terminate the worker thread by getting it to execute this function 
static void WINAPI KillThrd_ForceDeath (void) {


   // Get the address of the worker thread's internal data block. This was
   // set by KillThrd_ThreadFunc
   PKILLTHRD_WORKERINFO pktwi =
      (PKILLTHRD_WORKERINFO) TlsGetValue(gs_nTlsIndex);

   RaiseException(SE_KILLTHREAD, EXCEPTION_NONCONTINUABLE, 0, NULL);
   // RaiseException never returns
}


///////////////////////////////////////////////////////////////////////////////
// Macros used to abstract the instruction pointer register for the various
// CPU platforms.


#if defined(_X86_)
#define PROGCTR(Context)  (Context.Eip)
#endif

#if defined(_MIPS_)
#define PROGCTR(Context)  (Context.Fir)
#endif

#if defined(_ALPHA_)
#define PROGCTR(Context)  (Context.Fir)
#endif

#if defined(_PPC_)
#define PROGCTR(Context)  (Context.Iar)
#endif

#if !defined(PROGCTR)
#error Module contains CPU-specific code; modify and recompile.
#endif


///////////////////////////////////////////////////////////////////////////////


// The control thread calls this function to kill a worker thread.  If the
// worker thread is not currently protected by KillThrd_DelayDeath, we attempt
// to kill the thread now by suspending it, changing it's instruction pointer
// to KillThrd_ForceDeath, and resuming the thread.  Effectively we are raising
// an exception in the worker thread.  If the worker thread is protected by
// KillThrd_DelayDeath, we simply set an event and let the thread kill itself
// when it calls KillThrd_DelayDeath(FALSE) and ends its protection.
void WINAPI KillThrd_Kill (PKILLTHRD pkt) {

   WaitForSingleObject(pkt->m_hmtxControl, INFINITE);

   if (WaitForSingleObject(pkt->m_hmtxDelay, 0) == WAIT_TIMEOUT) {

      // The worker is delaying its death, set a flag that the worker
      // will check later.
      SetEvent(pkt->m_heventEnd);
   } else {

      // The worker can be terminated now!
      CONTEXT context;

      // Stop the worker thread
      SuspendThread(pkt->m_hThread);

      if (WaitForSingleObject(pkt->m_hThread, 0) == WAIT_TIMEOUT) {
         // The worker has not yet terminated

         // Get the worker thread's current CPU registers
         context.ContextFlags = CONTEXT_CONTROL;
         GetThreadContext(pkt->m_hThread, &context);

         // Change the instruction pointer to our function
         PROGCTR(context) = (DWORD) KillThrd_ForceDeath;
         SetThreadContext(pkt->m_hThread, &context);

         // Resuming the thread forces our function to be called which
         // rasies an SE_KILLTHREAD exception in the worker thread.
         ResumeThread(pkt->m_hThread);
      }
      ReleaseMutex(pkt->m_hmtxDelay);
   }
   ReleaseMutex(pkt->m_hmtxControl);
}


///////////////////////////////////////////////////////////////////////////////


// This function is used to allow the worker thread to protect sections of code
// from termination by the control thread.  Call KillThrd_DelayDeath(TRUE) to
// start protection from KillThrd_Kill and KillThrd_DelayDeath(FALSE) to end
// protection.  Multiple KillThrd_DelayDeath(TRUE) calls are allowed.  A delay
// count is maintained and the thread remains protected until the count is 0.
void WINAPI KillThrd_DelayDeath (BOOL fBlock) {

   // Get the address of the worker thread's internal data block. This was
   // set by KillThrd_ThreadFunc
   PKILLTHRD_WORKERINFO pktwi = 
      (PKILLTHRD_WORKERINFO) TlsGetValue(gs_nTlsIndex);

   WaitForSingleObject(pktwi->m_hmtxControl, INFINITE);

   if (fBlock) {

      // The worker wants to delay its death
      // We get and keep the m_hmtxDelay mutex while protected
      // from termination by KillThrd_Kill.
      WaitForSingleObject(pktwi->m_hmtxDelay, INFINITE);

      // Increment the delay death count
      pktwi->m_dwDelayCount++;
   } else {

      // The worker wants to allow its death
      // Decrement the delay death count
      pktwi->m_dwDelayCount--;
      ReleaseMutex(pktwi->m_hmtxDelay);

      // If the delay death count is zero and
      if ((pktwi->m_dwDelayCount == 0) &&
         (WaitForSingleObject(pktwi->m_heventEnd, 0) == WAIT_OBJECT_0)) {

         // The delay death count is zero AND KillThrd_Kill has been called.
         // Force us (the worker thread) to terminate now.
         KillThrd_ForceDeath();
      }
   }
   ReleaseMutex(pktwi->m_hmtxControl);
}


///////////////////////////////// End of File /////////////////////////////////

Figure 3   KTTEST

KTTEST.CPP

 /*****************************************************************************
Module:  KTTest.CPP
Notices: By Jeffrey Richter
Purpose: Demonstrates how to kill a thread cleanly.
*****************************************************************************/


#define STRICT
#include <windows.h>
#include <windowsx.h>
#pragma warning(disable: 4001)    /* Single line comment */

#include <tchar.h>

#include "resource.h"
#include "KillThrd.h"             // Kill Thread functions


//////////////////////////////////////////////////////////////////////////////


// This simple C++ class exists in order to test C++ EH.
class CTestClass {
   HWND   m_hwnd;       // Handle of window
   HANDLE m_hThread;    // Handle to worker thread's kernel object

public:
   CTestClass (HWND hwnd);
   ~CTestClass ();

   // This cast operator simply returns the thread handle
   operator HANDLE() const { return(m_hThread); }
};


CTestClass::CTestClass (HWND hwnd) {
   m_hwnd = hwnd;

   // Create a "real" handle to the thread kernel object
   DuplicateHandle(GetCurrentProcess(), GetCurrentThread(),
              GetCurrentProcess(), &m_hThread, 0, FALSE, DUPLICATE_SAME_ACCESS);

   ListBox_SetCurSel(m_hwnd,
      ListBox_AddString(m_hwnd, _T("C++ object created")));
}


CTestClass::~CTestClass () {
   // Close the thread handle
   CloseHandle(m_hThread);
   ListBox_SetCurSel(m_hwnd,
      ListBox_AddString(m_hwnd, _T("C++ object destroyed")));
}


///////////////////////////// HANDLE_DLGMSG Macro ////////////////////////////


// The normal HANDLE_MSG macro in WINDOWSX.H does not work properly for dialog
// boxes because DlgProc's return a BOOL instead of an LRESULT (like
// WndProcs). This HANDLE_DLGMSG macro corrects the problem:
#define HANDLE_DLGMSG(hwnd, message, fn)                    \
   case (message): return (SetDlgMsgResult(hwnd, uMsg,      \
                            HANDLE_##message((hwnd), (wParam), (lParam), (fn))))


//////////////////////////////////////////////////////////////////////////////


BOOL KTTest_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM lParam) {

   // Disable the "End" button since the worker thread hasn't started yet
   EnableWindow(GetDlgItem(hwnd, IDC_END), FALSE);

   return(TRUE);                  // Accept default focus window.
}


//////////////////////////////////////////////////////////////////////////////


// This is the interesting test code. This function even creates a stack-based
// C++ object so that you can see that the object's destructor is called when
// the thread is forcibly terminated.

// Turn off optimizations so that the compiler generates the loop code
// See comment inside function
#pragma optimize("g", off)


DWORD WINAPI WorkerThreadWithoutSEH (LPVOID pvParam) {
   HWND hwnd = (HWND) pvParam;

   // Windows 95 has a bug which causes an exception to be raised when a
   // thread is waiting in a call WaitForSingleObject/WaitForMultipleObjects
   // and the thread's instruction pointer is changed. Using the fIsWin95
   // flag ensures that the is bug does not expose itself.
   OSVERSIONINFO osvi;
   osvi.dwOSVersionInfoSize = sizeof(osvi);
   GetVersionEx(&osvi);
   BOOL fIsWin95 = (osvi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS);

   ListBox_SetCurSel(hwnd, 
      ListBox_AddString(hwnd, _T("Starting Worker thread")));

   // Create a C++ object to test C++ EH. The C++ object creates a handle to
   // this thread's kernel object.
   CTestClass TestClass(hwnd);


   ListBox_SetCurSel(hwnd, ListBox_AddString(hwnd, _T("Sleep (3 seconds)")));
   Sleep(3000);


   ListBox_SetCurSel(hwnd,
      ListBox_AddString(hwnd, _T("WaitForSingleObject (3 seconds)")));
   // Wait on this thread's kernel object - it will never be signalled.
   WaitForSingleObject((HANDLE) TestClass, 3000);


   ListBox_SetCurSel(hwnd,
      ListBox_AddString(hwnd, _T("WaitForMultipleObjects (3 seconds)")));
   HANDLE h = (HANDLE) TestClass;
   // Wait on this thread's kernel object - it will never be signalled.
   WaitForMultipleObjects(1, &h, TRUE, 3000);


   // Don't allow the UI thread to terminate us until our loop terminates
   KillThrd_DelayDeath(TRUE);
   ListBox_SetCurSel(hwnd,
                 ListBox_AddString(hwnd, _T("Looping a lot, can't be broken")));
   for (int n = 0 ; n < 200000; n++) {

      // We can delay the thread's death multiple times.
      KillThrd_DelayDeath(TRUE);

      // We would usually want to delay our death when using functions
      // that wait on CriticalSections or other thread synchronization
      // objects so that we don't get them and then die leaving other
      // threads suspended forever.
      free(malloc(10));
      KillThrd_DelayDeath(FALSE);
   }
   KillThrd_DelayDeath(FALSE);


   // The UI thread can terminate this loop at any time.
   ListBox_SetCurSel(hwnd, 
      ListBox_AddString(hwnd, _T("Looping a lot, can be broken")));

   // We have to turn off optimizations so that the compiler
   // produces code for the loop below.
   for (n = 0 ; n < 50000000; n++) {
      // Do nothing
   }

   // We made it to the end of the function
   ListBox_SetCurSel(hwnd, 
      ListBox_AddString(hwnd, _T("Finished looping")));

   return(0);
}

// Turn optimizations back on
#pragma optimize("g", on)


//////////////////////////////////////////////////////////////////////////////


DWORD WINAPI WorkerThreadExcFilter (DWORD dwExceptionCode, HWND hwnd) {

   // Exception filter used for testing. It adds an entry to the listbox
   // when the worker thread is termiante with KillThrd_Kill.
   if (dwExceptionCode == SE_KILLTHREAD) {
      ListBox_SetCurSel(hwnd, 
         ListBox_AddString(hwnd, _T("SE_KILLTHREAD detected")));
   }

   return(EXCEPTION_CONTINUE_SEARCH);
}


//////////////////////////////////////////////////////////////////////////////


// This is the worker thread.
DWORD WINAPI WorkerThread (LPVOID pvParam) {

   HWND hwnd = (HWND) pvParam;
   DWORD dwExitCode = 0;

   __try {
      __try {
         // Because we can't mix SEH and C++ EH in the same function, call
         // another function so that C++ objects are destructed properly.
         dwExitCode = WorkerThreadWithoutSEH(pvParam);
      }
      __except (WorkerThreadExcFilter(GetExceptionCode(), hwnd)) {
         // We never get in here because WorkerThreadExcFilter always
         // returns EXCEPTION_CONTINUE_SEARCH.
      }
   }
   __finally {
      // This executes even if the thread is dying.
      ListBox_SetCurSel(hwnd,
         ListBox_AddString(hwnd, _T("Inside __finally block")));
   }

   // This executes only if the thread dies naturally. It does execute
   // if KillThrd_Kill is used to kill this thread.
   ListBox_SetCurSel(hwnd,
      ListBox_AddString(hwnd, _T("Worker thread is dying on its own")));

   return(dwExitCode);
}


//////////////////////////////////////////////////////////////////////////////


// Special watchdog thread used for testing.  This thread just waits for
// the worker thread to terminate and then adds an entry to the listbox.
DWORD WINAPI WorkerMonitorThread (LPVOID pvParam) {

   HANDLE hThread = (HANDLE) pvParam;
   HWND hwnd = GetForegroundWindow();

   WaitForSingleObject(hThread, INFINITE);

   // When the worker thread dies, add an entry to the listbox.
   HWND hwndLB = GetDlgItem(hwnd, IDC_PROGRESS);
   ListBox_SetCurSel(hwndLB,
      ListBox_AddString(hwndLB, _T("--> Worker thread is definitely dead")));

   // Re-enable the "Start" button and disable the "End" button
   EnableWindow(GetDlgItem(hwnd, IDC_START), TRUE);
   EnableWindow(GetDlgItem(hwnd, IDC_END), FALSE);

   // Give focus to the "Start" button. NOTE: Because the "Start" button
   // was created with another thread, we have to attach our input queues
   // together first before calling SetFocus.
   AttachThreadInput(GetCurrentThreadId(),
                     GetWindowThreadProcessId(hwnd, NULL), TRUE);
   SetFocus(GetDlgItem(hwnd, IDC_START));
   return(0);
}


//////////////////////////////////////////////////////////////////////////////


void KTTest_OnCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) {
   static PKILLTHRD s_pkt = NULL;
   HWND hwndLB = GetDlgItem(hwnd, IDC_PROGRESS);
   DWORD dwThreadId;

   switch (id) {
      case IDC_START:
         ListBox_ResetContent(hwndLB);

         // Create the worker thread using KillThrd_CreateThread so that
         // the thread can be killed cleanly at any time. This function
         // allocates a block of memory and returns the pointer. This
         // pointer is saved in a static so that we can use it when
         // the user presses the "End" button.
         s_pkt = KillThrd_CreateThread(NULL, 0, WorkerThread,
            hwndLB, 0, &dwThreadId);

         // For testing purposes, create a watchdog thread that sleeps
         // until the worker thread dies. This thread adds a message to
         // the listbox when the worker thread is truely dead.
         CloseHandle(CreateThread(NULL, 0, WorkerMonitorThread,
            (PVOID) s_pkt->m_hThread, 0, &dwThreadId));

         // Disable the "Start" button and enable the "End" button.
         EnableWindow(hwndCtl, FALSE);
         EnableWindow(GetDlgItem(hwnd, IDC_END), TRUE);
         SetFocus(GetDlgItem(hwnd, IDC_END));
         break;

      case IDC_END:
         // The worker thread should die now!  Disable the "End" button.
         EnableWindow(hwndCtl, FALSE);

         // Kill the worker thread passing the address of the block
         // returned from KillThrd_CreateThread.
         KillThrd_Kill(s_pkt);

         // Cleanup the resources.  If you'd like, you can use the thread
         // handle in this structure BEFORE calling KillThrd_Close
         KillThrd_Close(s_pkt);
         break;

      case IDCANCEL:              // Allows dialog box to close
         EndDialog(hwnd, id);
         break;
   }
}


//////////////////////////////////////////////////////////////////////////////


BOOL WINAPI KTTest_DlgProc (HWND hwnd, UINT uMsg,
   WPARAM wParam, LPARAM lParam) {

   switch (uMsg) {

      // Standard Window's messages
      HANDLE_DLGMSG(hwnd, WM_INITDIALOG, KTTest_OnInitDialog);
      HANDLE_DLGMSG(hwnd, WM_COMMAND,    KTTest_OnCommand);
   }
   return(FALSE);                 // We didn't process the message.
}


//////////////////////////////////////////////////////////////////////////////


int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE hinstPrev,
                    LPSTR lpszCmdLine, int nCmdShow) {

   DialogBox(hinstExe, MAKEINTRESOURCE(IDD_KILLTHRDTEST), NULL,
             KTTest_DlgProc);

   return(0);
}


//////////////////////////////// End of File /////////////////////////////////

RESOURCE.H

 //{{NO_DEPENDENCIES}}
// Microsoft Developer Studio generated include file.
// Used by KTTest.rc
//
#define IDD_KILLTHRDTEST                101
#define IDC_START                       1008
#define IDC_END                         1009
#define IDC_PROGRESS                    1015

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE        104
#define _APS_NEXT_COMMAND_VALUE         40001
#define _APS_NEXT_CONTROL_VALUE         1016
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif