Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CoWaitForMultipleHandles API doesn't behave as documented

This was triggered by another question I was looking at. It might be too long to read, so please bear with me.

Apparently, CoWaitForMultipleHandles does not behave as documented on MSDN.

The code below (based upon the original question) is a console app, which starts an STA thread with a test Win32 window and tries post and to pump some messages. It does three different tests on CoWaitForMultipleHandles, all without COWAIT_WAITALL flag.

Test #1 is aimed to verify this:

COWAIT_INPUTAVAILABLE If set, the call to CoWaitForMultipleHandles will return S_OK if input exists for the queue, even if the input has been seen (but not removed) using a call to another function, such as PeekMessage.

This is not happening, CoWaitForMultipleHandles blocks and doesn't return until the wait handle is signalled. I do assume that any pending message should be treated as input (same as with MWMO_INPUTAVAILABLE of MsgWaitForMultipleObjectsEx, which works as expected).

Test #2 is aimed to verify this:

COWAIT_DISPATCH_WINDOW_MESSAGES Enables dispatch of window messages from CoWaitForMultipleHandles in an ASTA or STA. Default in ASTA is no window messages dispatched, default in STA is only a small set of special-cased messages dispatched. The value has no meaning in MTA and is ignored.

This doesn't work either. When CoWaitForMultipleHandles is called with COWAIT_DISPATCH_WINDOW_MESSAGES flag alone, it instantly returns error CO_E_NOT_SUPPORTED (0x80004021). If it's a combination of COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS, the call blocks but doesn't pump any messages.

Test #3 demonstrates the only way I could make CoWaitForMultipleHandles pump the Windows message queue of the calling thread. It is a combination of COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS | COWAIT_INPUTAVAILABLE. This does pump and dispatch messages, although apparently it is an undocumented behaviour.

