[PATCH v2 0/2] MR11333: winemac, ntdll: Convert the main thread to a Wine system thread when needed.
Similar to what https://gitlab.winehq.org/wine/wine/-/merge_requests/9579 and https://gitlab.winehq.org/wine/wine/-/merge_requests/10058 were doing. This is just for winemac, the magic happens in ntdll where CFRunLoopRun start proc is treated specifically by `spawn_thread` and doesn't actually spawns a new thread but schedules a server_init_thread call in the pre-existing main loop, initializing Wine thread data and recursively calling the macOS main loop without (I think?) returning from it. It then makes it possible at least to use Wine tracing subsystem in winemac main thread, and ultimately should make it possible to call some win32u functions directly from it too. We cannot process window messages there, it's a system thread without a user stack, but it can interact with win32u state and post messages to other threads. It's still going to be a long road ahead if we want to replace winemac internal events (which I think would be nice), because many things still expect some winemac specific threading, but it would be possible at least. Ultimately I still think that it would be nice to generalize this "main" system thread into a ntdll "sched"-uling subsystem that could back all of the win32u drivers host fd polling / UI event processing / scheduling needs, including for an in-process window compositor, as well as any extra unixlib fd polling / timer / scheduling we might find useful to have. There was for instance some discussion about how to integrate DBus connection polling nicely, and this could use it. On macOS this would be the main thread, on other platforms a system thread would be spawned as needed. -- v2: ntdll: Allow converting the macOS main thread into a Wine thread. winemac: Return early on failures in macdrv_start_cocoa_app. https://gitlab.winehq.org/wine/wine/-/merge_requests/11333
From: Rémi Bernon <rbernon@codeweavers.com> --- dlls/winemac.drv/cocoa_main.m | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/dlls/winemac.drv/cocoa_main.m b/dlls/winemac.drv/cocoa_main.m index 8c9507a75b1..b3190d80e2a 100644 --- a/dlls/winemac.drv/cocoa_main.m +++ b/dlls/winemac.drv/cocoa_main.m @@ -114,7 +114,7 @@ int macdrv_start_cocoa_app(unsigned long long tickcount) { int ret = -1; CFRunLoopSourceRef source; - struct cocoa_app_startup_info startup_info; + struct cocoa_app_startup_info startup_info = { .tickcount = tickcount }; uint64_t uptime_mach = mach_absolute_time(); mach_timebase_info_data_t mach_timebase; NSDate* timeLimit; @@ -126,24 +126,20 @@ int macdrv_start_cocoa_app(unsigned long long tickcount) toTarget:[NSThread class] withObject:nil]; - startup_info.lock = [[NSConditionLock alloc] initWithCondition:COCOA_APP_NOT_RUNNING]; - startup_info.tickcount = tickcount; - startup_info.success = FALSE; + if (!(timeLimit = [NSDate dateWithTimeIntervalSinceNow:5])) return -1; + if (!(startup_info.lock = [[NSConditionLock alloc] initWithCondition:COCOA_APP_NOT_RUNNING])) return -1; mach_timebase_info(&mach_timebase); startup_info.uptime_ns = uptime_mach * mach_timebase.numer / mach_timebase.denom; - timeLimit = [NSDate dateWithTimeIntervalSinceNow:5]; - source_context.info = &startup_info; source_context.perform = run_cocoa_app; - source = CFRunLoopSourceCreate(NULL, 0, &source_context); - - if (source && startup_info.lock && timeLimit) + if ((source = CFRunLoopSourceCreate(NULL, 0, &source_context))) { CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopCommonModes); CFRunLoopSourceSignal(source); CFRunLoopWakeUp(CFRunLoopGetMain()); + CFRelease(source); if ([startup_info.lock lockWhenCondition:COCOA_APP_RUNNING beforeDate:timeLimit]) { @@ -152,8 +148,6 @@ int macdrv_start_cocoa_app(unsigned long long tickcount) } } - if (source) - CFRelease(source); [startup_info.lock release]; return ret; } -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11333
From: Rémi Bernon <rbernon@codeweavers.com> --- dlls/ntdll/unix/thread.c | 19 +++++++++++++++++++ dlls/winemac.drv/cocoa_main.m | 14 ++++++++------ dlls/winemac.drv/macdrv_cocoa.h | 1 + dlls/winemac.drv/macdrv_main.c | 11 +++++++++++ 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/dlls/ntdll/unix/thread.c b/dlls/ntdll/unix/thread.c index 3ab7cb9c283..c5468039ecb 100644 --- a/dlls/ntdll/unix/thread.c +++ b/dlls/ntdll/unix/thread.c @@ -64,6 +64,7 @@ #ifdef __APPLE__ #include <mach/mach.h> +#include <CoreFoundation/CoreFoundation.h> #endif #ifdef __FreeBSD__ #include <sys/thr.h> @@ -1316,6 +1317,24 @@ static NTSTATUS spawn_thread( struct thread_data *data ) pthread_attr_t attr; NTSTATUS status = STATUS_SUCCESS; +#ifdef __APPLE__ + if (data->start == CFRunLoopRun) + { + CFRunLoopSourceContext context = { .perform = (void *)server_init_thread, .info = data }; + CFRunLoopSourceRef source; + + if (!(source = CFRunLoopSourceCreate( NULL, 0, &context ))) return STATUS_NO_MEMORY; + + InterlockedIncrement( &nb_threads ); + CFRunLoopAddSource( CFRunLoopGetMain(), source, kCFRunLoopCommonModes ); + CFRunLoopSourceSignal( source ); + CFRunLoopWakeUp( CFRunLoopGetMain() ); + CFRelease( source ); + + return STATUS_SUCCESS; + } +#endif /* __APPLE__ */ + pthread_sigmask( SIG_BLOCK, &server_block_set, &sigset ); pthread_attr_init( &attr ); pthread_attr_setstack( &attr, get_kernel_stack( data ), kernel_stack_size ); diff --git a/dlls/winemac.drv/cocoa_main.m b/dlls/winemac.drv/cocoa_main.m index b3190d80e2a..29db337607b 100644 --- a/dlls/winemac.drv/cocoa_main.m +++ b/dlls/winemac.drv/cocoa_main.m @@ -100,6 +100,14 @@ static void run_cocoa_app(void* info) } } +void macdrv_init_cocoa_threads(void) +{ + /* Make sure Cocoa is in multi-threading mode by detaching a + do-nothing thread. */ + [NSThread detachNewThreadSelector:@selector(self) + toTarget:[NSThread class] + withObject:nil]; +} /*********************************************************************** * macdrv_start_cocoa_app @@ -120,12 +128,6 @@ int macdrv_start_cocoa_app(unsigned long long tickcount) NSDate* timeLimit; CFRunLoopSourceContext source_context = { 0 }; - /* Make sure Cocoa is in multi-threading mode by detaching a - do-nothing thread. */ - [NSThread detachNewThreadSelector:@selector(self) - toTarget:[NSThread class] - withObject:nil]; - if (!(timeLimit = [NSDate dateWithTimeIntervalSinceNow:5])) return -1; if (!(startup_info.lock = [[NSConditionLock alloc] initWithCondition:COCOA_APP_NOT_RUNNING])) return -1; diff --git a/dlls/winemac.drv/macdrv_cocoa.h b/dlls/winemac.drv/macdrv_cocoa.h index 9a91edbe8e3..6026b53054a 100644 --- a/dlls/winemac.drv/macdrv_cocoa.h +++ b/dlls/winemac.drv/macdrv_cocoa.h @@ -200,6 +200,7 @@ static inline CGPoint cgpoint_win_from_mac(CGPoint point) return point; } +extern void macdrv_init_cocoa_threads(void); extern int macdrv_start_cocoa_app(unsigned long long tickcount); extern void macdrv_window_rejected_focus(const struct macdrv_event *event); extern void macdrv_beep(void); diff --git a/dlls/winemac.drv/macdrv_main.c b/dlls/winemac.drv/macdrv_main.c index 96168242cc2..39d3efbc577 100644 --- a/dlls/winemac.drv/macdrv_main.c +++ b/dlls/winemac.drv/macdrv_main.c @@ -428,6 +428,7 @@ static NTSTATUS macdrv_init(void *arg) struct init_params *params = arg; SessionAttributeBits attributes; OSStatus status; + HANDLE thread; app_icon_callback = params->app_icon_callback; app_quit_request_callback = params->app_quit_request_callback; @@ -442,6 +443,16 @@ static NTSTATUS macdrv_init(void *arg) setup_options(); load_strings(params->strings); + macdrv_init_cocoa_threads(); + + /* convert the main thread to a Wine thread */ + if ((status = PsCreateSystemThread(&thread, THREAD_ALL_ACCESS, NULL, 0, NULL, (void *)CFRunLoopRun, NULL))) + { + ERR("Failed to spawn main thread, status %#x\n", status); + return STATUS_UNSUCCESSFUL; + } + NtClose(thread); + macdrv_err_on = ERR_ON(macdrv); if (macdrv_start_cocoa_app(NtGetTickCount())) { -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11333
v2: Leave the BOOL issue aside, just convert the main thread. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11333#note_148597
On Mon Jul 13 17:55:59 2026 +0000, Tim Clem wrote:
I agree with Brendan here. I think the friction of having to remember to use `bool` in the face of all system headers and existing ObjC code is overwhelming. Well I don't know, but I can confidently say that having to use WINBOOL in place of BOOL in all the driver code is going to be a real pita. Dealing with all of our drivers style differences is already extremely annoying, I would really prefer not having to deal with that too.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11333#note_148598
On Tue Aug 11 12:43:16 2026 +0000, Rémi Bernon wrote:
Well I don't know, but I can confidently say that having to use WINBOOL in place of BOOL in all the driver code is going to be a real pita. Dealing with all of our drivers style differences is already extremely annoying, I would really prefer not having to deal with that too. That's understandable, I think the best route then is to replace `BOOL` with `bool` in all the user funcs. It would be a lot of churn, but using standard data types is ultimately a good thing.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11333#note_148986
I don't know whether we want to have it converted on process launch, but my idea about an ntdll scheduling facility would allow something like that. The initialization would happen lazily, as soon as a unix module needs to use that facility. On macOS that would be backed by CFRunLoopRun and it would be initialized as needed. It doesn't seem necessary to have it on process launch, and it would better match what other platform have to do if we (pretend we) spawn a thread lazily.
Thinking about this more, it seems to me like it really should be done at process launch. Unlike on other platforms the Mac main thread is always running a run loop, and this is a core OS API (not something library-specific). It seems awkward that the main thread can sometimes make Wine calls and sometimes not. winemac is the primary user of the main thread, but it's not the only one. winecoreaudio will likely have a use for it soon too. I'd like to tinker with this more, but it seems like this could/should be done entirely in ntdll without any changes to winemac. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11333#note_148987
On Sat Aug 15 05:20:05 2026 +0000, Brendan Shanks wrote:
I don't know whether we want to have it converted on process launch, but my idea about an ntdll scheduling facility would allow something like that. The initialization would happen lazily, as soon as a unix module needs to use that facility. On macOS that would be backed by CFRunLoopRun and it would be initialized as needed. It doesn't seem necessary to have it on process launch, and it would better match what other platform have to do if we (pretend we) spawn a thread lazily. Thinking about this more, it seems to me like it really should be done at process launch. Unlike on other platforms the Mac main thread is always running a run loop, and this is a core OS API (not something library-specific). It seems awkward that the main thread can sometimes make Wine calls and sometimes not. winemac is the primary user of the main thread, but it's not the only one. winecoreaudio will likely have a use for it soon too. I'd like to tinker with this more, but it seems like this could/should be done entirely in ntdll without any changes to winemac. I think doing it at process launch makes things harder. Process initialization is already complex and we likely need to wait until it's fully finished before we can make additional wineserver requests or risk breaking various assumptions. Doing this in win32u driver initialization like here makes things simple.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11333#note_149037
That's understandable, I think the best route then is to replace `BOOL` with `bool` in all the user funcs. It would be a lot of churn, but using standard data types is ultimately a good thing.
I'm not sure we can change the win32u API. I think the easiest way is to have winemac specific helpers in .c files for any win32u call that needs to be made from the .m sources. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11333#note_149039
I'm not sure we can change the win32u API. I think the easiest way is to have winemac specific helpers in .c files for any win32u call that needs to be made from the .m sources.
We wouldn't need to change the win32u API, ObjC code would be able to call functions with `BOOL` arguments without a problem. i.e. ObjC code would see a prototype of `NtUserGetSystemMenu( HWND hwnd, WINBOOL revert )` and could call that just fine. The only case where `WINBOOL` would show up is when implementing functions that take/return BOOLs and have their pointers passed to win32u, of course there's a lot of these with the user/opengl/vulkan driver funcs. But that's a Wine-internal API that we can change, and since `BOOL`/`bool` cast back and forth without a problem it should just be a matter of changing the types. (You'd also have to use `WINBOOL` if a function takes a `BOOL *`, but I don't see any NtUser functions like this) -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11333#note_149134
On Mon Aug 17 23:09:57 2026 +0000, Brendan Shanks wrote:
I'm not sure we can change the win32u API. I think the easiest way is to have winemac specific helpers in .c files for any win32u call that needs to be made from the .m sources. We wouldn't need to change the win32u API, ObjC code would be able to call functions with `BOOL` arguments without a problem. i.e. ObjC code would see a prototype of `NtUserGetSystemMenu( HWND hwnd, WINBOOL revert )` and could call that just fine. The only case where `WINBOOL` would show up is when implementing functions that take/return BOOLs and have their pointers passed to win32u, of course there's a lot of these with the user/opengl/vulkan driver funcs. But that's a Wine-internal API that we can change, and since `BOOL`/`bool` cast back and forth without a problem it should just be a matter of changing the types. (You'd also have to use `WINBOOL` if a function takes a `BOOL *`, but I don't see any NtUser functions like this) That's probably better than having to type WINBOOL, but it still feels ugly and quite confusing to me that some `winemac` code uses `BOOL` in various places while that `BOOL` is not be the same as the `BOOL` type in some other `winemac`, or every other module, code. There is code that can't be changed, for instance `DllMain`.
Anyway, it's already the case, and I'm more interested in converting the main thread to a Wine thread, than fixing that aspect of things. I think it can stay as it current is, and intermediate helpers be used between C and ObjC code like we already do. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11333#note_149147
On Tue Aug 18 07:32:14 2026 +0000, Rémi Bernon wrote:
That's probably better than having to type WINBOOL, but it still feels ugly and quite confusing to me that some `winemac` code uses `BOOL` in various places while that `BOOL` is not be the same as the `BOOL` type in some other `winemac`, or every other module, code. There is code that can't be changed, for instance `DllMain`. Anyway, it's already the case, and I'm more interested in converting the main thread to a Wine thread, than fixing that aspect of things. I think it can stay as it current is, and intermediate helpers be used between C and ObjC code like we already do. I updated !9579 with a version of this MR that converts the main thread at process launch, and is entirely contained in ntdll. I just don't think winemac should be involved in this.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/11333#note_149633
participants (3)
-
Brendan Shanks (@bshanks) -
Rémi Bernon -
Rémi Bernon (@rbernon)