Delphi Win32 TXMLDocument 无法从线程中实例化和使用?

Delphi Win32 TXMLDocument can't be instantiated and used from a thread?

我一直在使用 Indy TIdTCPServer 对象并在 TIdTCPServer.OnExecute 事件期间实例化 TXMLDocument 对象实例。当 xml.Active 设置为 true:

时,我发现异常令人惊讶

Microsoft MSXML is not installed

procedure TForm4.tcpRXExecute(AContext: TIdContext);
var
  sResponseXML : string;
  xml:IXMLDocument;
begin
  // get message from client
  sResponseXML := AContext.Connection.IOHandler.ReadLn;

  xml:=TXMLDocument.Create(nil);
  
  // error here:  "Microsoft MSXML is not installed"
  xml.Active:=true;

  xml.Encoding:='UTF-8';

  xml.LoadFromXML(sResponseXML);

  // use the xml document
  
  //AContext.Connection.IOHandler.WriteLn('... message sent from server :)');
end;

深入观察,我发现发生异常是因为 TMSXMLDOMDocumentFactory.TryCoCreateInstance() 无法创建正确的文档对象实例,尽管它从主应用程序的其他部分接收到相同的 GuidList线。我不明白为什么如果从组件的线程调用对象没有实例化。

这是应实例化对象的 Embarcadero 代码:

class function TMSXMLDOMDocumentFactory.TryCoCreateInstance(const GuidList: array of TGUID): IUnknown;
var
  I: Integer;
  Status: HResult;
begin
  for I := Low(GuidList) to High(GuidList) do
  begin
    // never successful if the XML document object was being used from the Execute event handler.
    Status := CoCreateInstance(GuidList[I], nil, CLSCTX_INPROC_SERVER or
      CLSCTX_LOCAL_SERVER, IDispatch, Result);
    if Status = S_OK then Exit;
  end;
end;

我想它一定与 CLSCTX_INPROC_SERVERCLSCTX_LOCAL_SERVER (https://docs.microsoft.com/en-us/windows/win32/api/wtypesbase/ne-wtypesbase-clsctx) 有关,但我不明白为什么这些会成为问题。

即使那是原因,我如何从该事件处理程序中使用 TXMLDocument

MSXML 是一种基于 COM 的技术。您需要在访问 COM 接口的每个线程上下文中调用 CoInitialize/Ex() 来初始化 COM 库。否则,在这种情况下,CoCreateInstance() 将失败并出现 CO_E_NOTINITIALIZED 错误。 Delphi的RTL在主线程中为你初始化COM库,但你必须自己在工作线程中完成,例如TIdTCPServer.

使用的那些

默认情况下,TIdTCPServer 为每个客户端连接创建一个新线程。在这种情况下,最容易 初始化 COM 的地方是服务器的 OnConnect 事件(因为 OnExecute 事件是循环的)。

procedure TForm4.tcpRXConnect(AContext: TIdContext);
begin
  CoInitialize(nil);
end;

procedure TForm4.tcpRXDisconnect(AContext: TIdContext);
begin
  CoUninitialize();
end;

但是,由于 TIdTCPServer 支持线程池,并且 COM 每个线程只应初始化一次,因此 最好 在这种情况下初始化 COM 的地方 1直接在每个线程的Execute()方法中。为此,明确地将 TIdSchedulerOfThread 派生的组件(TIdSchedulerOfThreadDefaultTIdSchedulerOfThreadPool 等)分配给 TIdTCPServer.Scheduler 属性(可以在设计时完成),然后将 TIdSchedulerOfThread.ThreadClass 属性(必须在运行时完成,在服务器激活之前)设置为 TIdThreadWithTask-派生的 class 覆盖虚拟 BeginExecute()AfterExecute() 方法。

type
  TMyThreadWithTask = class(TIdThreadWithTask)
  protected
    procedure BeforeExecute; override;
    procedure AfterExecute; override;
  end;

procedure TMyThreadWithTask.BeforeExecute;
begin
  CoInitialize(nil);
  inherited;
end;

procedure TMyThreadWithTask.AfterExecute;
begin
  inherited;
  CoUninitialize();
end;

procedure TForm4.FormCreate(Sender: TObject);
begin
  IdSchedulerOfThreadDefault1.ThreadClass := TMyThreadWithTask;
end;

1:至少在 https://github.com/IndySockets/Indy/issues/6 在 Indy 的未来版本中实现之前。