The test code (a ready-to-run console app):

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleTestApp
{
    static class Program
    {
        // Main 
        static void Main(string[] args)
        {
            Console.WriteLine("Starting an STA thread...");
            RunStaThread();

            Console.WriteLine("\nSTA thread finished.");
            Console.WriteLine("Press Enter to exit.");
            Console.ReadLine();
        }

        // start and run an STA thread
        static void RunStaThread()
        {
            var thread = new Thread(() =>
            {
                // create a simple Win32 window
                IntPtr hwnd = CreateTestWindow();

                // Post some WM_TEST messages
                Console.WriteLine("Post some WM_TEST messages...");
                NativeMethods.PostMessage(hwnd, NativeMethods.WM_TEST, new IntPtr(1), IntPtr.Zero);
                NativeMethods.PostMessage(hwnd, NativeMethods.WM_TEST, new IntPtr(2), IntPtr.Zero);
                NativeMethods.PostMessage(hwnd, NativeMethods.WM_TEST, new IntPtr(3), IntPtr.Zero);

                // Test #1
                Console.WriteLine("\nTest #1. CoWaitForMultipleHandles with COWAIT_INPUTAVAILABLE only, press Enter to stop...");
                var task = ReadLineAsync();

                uint index;
                var result = NativeMethods.CoWaitForMultipleHandles(
                    NativeMethods.COWAIT_INPUTAVAILABLE,
                    NativeMethods.INFINITE,
                    1, new[] { task.AsUnmanagedHandle() },
                    out index);
                Console.WriteLine("Result: " + result + ", pending messages in the queue: " + (NativeMethods.GetQueueStatus(0x1FF) >> 16 != 0));

                // Test #2
                Console.WriteLine("\nTest #2. CoWaitForMultipleHandles with COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS, press Enter to stop...");
                task = ReadLineAsync();

                result = NativeMethods.CoWaitForMultipleHandles(
                    NativeMethods.COWAIT_DISPATCH_WINDOW_MESSAGES | 
                        NativeMethods.COWAIT_DISPATCH_CALLS,
                    NativeMethods.INFINITE, 
                    1, new[] { task.AsUnmanagedHandle() },
                    out index);
                Console.WriteLine("Result: " + result + ", pending messages in the queue: " + (NativeMethods.GetQueueStatus(0x1FF) >> 16 != 0));

                // Test #3
                Console.WriteLine("\nTest #3. CoWaitForMultipleHandles with COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS | COWAIT_INPUTAVAILABLE, press Enter to stop...");
                task = ReadLineAsync();

                result = NativeMethods.CoWaitForMultipleHandles(
                    NativeMethods.COWAIT_DISPATCH_WINDOW_MESSAGES | 
                        NativeMethods.COWAIT_DISPATCH_CALLS | 
                        NativeMethods.COWAIT_INPUTAVAILABLE,
                    NativeMethods.INFINITE,
                    1, new[] { task.AsUnmanagedHandle() },
                    out index);
                Console.WriteLine("Result: " + result + ", pending messages in the queue: " + (NativeMethods.GetQueueStatus(0x1FF) >> 16 != 0));
            });

            thread.SetApartmentState(ApartmentState.STA);
            thread.Start();

            thread.Join();
        }

        //
        // Helpers
        //

        // create a window to handle messages
        static IntPtr CreateTestWindow()
        {
            // Create a simple Win32 window 
            var hwndStatic = NativeMethods.CreateWindowEx(0, "Static", String.Empty, NativeMethods.WS_POPUP,
                0, 0, 0, 0, NativeMethods.HWND_MESSAGE, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);

            // subclass it with a custom WndProc
            IntPtr prevWndProc = IntPtr.Zero;

            NativeMethods.WndProc newWndProc = (hwnd, msg, wParam, lParam) =>
            {
                if (msg == NativeMethods.WM_TEST)
                    Console.WriteLine("WM_TEST processed: " + wParam);
                return NativeMethods.CallWindowProc(prevWndProc, hwnd, msg, wParam, lParam);
            };

            prevWndProc = NativeMethods.SetWindowLong(hwndStatic, NativeMethods.GWL_WNDPROC,
                Marshal.GetFunctionPointerForDelegate(newWndProc));
            if (prevWndProc == IntPtr.Zero)
                throw new ApplicationException();

            return hwndStatic;
        }

        // call Console.ReadLine on a pool thread
        static Task<string> ReadLineAsync()
        {
            return Task.Run(() => Console.ReadLine());
        }

        // get Win32 waitable handle of Task object
        static IntPtr AsUnmanagedHandle(this Task task)
        {
            return ((IAsyncResult)task).AsyncWaitHandle.SafeWaitHandle.DangerousGetHandle();
        }
    }

    // Interop
    static class NativeMethods
    {
        [DllImport("user32")]
        public static extern IntPtr SetWindowLong(IntPtr hwnd, int nIndex, IntPtr dwNewLong);

        [DllImport("user32")]
        public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll")]
        public static extern IntPtr CreateWindowEx(
            uint dwExStyle, string lpClassName, string lpWindowName, uint dwStyle, 
            int x, int y, int nWidth, int nHeight, 
            IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);

        [DllImport("user32.dll")]
        public static extern bool PostMessage(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll")]
        public static extern int MessageBox(IntPtr hwnd, string text, String caption, int options);

        [DllImport("ole32.dll", SetLastError = true)]
        public static extern uint CoWaitForMultipleHandles(uint dwFlags, uint dwTimeout,
           int cHandles, IntPtr[] pHandles, out uint lpdwindex);

        [DllImport("user32.dll")]
        public static extern uint GetQueueStatus(uint flags);

        [UnmanagedFunctionPointer(CallingConvention.StdCall)]
        public delegate IntPtr WndProc(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam);

        public static IntPtr HWND_MESSAGE = new IntPtr(-3);

        public const int GWL_WNDPROC = -4;
        public const uint WS_POPUP = 0x80000000;

        public const uint WM_USER = 0x0400;
        public const uint WM_TEST = WM_USER + 1;

        public const uint COWAIT_WAITALL = 1;
        public const uint COWAIT_ALERTABLE = 2;
        public const uint COWAIT_INPUTAVAILABLE = 4;
        public const uint COWAIT_DISPATCH_CALLS = 8;
        public const uint COWAIT_DISPATCH_WINDOW_MESSAGES = 0x10;

        public const uint RPC_S_CALLPENDING = 0x80010115;

        public const uint WAIT_TIMEOUT = 0x00000102;
        public const uint WAIT_FAILED = 0xFFFFFFFF;
        public const uint WAIT_OBJECT_0 = 0;
        public const uint WAIT_ABANDONED_0 = 0x00000080;
        public const uint WAIT_IO_COMPLETION = 0x000000C0;

        public const uint INFINITE = 0xFFFFFFFF;
    }
}

The output:

Starting an STA thread...
Post some WM_TEST messages...

Test #1. CoWaitForMultipleHandles with COWAIT_INPUTAVAILABLE only, press Enter to stop...

Result: 0, pending messages in the queue: True

Test #2. CoWaitForMultipleHandles with COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS, press Enter to stop...

Result: 0, pending messages in the queue: True

Test #3. CoWaitForMultipleHandles with COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS | COWAIT_INPUTAVAILABLE, press Enter to stop...
WM_TEST processed: 1
WM_TEST processed: 2
WM_TEST processed: 3

Result: 0, pending messages in the queue: False

STA thread finished.
Press Enter to exit.

All tests are done under Windows 8.1 Pro 64bit + NET v4.5.1.

  • Am I misreading the docs or missing something else?

  • Should I report this as a bug (at least, a bug in the docs)?

  • Should CoWaitForMultipleHandles be avoided and replaced with a solution based on MsgWaitForMultipleObjectsEx (which behaves in accordance with the docs)?

[UPDATE] Under Windows 7, neither COWAIT_DISPATCH_WINDOW_MESSAGES nor COWAIT_DISPATCH_CALLS are supported, CoWaitForMultipleHandles fails with E_INVALIDARG (0x80070057). When called with zero as flags, it blocks without pumping.

like image 376
noseratio Avatar asked Jan 20 '14 05:01

noseratio


1 Answers

CoWaitForMultipleHandles is intended to process COM window messages (e.g. cross-apartment marshalling) and a few others (don't ask me which) in STA, or simply block in MTA. In this blog post, «Managed blocking» by Chris Brumme, it says CWFMH processes "just the right amount" of window messages. However, since it leaves any non-COM posted window message in the queue, the queue may still fill up, just not with COM window messages.

According to this document, «Migrating your Windows 8 Consumer Preview app to Windows 8 Release Preview», it says:

CoWaitForMultipleHandles function is no longer supported in Windows Store apps. In addition the following CoWait_Flags have been removed:

COWAIT_DISPATCH_CALLS

COWAIT_DISPATCH_WINDOW_MESSAGES

If you really want to process all messages, you should use MsgWaitForMultipleObjectsEx in a message loop with GetMessage or PeekMessage with PM_REMOVE. Doing so means a potential reentrancy frenzy. You still don't control further calls into the STA from other components down the stack. That is, a modal dialog (e.g. Common Dialog Box for Open) might pump every message in a plain-old window message loop, but some framework might call CoWaitForMultipleHandles.

Bottom line is, if you're doing intensive processing or blocking operations, delegate it to another thread (possibly using a queue), and if needed tell the invoking UI thread to update after the operation is done.

This is different than e.g. lengthy UI calls, such as OLE embedding or a modal dialog, where there's usually a window message loop somewhere along the stack. Or from lengthy but chunkable/resumable operations (e.g. a state machine), where you can cooperate by processing messages once in a while, or by using wait functions that return when there are messages so you can process them before waiting again.

Be careful, this only works well for one handle; for multiple handles e.g. mutexes, you'd want either all or none, and the next best approach is an active loop, with a timed-out call to WaitForMultipleObjects followed by a PeekMessage with PM_REMOVE window message loop. This is a border-line case, acceptable for a UI application that is the user's center of attention (e.g. it's their main work), but unacceptable if such code may run unattended and on demand. Unless you know for sure that it needs to happen in an STA or UI thread, my advice is don't do this.

Finally, you should probably open a bug at Microsoft Connect, at least to update the documentation. Or actually make it work as "expected".

like image 129
acelent Avatar answered Nov 15 '22 21:11

acelent