Search code examples
windowsdelphiwinapieventscom

How to pump COM messages?


I want to wait for a WebBrowser control to finish navigation. So i create an Event, and then i want to wait for it to be set:

procedure TContoso.NavigateToEmpty(WebBrowser: IWebBrowser2);
begin
   FEvent.ResetEvent;
   WebBrowser.Navigate2('about:blank'); //Event is signalled in the DocumentComplete event

   Self.WaitFor;
end;

And then i set the event in the DocumentComplete event:

procedure TContoso.DocumentComplete(ASender: TObject; const pDisp: IDispatch; const URL: OleVariant);
var
    doc: IHTMLDocument2;
begin
    if (pDisp <> FWebBrowser.DefaultInterface) then
    begin
       //This DocumentComplete event is for another frame
       Exit;
    end;

    //Set the event that it's complete
    FEvent.SetEvent;
end;

The problem comes in how to wait for this event to happen.

WaitFor it

First reaction would be to wait for the event to become triggered:

procedure TContoso.WaitFor;
begin
   FEvent.WaitFor;
end;

The problem with that is that the DocumentComplete event can never fire, because the application never goes idle enough to allow the COM event to get through.

Busy Sleep Wait

My first reaction was to do a busy sleep, waiting for a flag:

procedure TContoso.NavigateToEmpty(WebBrowser: IWebBrowser2);
begin
   FIsDocumentComplete := False;
   WebBrowser.Navigate2('about:blank'); //Flag is set in the DocumentComplete event
   Self.WaitFor;
end;

procedure TContoso.WaitFor;
var
   n: Iterations;
const
   MaxIterations = 25; //100ms each * 10 * 5 = 5 seconds
begin
   while n < MaxIterations do
   begin
      if FIsDocumentComplete then
         Exit;
      Inc(n);
      Sleep(100); //100ms
   end;
end;

The problem with a Sleep, is that it doesn't allow the application to do idle enough to allow the COM event messages to get through.

Use CoWaitForMultipleHandles

After research, it seems that COM folks created a function created exactly for this situation: (archive)

While a thread in a Single-Threaded Apartment (STA) blocks, we will pump certain messages for you. Message pumping during blocking is one of the black arts at Microsoft. Pumping too much can cause reentrancy that invalidates assumptions made by your application. Pumping too little causes deadlocks. Starting with Windows 2000, OLE32 exposes CoWaitForMultipleHandles so that you can pump “just the right amount.”

So i tried that:

procedure TContoso.WaitFor;
var
   hr: HRESULT;
   dwIndex: DWORD;
begin
   hr := CoWaitForMultipleHandles(0, 5000, 1, @FEvent.Handle, {out}dwIndex);
   OleCheck(hr);
end;

The problem is that just doesn't work; it doesn't allow the COM event to appear. Use UseCOMWait wait

i could also try Delphi's own mostly secret feature of TEvent: UseCOMWait

Set UseCOMWait to True to ensure that when a thread is blocked and waiting for the object, any STA COM calls can be made back into this thread.

Excellent! Lets use that:

FEvent := TEvent.Create(True);

function TContoso.WaitFor: Boolean;
begin
   FEvent.WaitFor;
end;

Except that doesn't work; because the callback event never gets fired.

MsgWaitForMultipleBugs

So now i start to delve into the awful, awful, awful, awful, buggy, error-prone, re-entrancy inducing, sloppy, requires a mouse nudge, sometimes crashes world of MsgWaitForMultipleObjects:

function TContoso.WaitFor: Boolean;
var
//  hr: HRESULT;
//  dwIndex: DWORD;
//  msg: TMsg;
    dwRes: DWORD;
