Currently, the process's main thread runs `main()` -> `__wine_main()` -> `apple_main_thread()` -> `CFRunLoopRun()`. A run loop source is added that creates the Wine main thread and runs `start_main_thread()`.
With this MR, the Wine main thread calls `transform_mac_main_thread()` in `server_init_process()`. That essentially calls `NtCreateThreadEx()`, but instead of creating a new pthread it adds a source to the main thread run loop to run `start_thread()`. This transforms the process main thread into a Wine thread, after that it calls the PE `__wine_mac_run_cfrunloop()` in ntdll. That does a run_mac_cfrunloop Unix call, which just runs `CFRunLoopRun()`. The process main thread is now a Wine thread, but is otherwise still a normal Mac application main thread (including the ability for winemac to transform into a Cocoa application).
In theory it might be possible to avoid the initial `CFRunLoopRun()` and have `apple_main_thread()` just spawn the Wine main thread and wait to run `start_thread()`, but the extra `CFRunLoopRun()` on the stack doesn't hurt anything.
An exception/crash on the process main thread will be caught like a normal syscall fault, but `__wine_mac_run_cfrunloop()` prints an error and terminates the process. I think this is an improvement over the current behavior, which is a segfault in the signal handler when it tries to access the non-existent TEB that results in the thread hanging and eating 100% CPU. (Arguably it would be even better to unregister the signal handler and re-throw the signal so the user gets a normal macOS crash report with a helpful backtrace, maybe this could be done later.)
Besides exception handling, having the main thread be a Wine thread will also allow the full use of Wine logging functions (!9503), and should allow winemac to be simplified (where many AppKit functions must be called from the main thread, and events are delivered to the main thread).
From: Brendan Shanks bshanks@codeweavers.com
--- dlls/ntdll/unix/thread.c | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-)
diff --git a/dlls/ntdll/unix/thread.c b/dlls/ntdll/unix/thread.c index 24cd05877b4..189c174f58f 100644 --- a/dlls/ntdll/unix/thread.c +++ b/dlls/ntdll/unix/thread.c @@ -1310,6 +1310,29 @@ static NTSTATUS update_attr_list( PS_ATTRIBUTE_LIST *attr, HANDLE thread, const return status; }
+ +/*********************************************************************** + * create_pthread + */ +static NTSTATUS create_pthread( struct ntdll_thread_data *thread_data, TEB *teb ) +{ + pthread_t pthread_id; + pthread_attr_t pthread_attr; + NTSTATUS status = STATUS_SUCCESS; + + pthread_attr_init( &pthread_attr ); + pthread_attr_setstack( &pthread_attr, thread_data->kernel_stack, kernel_stack_size ); + pthread_attr_setguardsize( &pthread_attr, 0 ); + pthread_attr_setscope( &pthread_attr, PTHREAD_SCOPE_SYSTEM ); /* force creating a kernel thread */ + + if (pthread_create( &pthread_id, &pthread_attr, (void * (*)(void *))start_thread, teb )) + status = STATUS_NO_MEMORY; + + pthread_attr_destroy( &pthread_attr ); + return status; +} + + /*********************************************************************** * NtCreateThread (NTDLL.@) */ @@ -1334,8 +1357,6 @@ NTSTATUS WINAPI NtCreateThreadEx( HANDLE *handle, ACCESS_MASK access, OBJECT_ATT THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER | THREAD_CREATE_FLAGS_SKIP_LOADER_INIT | THREAD_CREATE_FLAGS_BYPASS_PROCESS_FREEZE; sigset_t sigset; - pthread_t pthread_id; - pthread_attr_t pthread_attr; data_size_t len; struct object_attributes *objattr; struct ntdll_thread_data *thread_data; @@ -1442,18 +1463,12 @@ NTSTATUS WINAPI NtCreateThreadEx( HANDLE *handle, ACCESS_MASK access, OBJECT_ATT thread_data->start = start; thread_data->param = param;
- pthread_attr_init( &pthread_attr ); - pthread_attr_setstack( &pthread_attr, thread_data->kernel_stack, kernel_stack_size ); - pthread_attr_setguardsize( &pthread_attr, 0 ); - pthread_attr_setscope( &pthread_attr, PTHREAD_SCOPE_SYSTEM ); /* force creating a kernel thread */ InterlockedIncrement( &nb_threads ); - if (pthread_create( &pthread_id, &pthread_attr, (void * (*)(void *))start_thread, teb )) + if ((status = create_pthread( thread_data, teb ))) { InterlockedDecrement( &nb_threads ); virtual_free_teb( teb ); - status = STATUS_NO_MEMORY; } - pthread_attr_destroy( &pthread_attr );
done: pthread_sigmask( SIG_SETMASK, &sigset, NULL );
From: Brendan Shanks bshanks@codeweavers.com
--- dlls/ntdll/unix/thread.c | 46 +++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 17 deletions(-)
diff --git a/dlls/ntdll/unix/thread.c b/dlls/ntdll/unix/thread.c index 189c174f58f..d6a6f505d59 100644 --- a/dlls/ntdll/unix/thread.c +++ b/dlls/ntdll/unix/thread.c @@ -1334,24 +1334,12 @@ static NTSTATUS create_pthread( struct ntdll_thread_data *thread_data, TEB *teb
/*********************************************************************** - * NtCreateThread (NTDLL.@) - */ -NTSTATUS WINAPI NtCreateThread( HANDLE *handle, ACCESS_MASK access, OBJECT_ATTRIBUTES *attr, - HANDLE process, CLIENT_ID *id, CONTEXT *ctx, INITIAL_TEB *teb, - BOOLEAN suspended ) -{ - FIXME( "%p %d %p %p %p %p %p %d, stub!\n", - handle, access, attr, process, id, ctx, teb, suspended ); - return STATUS_NOT_IMPLEMENTED; -} - -/*********************************************************************** - * NtCreateThreadEx (NTDLL.@) + * create_thread */ -NTSTATUS WINAPI NtCreateThreadEx( HANDLE *handle, ACCESS_MASK access, OBJECT_ATTRIBUTES *attr, - HANDLE process, PRTL_THREAD_START_ROUTINE start, void *param, - ULONG flags, ULONG_PTR zero_bits, SIZE_T stack_commit, - SIZE_T stack_reserve, PS_ATTRIBUTE_LIST *attr_list ) +static NTSTATUS create_thread( HANDLE *handle, ACCESS_MASK access, OBJECT_ATTRIBUTES *attr, + HANDLE process, PRTL_THREAD_START_ROUTINE start, void *param, + ULONG flags, ULONG_PTR zero_bits, SIZE_T stack_commit, + SIZE_T stack_reserve, PS_ATTRIBUTE_LIST *attr_list ) { static const ULONG supported_flags = THREAD_CREATE_FLAGS_CREATE_SUSPENDED | THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH | THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER | THREAD_CREATE_FLAGS_SKIP_LOADER_INIT | @@ -1482,6 +1470,30 @@ done: return status; }
+/*********************************************************************** + * NtCreateThread (NTDLL.@) + */ +NTSTATUS WINAPI NtCreateThread( HANDLE *handle, ACCESS_MASK access, OBJECT_ATTRIBUTES *attr, + HANDLE process, CLIENT_ID *id, CONTEXT *ctx, INITIAL_TEB *teb, + BOOLEAN suspended ) +{ + FIXME( "%p %d %p %p %p %p %p %d, stub!\n", + handle, access, attr, process, id, ctx, teb, suspended ); + return STATUS_NOT_IMPLEMENTED; +} + +/*********************************************************************** + * NtCreateThreadEx (NTDLL.@) + */ +NTSTATUS WINAPI NtCreateThreadEx( HANDLE *handle, ACCESS_MASK access, OBJECT_ATTRIBUTES *attr, + HANDLE process, PRTL_THREAD_START_ROUTINE start, void *param, + ULONG flags, ULONG_PTR zero_bits, SIZE_T stack_commit, + SIZE_T stack_reserve, PS_ATTRIBUTE_LIST *attr_list ) +{ + return create_thread( handle, access, attr, process, start, param, flags, + zero_bits, stack_commit, stack_reserve, attr_list ); +} +
/*********************************************************************** * abort_thread
From: Brendan Shanks bshanks@codeweavers.com
--- dlls/ntdll/unix/thread.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/dlls/ntdll/unix/thread.c b/dlls/ntdll/unix/thread.c index d6a6f505d59..fa41412c4e3 100644 --- a/dlls/ntdll/unix/thread.c +++ b/dlls/ntdll/unix/thread.c @@ -1339,7 +1339,8 @@ static NTSTATUS create_pthread( struct ntdll_thread_data *thread_data, TEB *teb static NTSTATUS create_thread( HANDLE *handle, ACCESS_MASK access, OBJECT_ATTRIBUTES *attr, HANDLE process, PRTL_THREAD_START_ROUTINE start, void *param, ULONG flags, ULONG_PTR zero_bits, SIZE_T stack_commit, - SIZE_T stack_reserve, PS_ATTRIBUTE_LIST *attr_list ) + SIZE_T stack_reserve, PS_ATTRIBUTE_LIST *attr_list, + NTSTATUS (*thread_create)( struct ntdll_thread_data *thread_data, TEB *teb ) ) { static const ULONG supported_flags = THREAD_CREATE_FLAGS_CREATE_SUSPENDED | THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH | THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER | THREAD_CREATE_FLAGS_SKIP_LOADER_INIT | @@ -1452,7 +1453,7 @@ static NTSTATUS create_thread( HANDLE *handle, ACCESS_MASK access, OBJECT_ATTRIB thread_data->param = param;
InterlockedIncrement( &nb_threads ); - if ((status = create_pthread( thread_data, teb ))) + if ((status = thread_create( thread_data, teb ))) { InterlockedDecrement( &nb_threads ); virtual_free_teb( teb ); @@ -1491,7 +1492,7 @@ NTSTATUS WINAPI NtCreateThreadEx( HANDLE *handle, ACCESS_MASK access, OBJECT_ATT SIZE_T stack_reserve, PS_ATTRIBUTE_LIST *attr_list ) { return create_thread( handle, access, attr, process, start, param, flags, - zero_bits, stack_commit, stack_reserve, attr_list ); + zero_bits, stack_commit, stack_reserve, attr_list, create_pthread ); }
From: Brendan Shanks bshanks@codeweavers.com
--- dlls/ntdll/unix/loader.c | 16 ++++++++++++++++ dlls/ntdll/unixlib.h | 1 + 2 files changed, 17 insertions(+)
diff --git a/dlls/ntdll/unix/loader.c b/dlls/ntdll/unix/loader.c index cb700dd80e8..dbfb994282b 100644 --- a/dlls/ntdll/unix/loader.c +++ b/dlls/ntdll/unix/loader.c @@ -1129,6 +1129,20 @@ static NTSTATUS unwind_builtin_dll( void *args ) #endif /* SO_DLLS_SUPPORTED */
+/*********************************************************************** + * run_mac_cfrunloop + */ +static NTSTATUS run_mac_cfrunloop( void *args ) +{ +#ifdef __APPLE__ + CFRunLoopRun(); /* Should never return, except on error. */ + return STATUS_SUCCESS; +#else + return STATUS_NOT_IMPLEMENTED; +#endif +} + + static const unixlib_entry_t unix_call_funcs[] = { load_so_dll, @@ -1139,6 +1153,7 @@ static const unixlib_entry_t unix_call_funcs[] = unixcall_wine_server_handle_to_fd, unixcall_wine_spawnvp, system_time_precise, + run_mac_cfrunloop, };
@@ -1157,6 +1172,7 @@ const unixlib_entry_t unix_call_wow64_funcs[] = wow64_wine_server_handle_to_fd, wow64_wine_spawnvp, system_time_precise, + run_mac_cfrunloop, };
#endif /* _WIN64 */ diff --git a/dlls/ntdll/unixlib.h b/dlls/ntdll/unixlib.h index 9cb444342fe..ef2f974f59a 100644 --- a/dlls/ntdll/unixlib.h +++ b/dlls/ntdll/unixlib.h @@ -76,6 +76,7 @@ enum ntdll_unix_funcs unix_wine_server_handle_to_fd, unix_wine_spawnvp, unix_system_time_precise, + unix_mac_run_cfrunloop, };
extern unixlib_handle_t __wine_unixlib_handle;
From: Brendan Shanks bshanks@codeweavers.com
--- dlls/ntdll/loader.c | 20 ++++++++++++++++++++ dlls/ntdll/ntdll.spec | 1 + 2 files changed, 21 insertions(+)
diff --git a/dlls/ntdll/loader.c b/dlls/ntdll/loader.c index c5c34935efd..d25696e5ad9 100644 --- a/dlls/ntdll/loader.c +++ b/dlls/ntdll/loader.c @@ -3405,6 +3405,26 @@ NTSTATUS WINAPI __wine_unix_spawnvp( char * const argv[], int wait ) }
+/*********************************************************************** + * __wine_mac_run_cfrunloop + */ +NTSTATUS WINAPI __wine_mac_run_cfrunloop(void) +{ + static const WCHAR nameW[] = L"wine_mac_main_thread"; + THREAD_NAME_INFORMATION info; + NTSTATUS status; + + info.ThreadName.Length = info.ThreadName.MaximumLength = lstrlenW( nameW ) * sizeof(WCHAR); + info.ThreadName.Buffer = (WCHAR *)nameW; + NtSetInformationThread( GetCurrentThread(), ThreadNameInformation, &info, sizeof(info) ); + + status = WINE_UNIX_CALL( unix_mac_run_cfrunloop, NULL ); /* Should never return, except on error. */ + + ERR("macOS main thread crashed with status %lx, exiting\n", status); + for (;;) NtTerminateProcess( GetCurrentProcess(), status ); +} + + /*********************************************************************** * wine_server_call */ diff --git a/dlls/ntdll/ntdll.spec b/dlls/ntdll/ntdll.spec index 46cd1dccab5..38aa69de45b 100644 --- a/dlls/ntdll/ntdll.spec +++ b/dlls/ntdll/ntdll.spec @@ -1749,6 +1749,7 @@ # Unix interface @ stdcall __wine_unix_spawnvp(long ptr) @ stdcall __wine_ctrl_routine(ptr) +@ stdcall __wine_mac_run_cfrunloop() @ extern -private __wine_syscall_dispatcher @ extern -private __wine_unix_call_dispatcher @ extern -private -arch=arm64ec __wine_unix_call_dispatcher_arm64ec
From: Brendan Shanks bshanks@codeweavers.com
--- dlls/ntdll/unix/loader.c | 2 ++ dlls/ntdll/unix/unix_private.h | 1 + 2 files changed, 3 insertions(+)
diff --git a/dlls/ntdll/unix/loader.c b/dlls/ntdll/unix/loader.c index dbfb994282b..3d92d44b4de 100644 --- a/dlls/ntdll/unix/loader.c +++ b/dlls/ntdll/unix/loader.c @@ -110,6 +110,7 @@ void *pKiUserEmulationDispatcher = NULL; void *pLdrInitializeThunk = NULL; void *pRtlUserThreadStart = NULL; void *p__wine_ctrl_routine = NULL; +void *p__wine_mac_run_cfrunloop = NULL; SYSTEM_DLL_INIT_BLOCK *pLdrSystemDllInitBlock = NULL;
static void stub_syscall( const char *name ) @@ -1644,6 +1645,7 @@ static void load_ntdll_functions( HMODULE module ) GET_FUNC( LdrSystemDllInitBlock ); GET_FUNC( RtlUserThreadStart ); GET_FUNC( __wine_ctrl_routine ); + GET_FUNC( __wine_mac_run_cfrunloop ); GET_FUNC( __wine_syscall_dispatcher ); GET_FUNC( __wine_unix_call_dispatcher ); GET_FUNC( __wine_unixlib_handle ); diff --git a/dlls/ntdll/unix/unix_private.h b/dlls/ntdll/unix/unix_private.h index 3932b175a96..46e617490b8 100644 --- a/dlls/ntdll/unix/unix_private.h +++ b/dlls/ntdll/unix/unix_private.h @@ -170,6 +170,7 @@ extern void *pKiUserEmulationDispatcher; extern void *pLdrInitializeThunk; extern void *pRtlUserThreadStart; extern void *p__wine_ctrl_routine; +extern void *p__wine_mac_run_cfrunloop; extern SYSTEM_DLL_INIT_BLOCK *pLdrSystemDllInitBlock;
struct _FILE_FS_DEVICE_INFORMATION;
From: Brendan Shanks bshanks@codeweavers.com
--- dlls/ntdll/unix/server.c | 5 +++ dlls/ntdll/unix/thread.c | 64 ++++++++++++++++++++++++++++++++++ dlls/ntdll/unix/unix_private.h | 1 + 3 files changed, 70 insertions(+)
diff --git a/dlls/ntdll/unix/server.c b/dlls/ntdll/unix/server.c index 258a959de72..84f89367545 100644 --- a/dlls/ntdll/unix/server.c +++ b/dlls/ntdll/unix/server.c @@ -1751,6 +1751,11 @@ void server_init_process_done(void) thread_data->syscall_table = KeServiceDescriptorTable; thread_data->syscall_trace = TRACE_ON(syscall);
+#ifdef __APPLE__ + /* This must run after signal_init_process() so the syscall dispatcher pointer is present at 0x7ffe1000 */ + transform_mac_main_thread(); +#endif + /* always send the native TEB */ if (!(teb = NtCurrentTeb64())) teb = NtCurrentTeb();
diff --git a/dlls/ntdll/unix/thread.c b/dlls/ntdll/unix/thread.c index fa41412c4e3..329e0119d6b 100644 --- a/dlls/ntdll/unix/thread.c +++ b/dlls/ntdll/unix/thread.c @@ -63,6 +63,8 @@ #endif
#ifdef __APPLE__ +#include <CoreFoundation/CoreFoundation.h> +#include <dispatch/dispatch.h> #include <mach/mach.h> #endif #ifdef __FreeBSD__ @@ -1471,6 +1473,7 @@ done: return status; }
+ /*********************************************************************** * NtCreateThread (NTDLL.@) */ @@ -2771,3 +2774,64 @@ NTSTATUS WINAPI NtGetNextThread( HANDLE process, HANDLE thread, ACCESS_MASK acce *handle = ret_handle; return ret; } + + +#ifdef __APPLE__ +static dispatch_semaphore_t main_thread_transformed_semaphore; + +static void signal_semaphore_and_start_thread( TEB *teb ) +{ + dispatch_semaphore_signal( main_thread_transformed_semaphore ); + start_thread( teb ); +} + + +/*********************************************************************** + * run_on_mac_main_thread + */ +static NTSTATUS run_on_mac_main_thread( struct ntdll_thread_data *thread_data, TEB *teb ) +{ + CFRunLoopSourceContext source_context = { 0 }; + CFRunLoopSourceRef source; + + source_context.perform = (void (*)(void *))signal_semaphore_and_start_thread; + source_context.info = teb; + source = CFRunLoopSourceCreate( NULL, 0, &source_context ); + if (!source) + return STATUS_NO_MEMORY; + + CFRunLoopAddSource( CFRunLoopGetMain(), source, kCFRunLoopCommonModes ); + CFRunLoopSourceSignal( source ); + CFRunLoopWakeUp( CFRunLoopGetMain() ); + CFRelease( source ); + return STATUS_SUCCESS; +} + + +/*********************************************************************** + * transform_mac_main_thread + * + * Transform the process main thread into a Wine thread + */ +void transform_mac_main_thread( void ) +{ + NTSTATUS status; + HANDLE handle; + + main_thread_transformed_semaphore = dispatch_semaphore_create( 0 ); + + status = create_thread( &handle, THREAD_ALL_ACCESS, NULL, NtCurrentProcess(), + p__wine_mac_run_cfrunloop, NULL, + THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER | THREAD_CREATE_FLAGS_SKIP_LOADER_INIT, + 0, 0, 0, NULL, run_on_mac_main_thread ); + if (!status) + { + NtClose( handle ); + dispatch_semaphore_wait( main_thread_transformed_semaphore, DISPATCH_TIME_FOREVER ); + } + else + ERR("Failed to transform main thread: %x\n", status); + + dispatch_release( main_thread_transformed_semaphore ); +} +#endif diff --git a/dlls/ntdll/unix/unix_private.h b/dlls/ntdll/unix/unix_private.h index 46e617490b8..6d2ce3bedc2 100644 --- a/dlls/ntdll/unix/unix_private.h +++ b/dlls/ntdll/unix/unix_private.h @@ -262,6 +262,7 @@ extern void set_process_instrumentation_callback( void *callback ); extern void *get_cpu_area( USHORT machine ); extern void set_thread_id( TEB *teb, DWORD pid, DWORD tid ); extern NTSTATUS init_thread_stack( TEB *teb, ULONG_PTR limit, SIZE_T reserve_size, SIZE_T commit_size ); +extern void transform_mac_main_thread( void ); extern void DECLSPEC_NORETURN abort_thread( int status ); extern void DECLSPEC_NORETURN abort_process( int status ); extern void DECLSPEC_NORETURN exit_process( int status );
Wait, why do we need to marshal through the PE side?
On Tue Nov 25 22:01:53 2025 +0000, Elizabeth Figura wrote:
Wait, why do we need to marshal through the PE side?
`start_thread()` jumps to PE code, then we need the Unix call so GSBASE gets set. Also, the PE function lets us handle a syscall fault. I'm not sure I follow what the alternative would be.