| version 1.3, 2001/04/20 07:39:20 |
version 1.5, 2002/07/24 08:00:12 |
|
|
| #if defined(GC_WIN32_THREADS) || defined(WIN32_THREADS) |
#if defined(GC_WIN32_THREADS) |
| |
|
| #include "private/gc_priv.h" |
#include "private/gc_priv.h" |
| |
|
| |
#ifdef CYGWIN32 |
| |
# include <errno.h> |
| |
|
| |
/* Cygwin-specific forward decls */ |
| |
# undef pthread_create |
| |
# undef pthread_sigmask |
| |
# undef pthread_join |
| |
# undef dlopen |
| |
|
| |
# define DEBUG_CYGWIN_THREADS 0 |
| |
|
| |
GC_bool GC_thr_initialized = FALSE; |
| |
void * GC_start_routine(void * arg); |
| |
void GC_thread_exit_proc(void *arg); |
| |
|
| |
#endif |
| |
|
| |
|
| #if 0 |
#if 0 |
| #define STRICT |
#define STRICT |
| #include <windows.h> |
#include <windows.h> |
| Line 18 struct thread_entry { |
|
| Line 36 struct thread_entry { |
|
| /* !in_use ==> stack == 0 */ |
/* !in_use ==> stack == 0 */ |
| CONTEXT context; |
CONTEXT context; |
| GC_bool suspended; |
GC_bool suspended; |
| |
|
| |
# ifdef CYGWIN32 |
| |
void *status; /* hold exit value until join in case it's a pointer */ |
| |
pthread_t pthread_id; |
| |
# endif |
| |
|
| }; |
}; |
| |
|
| volatile GC_bool GC_please_stop = FALSE; |
volatile GC_bool GC_please_stop = FALSE; |
| Line 29 void GC_push_thread_structures GC_PROTO((void)) |
|
| Line 53 void GC_push_thread_structures GC_PROTO((void)) |
|
| /* Unlike the other threads implementations, the thread table here */ |
/* Unlike the other threads implementations, the thread table here */ |
| /* contains no pointers to the collectable heap. Thus we have */ |
/* contains no pointers to the collectable heap. Thus we have */ |
| /* no private structures we need to preserve. */ |
/* no private structures we need to preserve. */ |
| |
# ifdef CYGWIN32 |
| |
{ int i; /* pthreads may keep a pointer in the thread exit value */ |
| |
for (i = 0; i < MAX_THREADS; i++) |
| |
if (thread_table[i].in_use) GC_push_all((ptr_t)&(thread_table[i].status),(ptr_t)(&(thread_table[i].status)+1)); |
| |
} |
| |
#endif |
| } |
} |
| |
|
| void GC_stop_world() |
void GC_stop_world() |
| Line 36 void GC_stop_world() |
|
| Line 66 void GC_stop_world() |
|
| DWORD thread_id = GetCurrentThreadId(); |
DWORD thread_id = GetCurrentThreadId(); |
| int i; |
int i; |
| |
|
| |
#ifdef CYGWIN32 |
| |
if (!GC_thr_initialized) ABORT("GC_stop_world() called before GC_thr_init()"); |
| |
#endif |
| |
|
| GC_please_stop = TRUE; |
GC_please_stop = TRUE; |
| for (i = 0; i < MAX_THREADS; i++) |
for (i = 0; i < MAX_THREADS; i++) |
| if (thread_table[i].stack != 0 |
if (thread_table[i].stack != 0 |
| Line 53 void GC_stop_world() |
|
| Line 87 void GC_stop_world() |
|
| DWORD exitCode; |
DWORD exitCode; |
| if (GetExitCodeThread(thread_table[i].handle,&exitCode) && |
if (GetExitCodeThread(thread_table[i].handle,&exitCode) && |
| exitCode != STILL_ACTIVE) { |
exitCode != STILL_ACTIVE) { |
| thread_table[i].stack = 0; |
thread_table[i].stack = 0; /* prevent stack from being pushed */ |
| |
# ifndef CYGWIN32 |
| |
/* this breaks pthread_join on Cygwin, which is guaranteed to only see user pthreads */ |
| thread_table[i].in_use = FALSE; |
thread_table[i].in_use = FALSE; |
| CloseHandle(thread_table[i].handle); |
CloseHandle(thread_table[i].handle); |
| BZERO((void *)(&thread_table[i].context), sizeof(CONTEXT)); |
BZERO((void *)(&thread_table[i].context), sizeof(CONTEXT)); |
| |
#endif |
| continue; |
continue; |
| } |
} |
| if (SuspendThread(thread_table[i].handle) == (DWORD)-1) |
if (SuspendThread(thread_table[i].handle) == (DWORD)-1) |
| Line 134 void GC_push_all_stacks() |
|
| Line 171 void GC_push_all_stacks() |
|
| (LPCONTEXT)&thread_table[i].context)) |
(LPCONTEXT)&thread_table[i].context)) |
| ABORT("GetThreadContext failed"); |
ABORT("GetThreadContext failed"); |
| # ifdef I386 |
# ifdef I386 |
| if (thread_table[i].context.Esp >= (DWORD)thread_table[i].stack |
|
| || thread_table[i].context.Esp < (DWORD)bottom) |
|
| ABORT("Thread stack pointer out of range"); |
|
| GC_push_one ((word) thread_table[i].context.Edi); |
GC_push_one ((word) thread_table[i].context.Edi); |
| GC_push_one ((word) thread_table[i].context.Esi); |
GC_push_one ((word) thread_table[i].context.Esi); |
| GC_push_one ((word) thread_table[i].context.Ebp); |
GC_push_one ((word) thread_table[i].context.Ebp); |
| Line 144 void GC_push_all_stacks() |
|
| Line 178 void GC_push_all_stacks() |
|
| GC_push_one ((word) thread_table[i].context.Edx); |
GC_push_one ((word) thread_table[i].context.Edx); |
| GC_push_one ((word) thread_table[i].context.Ecx); |
GC_push_one ((word) thread_table[i].context.Ecx); |
| GC_push_one ((word) thread_table[i].context.Eax); |
GC_push_one ((word) thread_table[i].context.Eax); |
| GC_push_all_stack((char *) thread_table[i].context.Esp, |
if (thread_table[i].context.Esp >= (DWORD)thread_table[i].stack |
| thread_table[i].stack); |
|| thread_table[i].context.Esp < (DWORD)bottom) { |
| |
WARN("Thread stack pointer 0x%lx out of range, pushing everything", |
| |
thread_table[i].context.Esp); |
| |
GC_push_all_stack((char *) bottom, thread_table[i].stack); |
| |
} else { |
| |
GC_push_all_stack((char *) thread_table[i].context.Esp, |
| |
thread_table[i].stack); |
| |
} |
| # else |
# else |
| # ifdef ARM32 |
# ifdef ARM32 |
| if (thread_table[i].context.Sp >= (DWORD)thread_table[i].stack |
if (thread_table[i].context.Sp >= (DWORD)thread_table[i].stack |
| Line 331 void GC_get_next_stack(char *start, char **lo, char ** |
|
| Line 372 void GC_get_next_stack(char *start, char **lo, char ** |
|
| if (*lo < start) *lo = start; |
if (*lo < start) *lo = start; |
| } |
} |
| |
|
| |
#if !defined(MSWINCE) && !(defined(__MINGW32__) && !defined(_DLL)) |
| |
|
| # ifdef MSWINCE |
HANDLE WINAPI GC_CreateThread( |
| |
LPSECURITY_ATTRIBUTES lpThreadAttributes, |
| |
DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, |
| |
LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId ) |
| |
{ |
| |
return CreateThread(lpThreadAttributes, dwStackSize, lpStartAddress, |
| |
lpParameter, dwCreationFlags, lpThreadId); |
| |
} |
| |
|
| |
#else /* !defined(MSWINCE) && !(defined(__MINGW32__) && !defined(_DLL)) */ |
| |
|
| typedef struct { |
typedef struct { |
| HANDLE child_ready_h, parent_ready_h; |
HANDLE child_ready_h, parent_ready_h; |
| volatile struct thread_entry * entry; |
volatile struct thread_entry * entry; |
| Line 437 static DWORD WINAPI thread_start(LPVOID arg) |
|
| Line 488 static DWORD WINAPI thread_start(LPVOID arg) |
|
| /* Clear the thread entry even if we exit with an exception. */ |
/* Clear the thread entry even if we exit with an exception. */ |
| /* This is probably pointless, since an uncaught exception is */ |
/* This is probably pointless, since an uncaught exception is */ |
| /* supposed to result in the process being killed. */ |
/* supposed to result in the process being killed. */ |
| |
#ifndef __GNUC__ |
| __try { |
__try { |
| |
#endif /* __GNUC__ */ |
| ret = args.start (args.param); |
ret = args.start (args.param); |
| |
#ifndef __GNUC__ |
| } __finally { |
} __finally { |
| |
#endif /* __GNUC__ */ |
| LOCK(); |
LOCK(); |
| args.entry->stack = 0; |
args.entry->stack = 0; |
| args.entry->in_use = FALSE; |
args.entry->in_use = FALSE; |
| /* cast away volatile qualifier */ |
/* cast away volatile qualifier */ |
| BZERO((void *) &args.entry->context, sizeof(CONTEXT)); |
BZERO((void *) &args.entry->context, sizeof(CONTEXT)); |
| UNLOCK(); |
UNLOCK(); |
| |
#ifndef __GNUC__ |
| } |
} |
| |
#endif /* __GNUC__ */ |
| |
|
| return ret; |
return ret; |
| } |
} |
| |
#endif /* !defined(MSWINCE) && !(defined(__MINGW32__) && !defined(_DLL)) */ |
| |
|
| |
#ifdef MSWINCE |
| |
|
| typedef struct { |
typedef struct { |
| HINSTANCE hInstance; |
HINSTANCE hInstance; |
| HINSTANCE hPrevInstance; |
HINSTANCE hPrevInstance; |
| Line 504 DWORD WINAPI main_thread_start(LPVOID arg) |
|
| Line 564 DWORD WINAPI main_thread_start(LPVOID arg) |
|
| |
|
| LONG WINAPI GC_write_fault_handler(struct _EXCEPTION_POINTERS *exc_info); |
LONG WINAPI GC_write_fault_handler(struct _EXCEPTION_POINTERS *exc_info); |
| |
|
| |
/* threadAttach/threadDetach routines used by both CYGWIN and DLL implementation, |
| |
since both recieve explicit notification on thread creation/destruction |
| |
*/ |
| |
void threadAttach() { |
| |
int i; |
| |
/* It appears to be unsafe to acquire a lock here, since this */ |
| |
/* code is apparently not preeemptible on some systems. */ |
| |
/* (This is based on complaints, not on Microsoft's official */ |
| |
/* documentation, which says this should perform "only simple */ |
| |
/* inititalization tasks".) */ |
| |
/* Hence we make do with nonblocking synchronization. */ |
| |
|
| |
/* The following should be a noop according to the win32 */ |
| |
/* documentation. There is empirical evidence that it */ |
| |
/* isn't. - HB */ |
| |
# if defined(MPROTECT_VDB) |
| |
if (GC_incremental) SetUnhandledExceptionFilter(GC_write_fault_handler); |
| |
# endif |
| |
/* cast away volatile qualifier */ |
| |
for (i = 0; InterlockedExchange((LONG*)&thread_table[i].in_use,1) != 0; i++) { |
| |
/* Compare-and-swap would make this cleaner, but that's not */ |
| |
/* supported before Windows 98 and NT 4.0. In Windows 2000, */ |
| |
/* InterlockedExchange is supposed to be replaced by */ |
| |
/* InterlockedExchangePointer, but that's not really what I */ |
| |
/* want here. */ |
| |
if (i == MAX_THREADS - 1) |
| |
ABORT("too many threads"); |
| |
} |
| |
thread_table[i].id = GetCurrentThreadId(); |
| |
# ifdef CYGWIN32 |
| |
thread_table[i].pthread_id = pthread_self(); |
| |
# endif |
| |
if (!DuplicateHandle(GetCurrentProcess(), |
| |
GetCurrentThread(), |
| |
GetCurrentProcess(), |
| |
(HANDLE*)&thread_table[i].handle, |
| |
0, |
| |
0, |
| |
DUPLICATE_SAME_ACCESS)) { |
| |
DWORD last_error = GetLastError(); |
| |
GC_printf1("Last error code: %lx\n", last_error); |
| |
ABORT("DuplicateHandle failed"); |
| |
} |
| |
thread_table[i].stack = GC_get_stack_base(); |
| |
if (thread_table[i].stack == NULL) |
| |
ABORT("Failed to find stack base in threadAttach"); |
| |
/* If this thread is being created while we are trying to stop */ |
| |
/* the world, wait here. Hopefully this can't happen on any */ |
| |
/* systems that don't allow us to block here. */ |
| |
while (GC_please_stop) Sleep(20); |
| |
} |
| |
|
| |
void threadDetach(DWORD thread_id) { |
| |
int i; |
| |
|
| |
LOCK(); |
| |
for (i = 0; |
| |
i < MAX_THREADS && |
| |
!thread_table[i].in_use || thread_table[i].id != thread_id; |
| |
i++) {} |
| |
if (i >= MAX_THREADS ) { |
| |
WARN("thread %ld not found on detach", (GC_word)thread_id); |
| |
} |
| |
else { |
| |
thread_table[i].stack = 0; |
| |
thread_table[i].in_use = FALSE; |
| |
CloseHandle(thread_table[i].handle); |
| |
/* cast away volatile qualifier */ |
| |
BZERO((void *)&thread_table[i].context, sizeof(CONTEXT)); |
| |
} |
| |
UNLOCK(); |
| |
} |
| |
|
| |
#ifdef CYGWIN32 |
| |
|
| |
/* Called by GC_init() - we hold the allocation lock. */ |
| |
void GC_thr_init() { |
| |
if (GC_thr_initialized) return; |
| |
GC_thr_initialized = TRUE; |
| |
|
| |
#if 0 |
| |
/* this might already be handled in GC_init... */ |
| |
InitializeCriticalSection(&GC_allocate_ml); |
| |
#endif |
| |
|
| |
/* Add the initial thread, so we can stop it. */ |
| |
threadAttach(); |
| |
} |
| |
|
| |
struct start_info { |
| |
void *(*start_routine)(void *); |
| |
void *arg; |
| |
}; |
| |
|
| |
int GC_pthread_join(pthread_t pthread_id, void **retval) { |
| |
int result; |
| |
int i; |
| |
|
| |
# if DEBUG_CYGWIN_THREADS |
| |
GC_printf3("thread 0x%x(0x%x) is joining thread 0x%x.\n",(int)pthread_self(), |
| |
GetCurrentThreadId(), (int)pthread_id); |
| |
# endif |
| |
|
| |
/* Can't do any table lookups here, because thread being joined |
| |
might not have registered itself yet */ |
| |
|
| |
result = pthread_join(pthread_id, retval); |
| |
|
| |
LOCK(); |
| |
for (i = 0; !thread_table[i].in_use || thread_table[i].pthread_id != pthread_id; |
| |
i++) { |
| |
if (i == MAX_THREADS - 1) { |
| |
GC_printf1("Failed to find thread 0x%x in pthread_join()\n", pthread_id); |
| |
ABORT("thread not found on detach"); |
| |
} |
| |
} |
| |
UNLOCK(); |
| |
threadDetach(thread_table[i].id); |
| |
|
| |
# if DEBUG_CYGWIN_THREADS |
| |
GC_printf3("thread 0x%x(0x%x) completed join with thread 0x%x.\n", |
| |
(int)pthread_self(), GetCurrentThreadId(), (int)pthread_id); |
| |
# endif |
| |
|
| |
return result; |
| |
} |
| |
|
| |
/* Cygwin-pthreads calls CreateThread internally, but it's not |
| |
* easily interceptible by us.. |
| |
* so intercept pthread_create instead |
| |
*/ |
| |
int |
| |
GC_pthread_create(pthread_t *new_thread, |
| |
const pthread_attr_t *attr, |
| |
void *(*start_routine)(void *), void *arg) { |
| |
int result; |
| |
struct start_info * si; |
| |
|
| |
if (!GC_is_initialized) GC_init(); |
| |
/* make sure GC is initialized (i.e. main thread is attached) */ |
| |
|
| |
/* This is otherwise saved only in an area mmapped by the thread */ |
| |
/* library, which isn't visible to the collector. */ |
| |
si = GC_malloc_uncollectable(sizeof(struct start_info)); |
| |
if (0 == si) return(EAGAIN); |
| |
|
| |
si -> start_routine = start_routine; |
| |
si -> arg = arg; |
| |
|
| |
# if DEBUG_CYGWIN_THREADS |
| |
GC_printf2("About to create a thread from 0x%x(0x%x)\n",(int)pthread_self(), |
| |
GetCurrentThreadId); |
| |
# endif |
| |
result = pthread_create(new_thread, attr, GC_start_routine, si); |
| |
|
| |
if (result) { /* failure */ |
| |
GC_free(si); |
| |
} |
| |
|
| |
return(result); |
| |
} |
| |
|
| |
void * GC_start_routine(void * arg) |
| |
{ |
| |
struct start_info * si = arg; |
| |
void * result; |
| |
void *(*start)(void *); |
| |
void *start_arg; |
| |
pthread_t pthread_id; |
| |
int i; |
| |
|
| |
# if DEBUG_CYGWIN_THREADS |
| |
GC_printf2("thread 0x%x(0x%x) starting...\n",(int)pthread_self(), |
| |
GetCurrentThreadId()); |
| |
# endif |
| |
|
| |
/* If a GC occurs before the thread is registered, that GC will */ |
| |
/* ignore this thread. That's fine, since it will block trying to */ |
| |
/* acquire the allocation lock, and won't yet hold interesting */ |
| |
/* pointers. */ |
| |
LOCK(); |
| |
/* We register the thread here instead of in the parent, so that */ |
| |
/* we don't need to hold the allocation lock during pthread_create. */ |
| |
threadAttach(); |
| |
UNLOCK(); |
| |
|
| |
start = si -> start_routine; |
| |
start_arg = si -> arg; |
| |
pthread_id = pthread_self(); |
| |
|
| |
GC_free(si); /* was allocated uncollectable */ |
| |
|
| |
pthread_cleanup_push(GC_thread_exit_proc, pthread_id); |
| |
result = (*start)(start_arg); |
| |
pthread_cleanup_pop(0); |
| |
|
| |
# if DEBUG_CYGWIN_THREADS |
| |
GC_printf2("thread 0x%x(0x%x) returned from start routine.\n", |
| |
(int)pthread_self(),GetCurrentThreadId()); |
| |
# endif |
| |
|
| |
LOCK(); |
| |
for (i = 0; thread_table[i].pthread_id != pthread_id; i++) { |
| |
if (i == MAX_THREADS - 1) |
| |
ABORT("thread not found on exit"); |
| |
} |
| |
thread_table[i].status = result; |
| |
UNLOCK(); |
| |
|
| |
return(result); |
| |
} |
| |
|
| |
void GC_thread_exit_proc(void *arg) |
| |
{ |
| |
pthread_t pthread_id = (pthread_t)arg; |
| |
int i; |
| |
|
| |
# if DEBUG_CYGWIN_THREADS |
| |
GC_printf2("thread 0x%x(0x%x) called pthread_exit().\n",(int)pthread_self(),GetCurrentThreadId()); |
| |
# endif |
| |
|
| |
LOCK(); |
| |
for (i = 0; thread_table[i].pthread_id != pthread_id; i++) { |
| |
if (i == MAX_THREADS - 1) |
| |
ABORT("thread not found on exit"); |
| |
} |
| |
UNLOCK(); |
| |
|
| |
#if 0 |
| |
/* TODO: we need a way to get the exit value after a pthread_exit so we can stash it safely away */ |
| |
thread_table[i].status = ??? |
| |
#endif |
| |
} |
| |
|
| |
/* nothing required here... */ |
| |
int GC_pthread_sigmask(int how, const sigset_t *set, sigset_t *oset) { |
| |
return pthread_sigmask(how, set, oset); |
| |
} |
| |
int GC_pthread_detach(pthread_t thread) { |
| |
return pthread_detach(thread); |
| |
} |
| |
#else |
| |
|
| /* |
/* |
| * This isn't generally safe, since DllMain is not premptible. |
* This isn't generally safe, since DllMain is not premptible. |
| * If another thread holds the lock while this runs we're in trouble. |
* If another thread holds the lock while this runs we're in trouble. |
| Line 517 BOOL WINAPI DllMain(HINSTANCE inst, ULONG reason, LPVO |
|
| Line 820 BOOL WINAPI DllMain(HINSTANCE inst, ULONG reason, LPVO |
|
| GC_init(); /* Force initialization before thread attach. */ |
GC_init(); /* Force initialization before thread attach. */ |
| /* fall through */ |
/* fall through */ |
| case DLL_THREAD_ATTACH: |
case DLL_THREAD_ATTACH: |
| { |
threadAttach(); |
| int i; |
|
| /* It appears to be unsafe to acquire a lock here, since this */ |
|
| /* code is apparently not preeemptible on some systems. */ |
|
| /* (This is based on complaints, not on Microsoft's official */ |
|
| /* documentation, which says this should perform "only simple */ |
|
| /* inititalization tasks".) */ |
|
| /* Hence we make do with nonblocking synchronization. */ |
|
| |
|
| /* The following should be a noop according to the win32 */ |
|
| /* documentation. There is empirical evidence that it */ |
|
| /* isn't. - HB */ |
|
| # ifdef MPROTECT_VDB |
|
| if (GC_incremental) SetUnhandledExceptionFilter(GC_write_fault_handler); |
|
| # endif |
|
| |
|
| for (i = 0; |
|
| /* cast away volatile qualifier */ |
|
| InterlockedExchange((LPLONG) &thread_table[i].in_use, 1) != 0; |
|
| i++) { |
|
| /* Compare-and-swap would make this cleaner, but that's not */ |
|
| /* supported before Windows 98 and NT 4.0. In Windows 2000, */ |
|
| /* InterlockedExchange is supposed to be replaced by */ |
|
| /* InterlockedExchangePointer, but that's not really what I */ |
|
| /* want here. */ |
|
| if (i == MAX_THREADS - 1) |
|
| ABORT("too many threads"); |
|
| } |
|
| thread_table[i].id = GetCurrentThreadId(); |
|
| if (!DuplicateHandle(GetCurrentProcess(), |
|
| GetCurrentThread(), |
|
| GetCurrentProcess(), |
|
| /* cast away volatile qualifier */ |
|
| (HANDLE *) &thread_table[i].handle, |
|
| 0, |
|
| 0, |
|
| DUPLICATE_SAME_ACCESS)) { |
|
| DWORD last_error = GetLastError(); |
|
| GC_printf1("Last error code: %lx\n", last_error); |
|
| ABORT("DuplicateHandle failed"); |
|
| } |
|
| thread_table[i].stack = GC_get_stack_base(); |
|
| /* If this thread is being created while we are trying to stop */ |
|
| /* the world, wait here. Hopefully this can't happen on any */ |
|
| /* systems that don't allow us to block here. */ |
|
| while (GC_please_stop) Sleep(20); |
|
| } |
|
| break; |
break; |
| |
|
| case DLL_THREAD_DETACH: |
case DLL_THREAD_DETACH: |
| { |
threadDetach(GetCurrentThreadId()); |
| int i; |
|
| DWORD thread_id = GetCurrentThreadId(); |
|
| LOCK(); |
|
| for (i = 0; |
|
| i < MAX_THREADS && |
|
| (thread_table[i].stack == 0 || thread_table[i].id != thread_id); |
|
| i++) {} |
|
| if (i >= MAX_THREADS) { |
|
| WARN("thread %ld not found on detach", (GC_word)thread_id); |
|
| } else { |
|
| thread_table[i].stack = 0; |
|
| thread_table[i].in_use = FALSE; |
|
| CloseHandle(thread_table[i].handle); |
|
| /* cast away volatile qualifier */ |
|
| BZERO((void *) &thread_table[i].context, sizeof(CONTEXT)); |
|
| } |
|
| UNLOCK(); |
|
| } |
|
| break; |
break; |
| |
|
| case DLL_PROCESS_DETACH: |
case DLL_PROCESS_DETACH: |
| { |
{ |
| int i; |
int i; |
| Line 611 BOOL WINAPI DllMain(HINSTANCE inst, ULONG reason, LPVO |
|
| Line 852 BOOL WINAPI DllMain(HINSTANCE inst, ULONG reason, LPVO |
|
| } |
} |
| return TRUE; |
return TRUE; |
| } |
} |
| |
#endif /* CYGWIN32 */ |
| |
|
| # endif /* !MSWINCE */ |
# endif /* !MSWINCE */ |
| |
|
| #endif /* WIN32_THREADS */ |
#endif /* GC_WIN32_THREADS */ |