Combining Windows message loop with Swift's run loop

Hi,

I've been working on a Swift application for Windows. I designed the application main to combine the Windows message loop along with Swift's run loop, based on: swift-win32/ApplicationMain.swift at b7afacc96cbbd3e3a2bacf5c37cb9e9c22ab58ae · compnerd/swift-win32 · GitHub

/// Implements the Windows message loop together with Swift's run loop.
public func ApplicationMain() -> UINT {
    var msg: MSG = MSG()
    var nExitCode: UINT = UINT(EXIT_SUCCESS)

    mainLoop: while true {
        // Process all messages in thread's message queue; for GUI applications UI
        // events must have high priority.
        while PeekMessageW(&msg, nil, 0, 0, UINT(PM_REMOVE)) {
            if msg.message == UINT(WM_QUIT) {
                nExitCode = UINT(msg.wParam)
                break mainLoop
            }

            TranslateMessage(&msg)
            DispatchMessageW(&msg)
        }

        var time: Date? = nil
        repeat {
            // Execute Foundation.RunLoop once and determine the next time the timer
            // fires.  At this point handle all Foundation.RunLoop timers, sources and
            // Dispatch.DispatchQueue.main tasks
            time = RunLoop.current.limitDate(forMode: .default)
            // If Foundation.RunLoop doesn't contain any timers or the timers should
            // not be running right now, we interrupt the current loop or otherwise
            // continue to the next iteration.
        } while (time?.timeIntervalSinceNow ?? -1) <= 0

        // Yield control to the system until the earlier of a requisite timer
        // expiration or a message is posted to the runloop.
        _ = MsgWaitForMultipleObjects(0, nil, false,
                DWORD(exactly: ((time?.timeIntervalSinceNow ?? -1) * 1000).rounded(.towardZero))
                        ?? INFINITE,
                QS_ALLINPUT | DWORD(QS_KEY) | QS_MOUSE | DWORD(QS_RAWINPUT))
    }
    return nExitCode
}

The main problem arises when RunLoop.current.limitDate(forMode: .default) returns a specific point in time (Date), which can be affected by external factors like the computer going to sleep or the system time being changed. When these events occur, the timer's fire date remains in the past, causing it to not fire as expected. This ultimately results in an infinite loop of runloop executions, as there is a timer to be fired, but it won't trigger due to being in the past.

In my specific case, I have a timer set to fire daily, but its fire date is almost always missed due to these external factors.

Ideally, the runloop would also execute all timers in the past or limitDate would return a time interval that is not affected by external factors. Unfortunately, I could not find any workaround to achieve this.
I'm seeking guidance on how to better design my main loop to ensure that timers fire correctly, without relying on the system date. Any suggestions or insights would be greatly appreciated!

I should note that I am new to swift.

Thank you!

CC: @etcwilde

Evan may have thoughts on how to integrate runloops.

After looking at the Foundation source code, I discovered that RunLoop.run internally calls MsgWaitForMultipleObjects, allowing it to handle the Windows message loop as well. However, the function _CFRunLoopSetWindowsMessageQueueMask from CoreFoundation (swift-corelibs-foundation/CFRunLoop.c at main · apple/swift-corelibs-foundation · GitHub), which enables specifying which Windows messages should wake MsgWaitForMultipleObjects, is unfortunately inaccessible from the Swift API.

Without the ability to define the messages that wake MsgWaitForMultipleObjects inside the run loop, we must revert to using the main loop provided in my initial post, which has its own set of issues, as previously mentioned.

Therefore, I suggest making the _CFRunLoopSetWindowsMessageQueueMask function accessible through the Swift API, as it would simplify and stabilize application development for Windows.