begin
//  hr := CoWaitForMultipleHandles(0, 5000, 1, @FEvent.Handle, {out}dwIndex);
//  OleCheck(hr);
//  Result := (hr = S_OK);

    Result := False;
    while (True) do
    begin
        dwRes := MsgWaitForMultipleObjects(1, @FEvent.Handle, False, 5000, QS_SENDMESSAGE);
        if (dwRes = WAIT_OBJECT_0) then
        begin
            //Our event signalled
            Result := True;
            Exit;
        end
        else if (dwRes = WAIT_TIMEOUT) then
        begin
            //We waited our five seconds; give up
            Exit;
        end
        else if (dwRes = WAIT_ABANDONED_0) then
        begin
            //Our event object was destroyed; something's wrong
            Exit;
        end
        else if (dwRes = (WAIT_OBJECT_0+1)) then
        begin
            GetMessage(msg, 0, 0, 0);
        if msg.message = WM_QUIT then
        begin
            {
                http://blogs.msdn.com/oldnewthing/archive/2005/02/22/378018.aspx

                PeekMessage will always return WM_QUIT. If we get it, we need to
                cancel what we're doing and "re-throw" the quit message.

                    The other important thing about modality is that a WM_QUIT message
                    always breaks the modal loop. Remember this in your own modal loops!
                    If ever you call the PeekMessage function or The GetMessage
                    function and get a WM_QUIT message, you must not only exit your
                    modal loop, but you must also re-generate the WM_QUIT message
                    (via the PostQuitMessage message) so the next outer layer will
                    see the WM_QUIT message and do its cleanup as well. If you fail
                    to propagate the message, the next outer layer will not know that
                    it needs to quit, and the program will seem to "get stuck" in its
                    shutdown code, forcing the user to terminate the process the hard way.
            }
            PostQuitMessage(msg.wParam);
            Exit;
        end;
        TranslateMessage(msg);
        DispatchMessage(msg);
    end;
end;

The above code is wrong because:

  • i don't know what kind of message to wake up for (are com events sent?)
  • i don't know i don't want to call GetMessage, because that gets messages; i only want to get the COM message (see point one)
  • i might should be using PeekMessage (see point 2)
  • i don't know if i have to call GetMessage in a loop until it returns false (see Old New Thing)

I've been programming long enough to run away, far away, if i'm going to pump my own messages.

The questions

So i have four questions. All related. This post is one of the four:

  • How to make WebBrower.Navigate2 synchronous?
  • How to pump COM messages?
  • Does pumping COM messages cause COM events to callback?
  • How to use CoWaitForMultipleHandles

I am writing in, and using Delphi. But obviously any native code would work (C, C++, Assembly, Machine code).

See also


Solution

  • The short and long of it is that you have to pump ALL messages normally, you can't just single out COM messages by themselves (and besides, there is no documented messages that you can peek/pump by themselves, they are known only to COM's internals).

    How to make WebBrower.Navigate2 synchronous?

    You can't. But you don't have to wait for the OnDocumentComplete event, either. You can busy-loop inside of NavigateToEmpty() itself until the WebBrowser's ReadyState property is READYSTATE_COMPLETE, pumping the message queue when messages are waiting to be processed:

    procedure TContoso.NavigateToEmpty(WebBrowser: IWebBrowser2);
    begin
      WebBrowser.Navigate2('about:blank');
      while (WebBrowser.ReadyState <> READYSTATE_COMPLETE) and (not Application.Terminated) do
      begin
        // if MsgWaitForMultipleObjects(0, Pointer(nil)^, False, 5000, QS_ALLINPUT) = WAIT_OBJECT_0 then
        // if GetQueueStatus(QS_ALLINPUT) <> 0 then
          Application.ProcessMessages;
      end;
    end;
    

    How to pump COM messages?

    You can't, not by themselves anyway. Pump everything, and be prepared to handle any reentry issues that result from that.

    Does pumping COM messages cause COM events to callback?

    Yes.

    How to use CoWaitForMultipleHandles

    Try something like this:

    procedure TContoso.NavigateToEmpty(WebBrowser: IWebBrowser2);
    var
      hEvent: THandle;
      dwIndex: DWORD;
      hr: HRESULT;
    begin
      // when UseCOMWait() is true, TEvent.WaitFor() does not wait for, or
      // notify, when messages are pending in the queue, so use
      // CoWaitForMultipleHandles() directly instead.  But you have to still
      // use a waitable object, just don't signal it...
      hEvent := CreateEvent(nil, True, False, nil);
      if hEvent = 0 then RaiseLastOSError;
      try
        WebBrowser.Navigate2('about:blank');
        while (WebBrowser.ReadyState <> READYSTATE_COMPLETE) and (not Application.Terminated) do
        begin
          hr := CoWaitForMultipleHandles(COWAIT_INPUTAVAILABLE, 5000, 1, hEvent, dwIndex);
          case hr of
            S_OK: Application.ProcessMessages;
            RPC_S_CALLPENDING, RPC_E_TIMEOUT: begin end;
          else
            RaiseLastOSError(hr);
          end;
        end;
      finally
        CloseHandle(hEvent);
      end;
    end;