Home » excel » delphi – The application called an interface that was marshalled for a different thread

delphi – The application called an interface that was marshalled for a different thread

Posted by: admin April 23, 2020 Leave a comment

Questions:

i’m writing a delphi app that communicates with excel. one thing i noticed is that if i call the Save method on the Excel workbook object, it can appear to hang because excel has a dialog box open for the user. i’m using the late binding.

i’d like for my app to be able to notice when Save takes several seconds and then take some kind of action like show a dialog box telling this is what’s happening.

i figured this’d be fairly easy. all i’d need to do is create a thread that calls Save and have that thread call Excel’s Save routine. if it takes too long, i can take some action.

procedure TOfficeConnect.Save;
var
  Thread:TOfficeHangThread;
begin
  // spin off as thread so we can control timeout
  Thread:=TOfficeSaveThread.Create(m_vExcelWorkbook);

  if WaitForSingleObject(Thread.Handle, 5 {s} * 1000 {ms/s})=WAIT_TIMEOUT then
    begin
      Thread.FreeOnTerminate:=true;
      raise Exception.Create(_('The Office spreadsheet program seems to be busy.'));
    end;

  Thread.Free;
end;

  TOfficeSaveThread = class(TThread)
  private
    { Private declarations }
    m_vExcelWorkbook:variant;
  protected
    procedure Execute; override;
    procedure DoSave;
  public
    constructor Create(vExcelWorkbook:variant);
  end;

{ TOfficeSaveThread }

constructor TOfficeSaveThread.Create(vExcelWorkbook:variant);
begin
  inherited Create(true);

  m_vExcelWorkbook:=vExcelWorkbook;

  Resume;
end;

procedure TOfficeSaveThread.Execute;
begin
  m_vExcelWorkbook.Save;
end;

i understand this problem happens because the OLE object was created from another thread (absolutely).

how can i get around this problem? most likely i’ll need to “re-marshall” for this call somehow…

any ideas?

How to&Answers:

Rather than accessing the COM object from two threads, just show the message dialog in the secondary thread. The VCL isn’t thread-safe, but Windows is.

type
  TOfficeHungThread = class(TThread)
  private
    FTerminateEvent: TEvent;
  protected
    procedure Execute; override;
  public
   constructor Create;
   destructor Destroy; override;
   procedure Terminate; override;
  end;

...

constructor TOfficeHungThread.Create;
begin
  inherited Create(True);
  FTerminateEvent := TSimpleEvent.Create;
  Resume;
end;

destructor TOfficeHungThread.Destroy;
begin
  FTerminateEvent.Free;
  inherited;
end;

procedure TOfficeHungThread.Execute;
begin
  if FTerminateEvent.WaitFor(5000) = wrTimeout then
    MessageBox(Application.MainForm.Handle, 'The Office spreadsheet program seems to be busy.', nil, MB_OK);
end;

procedure TOfficeHungThread.Terminate;
begin
  FTerminateEvent.SetEvent;
end;

...

procedure TMainForm.Save;
var
  Thread: TOfficeHungThread;
begin
  Thread := TOfficeHungThread.Create;
  try
    m_vExcelWorkbook.Save;
    Thread.Terminate;
    Thread.WaitFor;
  finally
    Thread.Free;
  end;
end;

Answer:

The real problem here is that Office applications aren’t intended for multithreaded use. Because there can be any number of client applications issuing commands through COM, those commands are serialized to calls and processed one by one. But sometimes Office is in a state where it doesn’t accept new calls (for example when it is displaying a modal dialog) and your call gets rejected (giving you the “Call was rejected by callee”-error). See also the answer of Geoff Darst in this thread.

What you need to do is implement a IMessageFilter and take care of your calls being rejected. I did it like this:

function TIMessageFilterImpl.HandleInComingCall(dwCallType: Integer;
  htaskCaller: HTASK; dwTickCount: Integer;
  lpInterfaceInfo: PInterfaceInfo): Integer;
begin
  Result := SERVERCALL_ISHANDLED;
end;

function TIMessageFilterImpl.MessagePending(htaskCallee: HTASK;
  dwTickCount, dwPendingType: Integer): Integer;
begin
  Result := PENDINGMSG_WAITDEFPROCESS;
end;

function ShouldCancel(aTask: HTASK; aWaitTime: Integer): Boolean;
var
  lBusy: tagOLEUIBUSYA;
begin
  FillChar(lBusy, SizeOf(tagOLEUIBUSYA), 0);
  lBusy.cbStruct := SizeOf(tagOLEUIBUSYA);
  lBusy.hWndOwner := Application.Handle;

  if aWaitTime < 20000 then //enable cancel button after 20 seconds
    lBusy.dwFlags := BZ_NOTRESPONDINGDIALOG;

  lBusy.task := aTask;
  Result := OleUIBusy(lBusy) = OLEUI_CANCEL;
end;

function TIMessageFilterImpl.RetryRejectedCall(htaskCallee: HTASK;
  dwTickCount, dwRejectType: Integer): Integer;
begin
  if dwRejectType = SERVERCALL_RETRYLATER then
  begin
    if dwTickCount > 10000 then //show Busy dialog after 10 seconds
    begin
      if ShouldCancel(htaskCallee, dwTickCount) then
        Result := -1
      else
        Result := 100;
    end
    else
      Result := 100; //value between 0 and 99 means 'try again immediatly', value >= 100 means wait this amount of milliseconds before trying again
  end
  else
  begin
    Result := -1; //cancel
  end;
end;

The messagefilter has to be registered on the same thread as the one issuing the COM calls. My messagefilter implementation will wait 10 seconds before displaying the standard OLEUiBusy dialog. This dialog gives you the option to retry the rejected call (in your case Save) or switch to the blocking application (Excel displaying the modal dialog).
After 20 seconds of blocking, the cancel button will be enabled. Clicking the cancel button will cause your Save call to fail.

So forget messing around with threads and implement the messagefilter, which is the way
to deal with these issues.

Edit:
The above fixes “Call was rejected by callee” errors, but you have a Save that hangs. I suspect that Save brings up a popup that needs your attention (Does your workbook has a filename already?). If it is a popup that is in the way, try the following (not in a separate thread!):

{ Turn off Messageboxes etc. }
m_vExcelWorkbook.Application.DisplayAlerts := False;
try
  { Saves the workbook as a xls file with the name 'c:\test.xls' }
  m_vExcelWorkbook.SaveAs('c:\test.xls', xlWorkbookNormal);
finally
  { Turn on Messageboxes again }
  m_vExcelWorkbook.Application.DisplayAlerts := True;
end;

Also try to debug with Application.Visible := True; If there are any popups, there is a change you will see them and take actions to prevent them in the future.

Answer:

Try calling CoInitializeEx with COINIT_MULTITHREADED since MSDN states:

Multi-threading (also called free-threading) allows calls to methods of objects created by this thread to be run on any thread.

Answer:

‘Marshalling’ an interface from one thread to another can be done by using CoMarshalInterThreadInterfaceInStream to put the interface into a stream, move the stream to the other thread and then use CoGetInterfaceAndReleaseStream to get the interface back from the stream. see here for an example in Delphi.

Answer:

Lars’ answer is along the right lines I think. An alternative to his suggestion is to use the GIT (Global Interface Table), which can be used as a cross-thread repository for interfaces.

See this SO thread here for code for interacting with the GIT, where I posted a Delphi unit that provides simple access to the GIT.

It should simply be a question of registering your Excel interface into the GIT from your main thread, and then getting a separate reference to the interface from within your TOfficeHangThread thread using the GetInterfaceFromGlobal method.