[PATCH 0/2] MR10058: ntdll: Spawn main thread as a separate thread.
This makes sure macOS main thread is a Wine thread as in https://gitlab.winehq.org/wine/wine/-/merge_requests/9579, while keeping its event loop usable for GUI, and makes every other platform work the same. Their main thread is parked for now, but the idea is to use the poll loop for various unix side internal purposes, such as winewayland pipe polling, compositing, etc. On macOS the main loop would be used similarly. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058
From: Rémi Bernon <rbernon@codeweavers.com> --- dlls/ntdll/unix/process.c | 12 ++++++------ server/process.c | 6 ++++++ server/protocol.def | 4 ++++ server/thread.c | 1 + 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/dlls/ntdll/unix/process.c b/dlls/ntdll/unix/process.c index 3a47d5a950a..c0c382fc049 100644 --- a/dlls/ntdll/unix/process.c +++ b/dlls/ntdll/unix/process.c @@ -872,11 +872,7 @@ NTSTATUS WINAPI NtCreateUserProcess( HANDLE *process_handle_ptr, HANDLE *thread_ req->flags = thread_flags; req->request_fd = -1; wine_server_add_data( req, objattr, attr_len ); - if (!(status = wine_server_call( req ))) - { - thread_handle = wine_server_ptr_handle( reply->handle ); - id.UniqueThread = ULongToHandle( reply->tid ); - } + status = wine_server_call( req ); } SERVER_END_REQ; free( objattr ); @@ -894,10 +890,14 @@ NTSTATUS WINAPI NtCreateUserProcess( HANDLE *process_handle_ptr, HANDLE *thread_ NtWaitForSingleObject( process_info, FALSE, NULL ); SERVER_START_REQ( get_new_process_info ) { - req->info = wine_server_obj_handle( process_info ); + req->info = wine_server_obj_handle( process_info ); + req->access = thread_access; + req->attributes = thread_attr ? thread_attr->Attributes : 0; wine_server_call( req ); success = reply->success; status = reply->exit_code; + thread_handle = wine_server_ptr_handle( reply->handle ); + id.UniqueThread = ULongToHandle( reply->tid ); } SERVER_END_REQ; diff --git a/server/process.c b/server/process.c index b30c835f74f..91c4a76326c 100644 --- a/server/process.c +++ b/server/process.c @@ -1412,6 +1412,12 @@ DECL_HANDLER(get_new_process_info) if ((info = (struct startup_info *)get_handle_obj( current->process, req->info, 0, &startup_info_ops ))) { + struct thread *thread; + if ((thread = get_process_first_thread( info->process ))) + { + reply->tid = get_thread_id( thread ); + reply->handle = alloc_handle_no_access_check( current->process, thread, req->access, req->attributes ); + } reply->success = is_process_init_done( info->process ); reply->exit_code = info->process->exit_code; release_object( info ); diff --git a/server/protocol.def b/server/protocol.def index 0ac64297026..f7cecc336f0 100644 --- a/server/protocol.def +++ b/server/protocol.def @@ -1109,7 +1109,11 @@ typedef volatile struct /* Retrieve information about a newly started process */ @REQ(get_new_process_info) obj_handle_t info; /* info handle returned from new_process_request */ + unsigned int access; /* wanted handle access rights */ + unsigned int attributes; /* handle object attributes */ @REPLY + thread_id_t tid; /* main thread id */ + obj_handle_t handle; /* main thread handle (in the current process) */ int success; /* did the process start successfully? */ int exit_code; /* process exit code if failed */ @END diff --git a/server/thread.c b/server/thread.c index 3aed496450a..174d97539f5 100644 --- a/server/thread.c +++ b/server/thread.c @@ -1703,6 +1703,7 @@ DECL_HANDLER(new_thread) thread->dbg_hidden = !!(req->flags & THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER); thread->bypass_proc_suspend = !!(req->flags & THREAD_CREATE_FLAGS_BYPASS_PROCESS_FREEZE); reply->tid = get_thread_id( thread ); + if (request_fd == -1) goto done; /* thread handle will be returned from get_new_process_info */ if ((reply->handle = alloc_handle_no_access_check( current->process, thread, req->access, objattr->attributes ))) { -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10058
From: Rémi Bernon <rbernon@codeweavers.com> --- dlls/ntdll/Makefile.in | 1 + dlls/ntdll/unix/loader.c | 50 ---------------------------------- dlls/ntdll/unix/sched.c | 48 ++++++++++++++++++++++++++++++++ dlls/ntdll/unix/server.c | 14 ++++++++-- dlls/ntdll/unix/unix_private.h | 2 ++ server/process.c | 11 +++++++- server/protocol.def | 1 + 7 files changed, 74 insertions(+), 53 deletions(-) create mode 100644 dlls/ntdll/unix/sched.c diff --git a/dlls/ntdll/Makefile.in b/dlls/ntdll/Makefile.in index ad5c3bdc60f..5515e836129 100644 --- a/dlls/ntdll/Makefile.in +++ b/dlls/ntdll/Makefile.in @@ -56,6 +56,7 @@ SOURCES = \ unix/loadorder.c \ unix/process.c \ unix/registry.c \ + unix/sched.c \ unix/security.c \ unix/serial.c \ unix/server.c \ diff --git a/dlls/ntdll/unix/loader.c b/dlls/ntdll/unix/loader.c index 28fc09cd354..6f0f3a23bbd 100644 --- a/dlls/ntdll/unix/loader.c +++ b/dlls/ntdll/unix/loader.c @@ -1998,45 +1998,8 @@ jint JNI_OnLoad( JavaVM *vm, void *reserved ) #endif /* __ANDROID__ */ #ifdef __APPLE__ -static void *apple_wine_thread( void *arg ) -{ - start_main_thread(); - return NULL; -} - -/*********************************************************************** - * apple_create_wine_thread - * - * Spin off a secondary thread to complete Wine initialization, leaving - * the original thread for the Mac frameworks. - * - * Invoked as a CFRunLoopSource perform callback. - */ -static void apple_create_wine_thread( void *arg ) -{ - pthread_t thread; - pthread_attr_t attr; - - pthread_attr_init( &attr ); - pthread_attr_setdetachstate( &attr, PTHREAD_CREATE_JOINABLE ); - if (pthread_create( &thread, &attr, apple_wine_thread, NULL )) exit(1); - pthread_attr_destroy( &attr ); -} - - -/*********************************************************************** - * apple_main_thread - * - * Park the process's original thread in a Core Foundation run loop for - * use by the Mac frameworks, especially receiving and handling - * distributed notifications. Spin off a new thread for the rest of the - * Wine initialization. - */ static void apple_main_thread(void) { - CFRunLoopSourceContext source_context = { 0 }; - CFRunLoopSourceRef source; - if (!pthread_main_np()) return; #pragma clang diagnostic push @@ -2051,19 +2014,6 @@ static void apple_main_thread(void) * center scheduled on this thread's run loop. In theory, it's scheduled * in the first thread to ask for it. */ CFNotificationCenterGetDistributedCenter(); - - /* We use this run loop source for two purposes. First, a run loop exits - * if it has no more sources scheduled. So, we need at least one source - * to keep the run loop running. Second, although it's not critical, it's - * preferable for the Wine initialization to not proceed until we know - * the run loop is running. So, we signal our source immediately after - * adding it and have its callback spin off the Wine thread. */ - source_context.perform = apple_create_wine_thread; - source = CFRunLoopSourceCreate( NULL, 0, &source_context ); - CFRunLoopAddSource( CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes ); - CFRunLoopSourceSignal( source ); - CFRelease( source ); - CFRunLoopRun(); /* Should never return, except on error. */ } #endif /* __APPLE__ */ diff --git a/dlls/ntdll/unix/sched.c b/dlls/ntdll/unix/sched.c new file mode 100644 index 00000000000..250ecff44af --- /dev/null +++ b/dlls/ntdll/unix/sched.c @@ -0,0 +1,48 @@ +/* + * Copyright 2026 Rémi Bernon for CodeWeavers + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#if 0 +#pragma makedep unix +#endif + +#include "config.h" + +#include <stddef.h> +#include <stdarg.h> + +#include <poll.h> + +#include "unix_private.h" + +#ifdef __APPLE__ + +#include <CoreFoundation/CoreFoundation.h> + +void sched_run(void) +{ + CFRunLoopRun(); +} + +#else + +void sched_run(void) +{ + for (;;) poll( NULL, 0, -1 ); +} + +#endif diff --git a/dlls/ntdll/unix/server.c b/dlls/ntdll/unix/server.c index 10167d24215..2084a8127c6 100644 --- a/dlls/ntdll/unix/server.c +++ b/dlls/ntdll/unix/server.c @@ -1736,6 +1736,8 @@ void server_init_process_done(void) int suspend; FILE_FS_DEVICE_INFORMATION info; struct ntdll_thread_data *thread_data = ntdll_get_thread_data(); + HANDLE handle; + DWORD flags; if (!get_device_info( initial_cwd, &info ) && (info.Characteristics & FILE_REMOVABLE_MEDIA)) chdir( "/" ); @@ -1755,18 +1757,26 @@ void server_init_process_done(void) /* always send the native TEB */ if (!(teb = NtCurrentTeb64())) teb = NtCurrentTeb(); + flags = THREAD_CREATE_FLAGS_CREATE_SUSPENDED; + status = NtCreateThreadEx( &handle, THREAD_ALL_ACCESS, NULL, NtCurrentProcess(), + main_image_info.TransferAddress, peb, flags, 0, 0, 0, NULL ); + assert( !status ); + /* Signal the parent process to continue */ SERVER_START_REQ( init_process_done ) { + req->main = wine_server_obj_handle( handle ); req->teb = wine_server_client_ptr( teb ); req->peb = NtCurrentTeb64() ? NtCurrentTeb64()->Peb : wine_server_client_ptr( peb ); status = wine_server_call( req ); suspend = reply->suspend; } SERVER_END_REQ; - assert( !status ); - signal_start_thread( main_image_info.TransferAddress, peb, suspend, NtCurrentTeb() ); + if (!suspend) NtResumeThread( handle, NULL ); + NtClose( handle ); + + sched_run(); } diff --git a/dlls/ntdll/unix/unix_private.h b/dlls/ntdll/unix/unix_private.h index 10f6bb2c63c..1ea6ab56d18 100644 --- a/dlls/ntdll/unix/unix_private.h +++ b/dlls/ntdll/unix/unix_private.h @@ -388,6 +388,8 @@ extern NTSTATUS wow64_wine_server_handle_to_fd( void *args ); extern NTSTATUS wow64_wine_spawnvp( void *args ); #endif +extern void sched_run(void); + extern void dbg_init(void); extern void close_inproc_sync( HANDLE handle ); diff --git a/server/process.c b/server/process.c index 91c4a76326c..60e8ae18188 100644 --- a/server/process.c +++ b/server/process.c @@ -1481,12 +1481,21 @@ DECL_HANDLER(get_startup_info) DECL_HANDLER(init_process_done) { struct process *process = current->process; + struct thread *thread; if (is_process_init_done(process)) { set_error( STATUS_INVALID_PARAMETER ); return; } + if (!(thread = get_thread_from_handle( req->main, THREAD_ALL_ACCESS ))) + { + set_error( STATUS_INVALID_PARAMETER ); + return; + } + list_remove( &thread->proc_entry ); + list_add_head( &process->thread_list, &thread->proc_entry ); + release_object( thread ); current->teb = req->teb; process->peb = req->peb; @@ -1500,7 +1509,7 @@ DECL_HANDLER(init_process_done) if (process->image_info.subsystem != IMAGE_SUBSYSTEM_WINDOWS_CUI) process->idle_event = create_event( NULL, NULL, 0, 1, 0, NULL ); if (process->debug_obj) set_process_debug_flag( process, 1 ); - reply->suspend = (current->suspend || process->suspend); + reply->suspend = process->suspend; } /* open a handle to a process */ diff --git a/server/protocol.def b/server/protocol.def index f7cecc336f0..bd54db30a0a 100644 --- a/server/protocol.def +++ b/server/protocol.def @@ -1144,6 +1144,7 @@ typedef volatile struct /* Signal the end of the process initialization */ @REQ(init_process_done) + obj_handle_t main; /* main thread handle */ client_ptr_t teb; /* TEB of new thread (in process address space) */ client_ptr_t peb; /* PEB of new process (in process address space) */ @REPLY -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10058
Fwiw I didn't try swapping TIDs here, like @zfigura said it's hopefully not necessary, only made sure the handle returned when spawning a process is the newly spawn application thread and not the process main thread. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129160
If I understand this correctly, this means that `pgrep -x ShooterGame.exe` does not return the application's main thread as a pid anymore, rather the real main thread is the second thread of the process. Might be of note for any tool messing with e.g. thread priorities / io prio or sampling, etc. Though I'm not at all opposed to this change. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129162
On Mon Feb 9 10:36:59 2026 +0000, Torge Matthies wrote:
If I understand this correctly, this means that `pgrep -x ShooterGame.exe` does not return the application's main thread as a pid anymore, rather the real main thread is the second thread of the process. Might be of note for any tool messing with e.g. thread priorities / io prio or sampling, etc. Though I'm not at all opposed to this change. Yes, that's already the case on macOS, and it would be the case everywhere. Still, fwiw I think the unix main thread would be useful for some extra purposes, I ambition to use it for window surface presentation, but we could also imagine scheduling async I/O there, etc...
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129164
Won't this thread be app visible this way (appear in server's thread list and thus appear in NtGetNextThread or other ways to query threads in the system)? That has to be not app visible. At least, there are DRMs that will check the start address of each thread and will break if that is not a PE start address which they can attribute to valid dll. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129195
On Mon Feb 9 15:23:32 2026 +0000, Paul Gofman wrote:
Won't this thread be app visible this way (appear in server's thread list and thus appear in NtGetNextThread or other ways to query threads in the system)? That has to be not app visible. At least, there are DRMs that will check the start address of each thread and will break if that is not a PE start address which they can attribute to valid dll. It will be visible, though we can maybe try to hide it. Regarding its entry point it has the same entry point as the main thread, as this is initialized when mapping the main executable. The new application main thread is initialized as any other thread and also has `main_image_info.TransferAddress` as entry point. Idk if this is likely to cause any confusion, and whether we should try to use a different address?
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129197
On Mon Feb 9 15:23:32 2026 +0000, Rémi Bernon wrote:
It will be visible, though we can maybe try to hide it. Regarding its entry point it has the same entry point as the main thread, as this is initialized when mapping the main executable. The new application main thread is initialized as any other thread and also has `main_image_info.TransferAddress` as entry point. Idk if this is likely to cause any confusion, and whether we should try to use a different address? So it looks like this visibly diverges from how thing look on Windows. I think the only way is to have it hidden in some way and not appear in any app-visible queries. That would probably need some attribute for this first thread on the server and explicitly excluding it from any user visible queries. That would mean of course that such a thread will have some limitations in what it can do, likely it won't be able to send window messages.
Then, probably the condition when the process is considered terminated should exclude this service thread too? -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129198
won't be able to send window messages
I mean app visible messages of course, not, e. g., hardware input messages to server. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129200
On Mon Feb 9 15:31:55 2026 +0000, Paul Gofman wrote:
So it looks like this visibly diverges from how thing look on Windows. I think the only way is to have it hidden in some way and not appear in any app-visible queries. That would probably need some attribute for this first thread on the server and explicitly excluding it from any user visible queries. That would mean of course that such a thread will have some limitations in what it can do, likely it won't be able to send window messages. Then, probably the condition when the process is considered terminated should exclude this service thread too? Why would it be different from any Wine internal threads? They aren't causing problems in general.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129201
On Mon Feb 9 15:33:43 2026 +0000, Rémi Bernon wrote:
Why would it be different from any Wine internal threads? They aren't causing problems in general. If you mean component-specific threads, like, e. g, what dinput threads, those are different. They are spawned much later in initialization process, only after that component initialization. At this point Windows also has a bunch of various service threads, even if those don't exactly match what we are doing DRMs have to consider those possibility. This thread is going to be active at the process start without loading any specific WINAPI components yet, at this point Windows doesn't have any extra service threads. Then, those threads have some meaningful start address in, e g. dinput.dll and backtrace to that start, and not the bogus start address duplicating the app's main thread.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129202
On Mon Feb 9 15:38:17 2026 +0000, Paul Gofman wrote:
If you mean component-specific threads, like, e. g, what dinput threads, those are different. They are spawned much later in initialization process, only after that component initialization. At this point Windows also has a bunch of various service threads, even if those don't exactly match what we are doing DRMs have to consider those possibility. This thread is going to be active at the process start without loading any specific WINAPI components yet, at this point Windows doesn't have any extra service threads. Then, those threads have some meaningful start address in, e g. dinput.dll and backtrace to that start, and not the bogus start address duplicating the app's main thread. Well the start address is mostly a detail, we could very well put some ntdll address there if necessary. The necessity to keep that thread absolutely hidden on startup is more of an issue. I know it's not happening on Windows but how confident are we that it needs so be like that? At what point is it okay to spawn a thread?
That thread needs to be able to communicate with other application threads, especially it needs to be able to send window messages, otherwise it becomes a bit pointless to have and cannot replace Wayland thread, or be used to simplify macOS driver much either. We could maybe use only Wine internal hardware messages, trying to not leak anything into user space but that becomes tricky and error prone. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129212
On Mon Feb 9 15:48:38 2026 +0000, Rémi Bernon wrote:
Well the start address is mostly a detail, we could very well put some ntdll address there if necessary. The necessity to keep that thread absolutely hidden on startup is more of an issue. I know it's not happening on Windows but how confident are we that it needs so be like that? At what point is it okay to spawn a thread? That thread needs to be able to communicate with other application threads, especially it needs to be able to send window messages, otherwise it becomes a bit pointless to have and cannot replace Wayland thread, or be used to simplify macOS driver much either. We could maybe use only Wine internal hardware messages, trying to not leak anything into user space but that becomes tricky and error prone. A bit of side note, having all the async I/O done in a dedicated thread (and not interrupting the thread which requested IO) has a sync issue when GetOverlappedResult is used (and not GetOverlappedResultEx). Windows docs demand GetOverlappedResult() to be ever called from the same thread which requested I/O, and for a reason. The problem is that GetOverlappedResult() can skip event wait if IOSB is already filled with something not STATUS_PENDING. IOSB is filled before the completion event is set. So if IOSB completion is not interrupting the thread which correclty calls GetOverlappedResult() (delivered to a different thread), IOSB can be filled, app's IO thread sees that without waiting for event and continues to next IO, resetting the event. And then the IOSB completion thread sets the event, leading to premature completion of the next GetOverlappedResult() and it returning before completion. This is a side note because it is not like it works reliably now (currently the completion can be delivered through another thread as well), but maybe something to be kept in mind if we are going to be redesigning then.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129213
Well the start address is mostly a detail, we could very well put some ntdll address there if necessary. The necessity to keep that thread absolutely hidden on startup is more of an issue. I know it's not happening on Windows but how confident are we that it needs so be like that? At what point is it okay to spawn a thread?
Well, I don't believe that some work arounds on this level can work well. It would take some indefinite effort finding out an DRM in the wild as an example which will break this way, but even if we can't find it IMO the right question here is what will we do if we find one (while I did see a few games which do check those threads, previously they were breaking when we had start addresses in old dll.so which were not properly traceable). IMO we better have that right than hope that nothing needs it.
That thread needs to be able to communicate with other application threads, especially it needs to be able to send window messages, otherwise it becomes a bit pointless to have and cannot replace Wayland thread, or be used to simplify macOS driver much either. We could maybe use only Wine internal hardware messages, trying to not leak anything into user space but that becomes tricky and error prone.
I think the question here will be how the corresponding messages look on Windows. If they come from "system" they can probably come from this thread in Wine, if they do not, the question is, what sends those on Windows? If that comes, e. g., from DWM.exe on Windows maybe it can be mimicked through this thread too? -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129215
At what point is it okay to spawn a thread?
I think to spawn a thread which originates from ntdll or app's .exe is never okay because Windows doesn't do that, it should be hidden from start and forever. Or in theory live somewhere in another service process (which is apparently not an interesting solution here, many of the tasks meant for this thread can't be done well this way). -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129219
This merge request was closed by David Kahurani. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058
Also is hiding this thread in the server is too big of a problem? I guess it will have to be explicitly distinguished anyway for proper process termination (see remove_process_thread() which decides that process is dead based on running threads count, apparently that should not consider service thread). -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129224
These guys are ruining our channel -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129226
On Mon Feb 9 16:21:05 2026 +0000, Paul Gofman wrote:
Also is hiding this thread in the server is too big of a problem? I guess it will have to be explicitly distinguished anyway for proper process termination (see remove_process_thread() which decides that process is dead based on running threads count, apparently that should not consider service thread). Then, process suspend... should it be suspended with process suspend? That thing alone can be done with now existing Windows-supported thread flag, but also having service thread to be recognizable to account for all the cases when needed doesn't sound too bad maybe?
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10058#note_129227
participants (5)
-
David Kahurani (@ReDress) -
Paul Gofman (@gofman) -
Rémi Bernon -
Rémi Bernon (@rbernon) -
Torge Matthies (@tmatthies)