I've been working with an Indy TIdTCPServer
object and instantiating a TXMLDocument
object instance during the TIdTCPServer.OnExecute
event. I find it quite surprising that I get an exception when xml.Active
is set to 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;
Looking deeper, I found that the exception occurs because TMSXMLDOMDocumentFactory.TryCoCreateInstance()
is unable to create the correct document object instance despite receiving the same GuidList
as it received in other parts of the application from the main thread. I don't understand why the object isn't instantiated if called from the component's thread.
Here's the Embarcadero code where the object should be instantiated:
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;
I expect it must have something to do with CLSCTX_INPROC_SERVER
or CLSCTX_LOCAL_SERVER
(https://learn.microsoft.com/en-us/windows/win32/api/wtypesbase/ne-wtypesbase-clsctx), but I don't see why these could be a problem.
Even if that was the cause, how can I use TXMLDocument
from within that event handler?
MSXML is a COM-based technology. You need to call CoInitialize/Ex()
to initialize the COM library in every thread context that accesses COM interfaces. Otherwise, in this case, CoCreateInstance()
will fail with a CO_E_NOTINITIALIZED
error. Delphi's RTL initializes the COM library for you in the main thread, but you have to do it yourself in worker threads, such as those used by TIdTCPServer
.
By default, TIdTCPServer
creates a new thread for each client connection. In this case, the easiest place to initialize COM would be in the server's OnConnect
event (since the OnExecute
event is looped).
procedure TForm4.tcpRXConnect(AContext: TIdContext);
begin
CoInitialize(nil);
end;
procedure TForm4.tcpRXDisconnect(AContext: TIdContext);
begin
CoUninitialize();
end;
However, since TIdTCPServer
supports thread-pooling, and COM should be initialized only once per thread, the best place to initialize COM in this case 1 is directly in each thread's Execute()
method. To do that, explicitly assign a TIdSchedulerOfThread
-derived component (TIdSchedulerOfThreadDefault
, TIdSchedulerOfThreadPool
, etc) to the TIdTCPServer.Scheduler
property (can be done at design-time), and then set the TIdSchedulerOfThread.ThreadClass
property (must be done at runtime, before the server is activated) to a TIdThreadWithTask
-derived class that overrides the virtual BeginExecute()
and AfterExecute()
methods.
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: at least until https://github.com/IndySockets/Indy/issues/6 is implemented in a future version of Indy.