diff --git a/ports/windows/Makefile b/ports/windows/Makefile index 115d1a61ef51e..e9c13f09cc366 100644 --- a/ports/windows/Makefile +++ b/ports/windows/Makefile @@ -62,6 +62,7 @@ SRC_C = \ ports/unix/input.c \ ports/unix/gccollect.c \ windows_mphal.c \ + mpthreadport.c \ realpath.c \ init.c \ fmode.c \ diff --git a/ports/windows/init.c b/ports/windows/init.c index 87d581c30493e..930400fdedcfd 100644 --- a/ports/windows/init.c +++ b/ports/windows/init.c @@ -30,6 +30,7 @@ #ifdef _MSC_VER #include #endif +#include "sleep.h" #include "fmode.h" extern BOOL WINAPI console_sighandler(DWORD evt); @@ -53,6 +54,7 @@ void init() { _set_invalid_parameter_handler(invalid_param_handler); #endif SetConsoleCtrlHandler(console_sighandler, TRUE); + init_sleep(); #ifdef __MINGW32__ putenv("PRINTF_EXPONENT_DIGITS=2"); #elif _MSC_VER < 1900 @@ -65,4 +67,5 @@ void init() { void deinit() { SetConsoleCtrlHandler(console_sighandler, FALSE); + deinit_sleep(); } diff --git a/ports/windows/mpconfigport.h b/ports/windows/mpconfigport.h index 6e32d5fe5ebdf..467dc12ed6277 100644 --- a/ports/windows/mpconfigport.h +++ b/ports/windows/mpconfigport.h @@ -158,6 +158,7 @@ #define MICROPY_PY_MACHINE_INCLUDEFILE "ports/unix/modmachine.c" #define MICROPY_PY_MACHINE_PULSE (1) #define MICROPY_PY_MACHINE_PIN_BASE (1) +#define MICROPY_PY_THREAD (1) #define MICROPY_MACHINE_MEM_GET_READ_ADDR mod_machine_mem_get_addr #define MICROPY_MACHINE_MEM_GET_WRITE_ADDR mod_machine_mem_get_addr @@ -218,6 +219,11 @@ typedef long mp_off_t; #define MP_STATE_PORT MP_STATE_VM +#if MICROPY_PY_THREAD +#define MICROPY_BEGIN_ATOMIC_SECTION() (mp_thread_windows_begin_atomic_section(), 0xffffffff) +#define MICROPY_END_ATOMIC_SECTION(x) (void)x; mp_thread_windows_end_atomic_section() +#endif + #define MICROPY_MPHALPORT_H "windows_mphal.h" // We need to provide a declaration/definition of alloca() diff --git a/ports/windows/mpthreadport.c b/ports/windows/mpthreadport.c new file mode 100644 index 0000000000000..10ccc8d56f526 --- /dev/null +++ b/ports/windows/mpthreadport.c @@ -0,0 +1,261 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2021 Andrew Leech + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include + +#include "py/runtime.h" +#include "py/mpthread.h" +#include "py/gc.h" + +#if MICROPY_PY_THREAD + +#include +#include "shared/runtime/gchelper.h" + +// This value seems to be about right for both 32-bit and 64-bit builds. +#define THREAD_STACK_OVERFLOW_MARGIN (8192) + +// this structure forms a linked list, one node per active thread. +typedef struct _thread_t { + DWORD id; // system id of thread + HANDLE handle; // handle of thread + int ready; // whether the thread is ready and running + void *(*entry)(void *); // entrypoint, to be passed into thread wrapper + void *arg; // thread Python args, a GC root pointer + struct _thread_t *next; +} thread_t; + +static DWORD tls_key; +static thread_t *thread; +static HANDLE criticalSection = INVALID_HANDLE_VALUE; + +static DWORD wait_for_event(HANDLE event, bool wait) { + // WaitForSingleObjectEx will exit with WAIT_IO_COMPLETION when the thread + // runs an APC function (mp_thread_gc below) so we ignore this and wait + // for a normal event retult. + DWORD ret = WAIT_IO_COMPLETION; + while(WAIT_IO_COMPLETION==ret) { + ret = WaitForSingleObjectEx(event, (wait)?INFINITE:0, TRUE); + } + return ret; +} + +void mp_thread_windows_begin_atomic_section(void) { + if (criticalSection != INVALID_HANDLE_VALUE) { + wait_for_event(criticalSection, TRUE); + } +} + +void mp_thread_windows_end_atomic_section(void) { + if (criticalSection != INVALID_HANDLE_VALUE) { + ReleaseMutex(criticalSection); + } +} + +void mp_thread_init(void) { + tls_key = TlsAlloc(); + TlsSetValue(tls_key, (LPVOID) &mp_state_ctx.thread); + + // Needs to be a recursive mutex to emulate the behavior of + // BEGIN_ATOMIC_SECTION on bare metal. This is the default + // behavior of win32 Mutex and CriticalSection objects. + criticalSection = CreateMutex(NULL, FALSE, NULL); + + // create first entry in linked list of all threads. + thread = malloc(sizeof(thread_t)); + thread->id = GetCurrentThreadId(); + thread->ready = 1; + thread->arg = NULL; + thread->next = NULL; + + // The handle is used for running gc via QueueUserAPC below. + thread->handle = OpenThread(THREAD_SET_CONTEXT, FALSE, thread->id); +} + +void mp_thread_deinit(void) { + mp_thread_windows_begin_atomic_section(); + while (thread->next != NULL) { + thread_t *th = thread; + thread = thread->next; + TerminateThread(th->handle, -1); + free(th); + } + mp_thread_windows_end_atomic_section(); + assert(thread->id == GetCurrentThreadId()); + free(thread); +} + +// this signal handler is used to scan the regs and stack of a thread. +static void WINAPI mp_thread_gc(ULONG_PTR parameter) { + HANDLE thread_signal_done = (HANDLE)parameter; + gc_helper_collect_regs_and_stack(); + #if MICROPY_ENABLE_PYSTACK + void **ptrs = (void **)(void *)MP_STATE_THREAD(pystack_start); + gc_collect_root(ptrs, (MP_STATE_THREAD(pystack_cur) - MP_STATE_THREAD(pystack_start)) / sizeof(void *)); + #endif + SetEvent(thread_signal_done); +} + +// This function scans all pointers that are external to the current thread. +// It does this by signalling all other threads and getting them to scan their +// own registers and stack. Note that there may still be some edge cases left +// with race conditions and root-pointer scanning: a given thread may manipulate +// the global root pointers (in mp_state_ctx) while another thread is doing a +// garbage collection and tracing these pointers. +void mp_thread_gc_others(void) { + mp_thread_windows_begin_atomic_section(); + for (thread_t *th = thread; th != NULL; th = th->next) { + gc_collect_root(&th->arg, 1); + if (th->id == GetCurrentThreadId()) { + continue; + } + if (!th->ready) { + continue; + } + + // Used to signal when targetted thread gc is complete. + HANDLE thread_signal_done = CreateEvent(NULL, FALSE, FALSE, NULL); + + // Run mp_thread_gc function on target thread (soon). + QueueUserAPC(mp_thread_gc, th->handle, (ULONG_PTR)thread_signal_done); + + // wait for it to finish running. + wait_for_event(thread_signal_done, TRUE); + } + mp_thread_windows_end_atomic_section(); +} + +mp_state_thread_t *mp_thread_get_state(void) { + return (mp_state_thread_t *)TlsGetValue(tls_key); +} + +void mp_thread_set_state(mp_state_thread_t *state) { + TlsSetValue(tls_key, (LPVOID) state); +} + +void mp_thread_start(void) { + mp_thread_windows_begin_atomic_section(); + for (thread_t *th = thread; th != NULL; th = th->next) { + if (th->id == GetCurrentThreadId()) { + th->ready = 1; + break; + } + } + mp_thread_windows_end_atomic_section(); +} + +static DWORD WINAPI win32_entry(PVOID args) { + // Win32 threads require different function form to posix. + thread_t *th = (thread_t *)args; + th->entry(th->arg); + return 0; +} + +void mp_thread_create(void *(*entry)(void *), void *arg, size_t *stack_size) { + // default stack size is 8k machine-words. + if (*stack_size == 0) { + *stack_size = 8192 * sizeof(void *); + } + + // ensure there is enough stack to include a stack-overflow margin. + if (*stack_size < 2 * THREAD_STACK_OVERFLOW_MARGIN) { + *stack_size = 2 * THREAD_STACK_OVERFLOW_MARGIN; + } + + mp_thread_windows_begin_atomic_section(); + + // create thread + DWORD id; + + thread_t *th = malloc(sizeof(thread_t)); + th->entry = entry; + th->arg = arg; + + HANDLE thread_handle = CreateThread(NULL, *stack_size, win32_entry, th, 0, &id); + + if (thread_handle == NULL) { + free(th); + mp_thread_windows_end_atomic_section(); + goto er; + } + + // adjust stack_size to provide room to recover from hitting the limit. + *stack_size -= THREAD_STACK_OVERFLOW_MARGIN; + + // add thread to linked list of all threads. + th->id = id; + th->handle = thread_handle; + th->ready = 0; + th->next = thread; + thread = th; + + mp_thread_windows_end_atomic_section(); + + return; + +er: + mp_raise_OSError(-1); +} + +void mp_thread_finish(void) { + mp_thread_windows_begin_atomic_section(); + thread_t *prev = NULL; + for (thread_t *th = thread; th != NULL; th = th->next) { + if (th->id == GetCurrentThreadId()) { + if (prev == NULL) { + thread = th->next; + } else { + prev->next = th->next; + } + free(th); + break; + } + prev = th; + } + mp_thread_windows_end_atomic_section(); +} + +void mp_thread_mutex_init(mp_thread_mutex_t *mutex) { + // The win32 Mutex is re-entrant, unlike the posix equivalent. + // To avoid this a sepaphore with size of 1 is used. + *mutex = CreateSemaphore(NULL, 1, 1, NULL); +} + +int mp_thread_mutex_lock(mp_thread_mutex_t *mutex, int wait) { + if (wait_for_event(*mutex, wait) == 0) { + return 1; + } else { + return 0; + } +} + +void mp_thread_mutex_unlock(mp_thread_mutex_t *mutex) { + ReleaseSemaphore(*mutex, 1, NULL); +} + +#endif // MICROPY_PY_THREAD diff --git a/ports/windows/mpthreadport.h b/ports/windows/mpthreadport.h new file mode 100644 index 0000000000000..86c19367ef708 --- /dev/null +++ b/ports/windows/mpthreadport.h @@ -0,0 +1,36 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2021 Andrew Leech + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +typedef void* mp_thread_mutex_t; // win32 HANDLE + +void mp_thread_init(void); +void mp_thread_deinit(void); +void mp_thread_gc_others(void); + +// Windows version of "enable/disable IRQs". +// Functions as a port-global lock for any code that must be serialised. +void mp_thread_windows_begin_atomic_section(void); +void mp_thread_windows_end_atomic_section(void); diff --git a/ports/windows/sleep.h b/ports/windows/sleep.h index da76e21978b7e..3bfc4446171c9 100644 --- a/ports/windows/sleep.h +++ b/ports/windows/sleep.h @@ -26,6 +26,8 @@ #ifndef MICROPY_INCLUDED_WINDOWS_SLEEP_H #define MICROPY_INCLUDED_WINDOWS_SLEEP_H +void init_sleep(void); +void deinit_sleep(void); // The main sleep implementation for the Windows port. void msec_sleep(double msec); diff --git a/ports/windows/windows_mphal.c b/ports/windows/windows_mphal.c index 5a65b2fd2aeae..2182c497a2baa 100644 --- a/ports/windows/windows_mphal.c +++ b/ports/windows/windows_mphal.c @@ -264,19 +264,58 @@ uint64_t mp_hal_time_ns(void) { return (uint64_t)tv.tv_sec * 1000000000ULL + (uint64_t)tv.tv_usec * 1000ULL; } -void msec_sleep(double msec) { - if (msec < 0.0) { - msec = 0.0; +HANDLE waitTimer = NULL; + +void init_sleep(void) { + waitTimer = CreateWaitableTimer(NULL, TRUE, NULL); +} + +void deinit_sleep(void) { + if (waitTimer != NULL) { + CloseHandle(waitTimer); + waitTimer = NULL; } - SleepEx((DWORD)msec, TRUE); } -#ifdef _MSC_VER -int usleep(__int64 usec) { - msec_sleep((double)usec / 1000.0); +int usleep_impl(__int64 usec) { + if (waitTimer == NULL) { + errno = EAGAIN; + return -1; + } + if (usec < 0 || usec > LLONG_MAX / 10) { + errno = EINVAL; + return -1; + } + + LARGE_INTEGER ft; + ft.QuadPart = -10 * usec; // 100 nanosecond interval, negative value = relative time + if (SetWaitableTimer(waitTimer, &ft, 0, NULL, NULL, 0) == 0) { + errno = EINVAL; + return -1; + } + if (WaitForSingleObject(waitTimer, INFINITE) != WAIT_OBJECT_0) { + errno = EAGAIN; + return -1; + } return 0; } -#endif + +// mingw and the likes provide their own usleep() which we need to +// replace with one that can be interrupted by APC function if needed +// (used in mp_thread_gc) +#undef usleep + +int usleep(__int64 usec) { + return usleep_impl(usec); +} + +void msec_sleep(double msec) { + if (msec < 0.0) { + msec = 0.0; + } + const double usec = msec * 1000.0; + usleep_impl(usec > (double)LLONG_MAX ? LLONG_MAX : (__int64)usec); +} void mp_hal_delay_ms(mp_uint_t ms) { #if MICROPY_ENABLE_SCHEDULER