Watch, Follow, &
Connect with Us

For forums, blogs and more please visit our
Developer Tools Community.


Welcome, Guest
Guest Settings
Help

Thread: OLE in a thread problem.


This question is answered. Helpful answers available: 1. Correct answers available: 1.


Permlink Replies: 3 - Last Post: Jul 27, 2015 10:37 AM Last Post By: Mike Meier
Mike Meier

Posts: 12
Registered: 5/25/05
OLE in a thread problem.  
Click to report abuse...   Click to reply to this thread Reply
  Posted: Jul 23, 2015 8:30 AM
I'm back.

I've spent several days trying to figure this one out, and this is the third time I've written this post. As I write it I keep thinking of little things to try. Alas, no luck, so here it is.

I've using OLE to get my program to run another, and everything works until I try to execute it in a thread. I've figure out the whole CoInitializeEX/CoUninitialize thing and that seems to be working. The problem seems to be executing the functions available. I keep getting an "Object not connected to server" error.

I'll include code below, and I can make it all available for download if anyone wants it, but briefly here's what I do.

1. Program starts
2. Create a "control" component. This handles the next to last step in communications with the external program.
3. Create a component that is acts as an interface to the lower level code. It handled errors, simulated operation, other options closely related to user actions. In turn this uses the "Control" component created in step 2.
4. Click a button to start the external program. This request goes through "Shell", then on to "Control", and "Control" uses ShellEx to start the external program.

So far no OLE functions have been called, and so far so good.

5. User clicks the "Connect" button, and in the usual manner the request gets passed to "Control" which then creates the FOMNIC component (the OLE TypeLibrary component) then then tries to execute the OLE "Connect" function. This works.

By the way, FOMNIC provides the following functions: create, get set_, and execute, which simply set and retrieve values from the external program, and executes a command within the external program.

So far, still working.

When I click a button to make a call to get, set_, or execute I get the "not connected" error. I've done everything I can think of to verify that I do indeed have a connection, but still, the error, but only when executing this in a thread. Otherwise, it works fine. The OLE "Get" routine references DefaultInterface. I suspect something is work here, but it is not nil so, so it exists, so what's going on?

Here's how I get the thread going.

procedure TMainForm.Timer1Timer(Sender: TObject);
begin
  // Handle other errors.
  if ErrorEvents.Count > 0 then ErrorHandleEvents;
 
  // Process pending OMNIC requests.
  if UseThreads then
    ThreadStart
  else
  begin
    ThreadUpdate;
    ThreadEnd(Sender);
  end;
end;


Here are the three routines that start, execute, and end the thread.
function TMainForm.ThreadStart:boolean;
const FuncName='ThreadStart';
begin
  try
    Result:=true;
 
    // If not already busy, get the Daq readings
    if (not ThreadIsBusy) and (OMNICThread=nil) then
    begin
      // Tell the world we are busy.
      ThreadIsBusy:=true;
 
      CoInitializeEX(nil,COINIT_MULTITHREADED);
      OMNICThread:=TOMNICThread.Create(ThreadEnd);
    end;
 
  except
    on E:Exception do
    begin
      Result:=false;
      inc(ErrorEvents.Count);
      ErrorEvents.Text.Add(FuncName+': '+E.Message);
      ErrorEvents.Text.Add(FuncName+': An error occurred while starting the OMNIC I/O thread.');
    end;
  end;
end;
 
function TMainForm.ThreadUpdate:boolean;
const FuncName='ThreadUpdate';
begin
  Result:=true;
  try
 
//    if not ErrorHalted then
    if (not OMNIC_Shell.IsBusy) then
      if OMNICFlag and (OMNICFunction <> ofNone) then
        OMNICResult:=OMNIC_Shell.OMNICUpdate(OMNICFunction)
      else
        if AnalysisFlag and (AnalysisFunction <> ofNone) then
          AnalysisResult:=OMNIC_Shell.OMNICUpdate(AnalysisFunction);
 
  except
    on E:Exception do
    begin
      Result:=false;
      inc(ErrorEvents.Count);
      ErrorEvents.Text.Add(FuncName+': '+E.Message);
      ErrorEvents.Text.Add(FuncName+': An error occurred while executing the OMNIC I/O thread.');
    end;
  end;
end;
 
procedure TMainForm.ThreadEnd(Sender: TObject);
const FuncName='ThreadEnd';
begin
  try
    AllSheetsUpdate;              // Refresh the info on the forms.
    if UseThreads then CoUninitialize; // Required for threaded COM/OLE applications
    OMNICThread:=nil;             // Set to nil. This helps when restarting the thread.
    ThreadIsBusy:=false;          // The thread is officially done.
  except
    on E:Exception do
    begin
      ThreadIsBusy:=false;
      inc(ErrorEvents.Count);
      ErrorEvents.Text.Add(FuncName+': '+E.Message);
      ErrorEvents.Text.Add(FuncName+': An error occurred while ending the OMNIC I/O thread.');
    end;
  end;
end;


When the user clicks a button OMNIC_Function gets a value other than ofNone and OMNICFlag is set to true.

Here's a typical routine in the Shell component. Not an exciting example, but illustrative.

function TOMNIC_Shell.DoGet:boolean;
const FuncName='DoGet';
begin
  Result:=true;
  try
 
    // Leave some bread crumbs.
    UpdateDebug(GetModuleStr(FuncName));
    ChangeStatusMsg('Get('+FItemName+')');
 
    if FOfflineMode then
    begin
      if UpperCase(FItemName)='RESULT ERROR' then FItemValue:='Ok'
      else
        if pos('MENUSTATUS',Uppercase(FItemName))<>0 then FItemValue:='ENABLED';
    end
    else
    begin
      // Make sure Omnic is running and is connected
      if not (ControlPtr.IsRunning or ControlPtr.Connected) then
        raise Exception.Create('Omnic is not running.');
 
      // Send the Get command
      Result:=ControlPtr.Get(FItemName,FItemValue);
      if not Result then raise Exception.Create('OMNIC Get('+FItemName+') failed.');
 
      Result:=ControlPtr.Get('Result Error',FErrorStr);
      if not Result then raise Exception.Create('OMNIC Get(Result Error) failed.');
 
      Result:=UpperCase(FErrorStr)='OK';
      if not Result then raise Exception.Create('OMNIC Error: '+FErrorStr);
    end;
 
  except
    on E:Exception do
    begin
      Result:=false;
      FMisc.LogThisError(GetModuleStr(FuncName),E.Message,'',-1,ofGet,ErrorPtr);
    end;
  end;
end;


The "Control" component, and FOMNIC, are defined thusly:

type
  TOMNIC_Control = class(TComponent)
  private { Private declarations }
    FOmnic: TOMNICApp;
    FMisc: TOMNIC_Misc;
    FConnected: boolean;
    FPath: string;
 
    // Component
    procedure SetPath(Value:string);
 
    // Misc
    function GetFunctionStr(S1:string):string;
    function GetModuleStr(S1:string):string;
 
  protected {Visible to decendents}
  public { Public declarations (visible to everyone)}
    ErrorPtr: TksErrorPtr;
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function Start:boolean;
    function Close:boolean;
    function Connect:boolean;
    function DisConnect:boolean;
    function IsRunning:boolean;
    function Get(S:WideString; var R:WideString):boolean;
    function Put(S,V:WideString):boolean;
    function Execute(S:WideString):boolean;
  published { Published declarations, public, and listed in the object inspector}
    property Connected: boolean read FConnected;
    property Path: string read FPath write SetPath;
  end;


Here's how I start the external program. This works fine, but it also does not use FOMNIC. FOMNIC gets created when I call my connect function..

function TOMNIC_Control.Start:boolean;
const FuncName='Start';
var ShellResult:integer;
var oPath:array [0..255] of Char;
var i:integer;
begin
  Result:=true;
  try
 
    // Is OMNIC already running?  If not, start it.
    // Check with the OS to see if OMNIC is running.
    if not isRunning then
    begin
      // ShellExecute to run OMNIC;
      for i:=0 to 255 do oPath[i]:=#0;
      for i:=0 to length(FPath)-1 do oPath[i]:=Char(FPath[i+1]);
      oPath[length(FPath)]:=#32;
      ShellResult:=ShellExecute(0, 'Open', @oPath, PChar(''), PChar(''), SW_Show);
      if ShellResult<=32 then raise exception.create('Shell API error');
    end;
 
  except
    on E:Exception do
    begin
      Result:=false;
      FMisc.LogThisError(GetModuleStr(FuncName),E.Message,'OMNIC path: '+FPath,ShellResult,
          ofOpen,ErrorPtr);
    end;
  end;
end;


And here's the Get routine in "Control"

function TOMNIC_Control.Get(S:WideString; var R:WideString):boolean;
const FuncName='Get';
begin
  Result:=true;
  try
    // Let's stick this in here to check on our connection.
    if (FOmnic.DefaultInterface = nil) or
       (FOmnic.IntFce = nil) then
      raise exception.Create('The connection to OMNIC failed.');
 
    R:=FOmnic.Get(S);
 
  except
    on E:Exception do
    begin
      Result:=false;
      FMisc.LogThisError(GetModuleStr(FuncName), E.Message, 'Variable: '+S, -1, ofGet, ErrorPtr);
    end;
  end;
end;


And here are the relevant parts of the code generated by the Type Library.

Type 
// *********************************************************************//
// Interface: IOmnicApp
// Flags:     (4416) Dual OleAutomation Dispatchable
// GUID:      {65AFE3C0-CB16-11D1-984A-00A024CDE0B2}
// *********************************************************************//
  IOmnicApp = interface(IDispatch)
    ['{65AFE3C0-CB16-11D1-984A-00A024CDE0B2}']
    procedure ExecuteCommand(var CmdText: WideString); safecall;
    function Get(var ItemName: WideString): WideString; safecall;
    procedure Set_(var ItemName: WideString; var Value: WideString); safecall;
  end;
 
// *********************************************************************//
// The Class CoOmnicApp provides a Create and CreateRemote method to          
// create instances of the default interface IOmnicApp exposed by              
// the CoClass OmnicApp. The functions are intended to be used by             
// clients wishing to automate the CoClass objects exposed by the         
// server of this typelibrary.                                            
// *********************************************************************//
  CoOmnicApp = class
    class function Create: IOmnicApp;
    class function CreateRemote(const MachineName: string): IOmnicApp;
  end;
 
// *********************************************************************//
// OLE Server Proxy class declaration
// Server Object    : TOmnicApp
// Help String      : OmnicApp Class
// Default Interface: IOmnicApp
// Def. Intf. DISP? : No
// Event   Interface: 
// TypeFlags        : (2) CanCreate
// *********************************************************************//
{$IFDEF LIVE_SERVER_AT_DESIGN_TIME}
  TOmnicAppProperties= class;
{$ENDIF}
  TOmnicApp = class(TOleServer)
  private
    FIntf:        IOmnicApp;
{$IFDEF LIVE_SERVER_AT_DESIGN_TIME}
    FProps:       TOmnicAppProperties;
    function      GetServerProperties: TOmnicAppProperties;
{$ENDIF}
    function      GetDefaultInterface: IOmnicApp;
  protected
    procedure InitServerData; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor  Destroy; override;
    procedure Connect; override;
    procedure ConnectTo(svrIntf: IOmnicApp);
    procedure Disconnect; override;
    procedure ExecuteCommand(var CmdText: WideString);
    function Get(var ItemName: WideString): WideString;
    procedure Set_(var ItemName: WideString; var Value: WideString);
    property DefaultInterface: IOmnicApp read GetDefaultInterface;
 
    property IntFce: IOmnicApp read FIntf;  // Added by me so I can make sure we made the connection.
 
  published
{$IFDEF LIVE_SERVER_AT_DESIGN_TIME}
    property Server: TOmnicAppProperties read GetServerProperties;
{$ENDIF}
  end;
 
constructor TOmnicApp.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
{$IFDEF LIVE_SERVER_AT_DESIGN_TIME}
  FProps := TOmnicAppProperties.Create(Self);
{$ENDIF}
end;
 
procedure TOmnicApp.Connect;
var
  punk: IUnknown;
begin
  if FIntf = nil then
  begin
    punk := GetServer;
    Fintf:= punk as IOmnicApp;
  end;
end;
 
function TOmnicApp.Get(var ItemName: WideString): WideString;
begin
  Result := DefaultInterface.Get(ItemName);
end;
Peter Below

Posts: 1,227
Registered: 12/16/99
Re: OLE in a thread problem.
Helpful
Click to report abuse...   Click to reply to this thread Reply
  Posted: Jul 23, 2015 10:21 AM   in response to: Mike Meier in response to: Mike Meier
Mike Meier wrote:

I'm back.

I've spent several days trying to figure this one out, and this is
the third time I've written this post. As I write it I keep thinking
of little things to try. Alas, no luck, so here it is.

I've using OLE to get my program to run another, and everything works
until I try to execute it in a thread. I've figure out the whole
CoInitializeEX/CoUninitialize thing and that seems to be working. The
problem seems to be executing the functions available. I keep getting
an "Object not connected to server" error.

You have to keep in mind that OLE is generally using the appartment
threaded execution model. That means that you cannot create a OLE
object in the main thread and execute one of the methods it offers in a
secondary thread, unless you jump through some awkward hoops to marshal
the interface of the OLE object from the main thread to the secondary
thread. If you do that, however, the gain of using a secondary thread
at all is limited, since a call to the object in the secondary thread
will simply be passed back to the main thread for execution by the OLE
system library, a bit like doing much of the work of a thread in a
Synchronized method...

I have not looked at the code you posted, by the way. Not enough time
today...

--
Peter Below (TeamB)
Remy Lebeau (Te...


Posts: 9,447
Registered: 12/23/01
Re: OLE in a thread problem.  
Click to report abuse...   Click to reply to this thread Reply
  Posted: Jul 23, 2015 10:43 AM   in response to: Mike Meier in response to: Mike Meier
Mike wrote:

Here are the three routines that start, execute, and end the thread.

You did not show any of the code for TOMNICThread itself. I assume that
TOMNICThread.Execute() is simply calling TMainForm.ThreadUpdate(), and TMainForm.ThreadEnd()
is assumes to the TOMNICThread.OnTerminate event. Is this right?

You are calling CoInitializeEx() in ThreadStart(), which is run in the context
of your main UI thread. Assuming ThreadEnd() is the OnTerminate handler,
it is calling CoUninitialize() in the context of the main UI thread as well.
Either way, you are calling the functions outside the context of your worker
thread that will actually be accessing the COM object. ThreadStart() and
ThreadEnd() are the wrong places to call the functions. You MUST call them
inside of the TOMNICThread.Execute() method instead, before the thread accesses
the COM object, and after it is done using COM, respectively. The VCL already
initializes COM for the main UI thread at app startup. Worker threads must
initialize COM for themselves.

And you CANNOT directly access a COM object across thread/apartment boundaries
UNLESS it is specifically designed to support that, which you have no control
over inside your hosting app. Otherwise, you MUST marshal the object across
boundaries, using either CoMarshalInterThreadInterfaceInStream()/CoGetInterfaceAndReleaseStream()
or IGlobalInterfaceTable. If you do not need to reuse a single object for
multiple jobs, you should create the COM object inside the context of the
thread that will actually be using it. That means creating the COM object
inside of TOMNICThread.Execute(), unless you create it in the main thread
and marshal it. Otherwise, IGlobalInterfaceTable would be best choice.
Create the object once and store it, and then retreive it whenever you need
to use it and let IGlobalInterfaceTable handle the marshaling for you.

--
Remy Lebeau (TeamB)
Mike Meier

Posts: 12
Registered: 5/25/05
Re: OLE in a thread problem.  
Click to report abuse...   Click to reply to this thread Reply
  Posted: Jul 27, 2015 10:37 AM   in response to: Remy Lebeau (Te... in response to: Remy Lebeau (Te...
Remy Lebeau (TeamB) wrote:
Mike wrote:

Here are the three routines that start, execute, and end the thread.

You did not show any of the code for TOMNICThread itself. I assume that
TOMNICThread.Execute() is simply calling TMainForm.ThreadUpdate(), and TMainForm.ThreadEnd()
is assumes to the TOMNICThread.OnTerminate event. Is this right?

You are calling CoInitializeEx() in ThreadStart(), which is run in the context
of your main UI thread. Assuming ThreadEnd() is the OnTerminate handler,
it is calling CoUninitialize() in the context of the main UI thread as well.
Either way, you are calling the functions outside the context of your worker
thread that will actually be accessing the COM object. ThreadStart() and
ThreadEnd() are the wrong places to call the functions. You MUST call them
inside of the TOMNICThread.Execute() method instead, before the thread accesses
the COM object, and after it is done using COM, respectively. The VCL already
initializes COM for the main UI thread at app startup. Worker threads must
initialize COM for themselves.

And you CANNOT directly access a COM object across thread/apartment boundaries
UNLESS it is specifically designed to support that, which you have no control
over inside your hosting app. Otherwise, you MUST marshal the object across
boundaries, using either CoMarshalInterThreadInterfaceInStream()/CoGetInterfaceAndReleaseStream()
or IGlobalInterfaceTable. If you do not need to reuse a single object for
multiple jobs, you should create the COM object inside the context of the
thread that will actually be using it. That means creating the COM object
inside of TOMNICThread.Execute(), unless you create it in the main thread
and marshal it. Otherwise, IGlobalInterfaceTable would be best choice.
Create the object once and store it, and then retreive it whenever you need
to use it and let IGlobalInterfaceTable handle the marshaling for you.

--
Remy Lebeau (TeamB)

Thanks, all. It sounds like I was trying to push a square peg into a round hole. I didn't have to absolutely execute this code in a thread, but once I started trying I got fixated on it. Personality defect.

In this program I try to execute all of the code that talks to the outside world in a thread, for obvious reasons. In this case the outside world still inside the PC, but it is quick, it just tells OMNIC (software that runs a spectrometer) to do something and OMNIC talks to its (outside world) hardware. Still, I need to do some more testing to make sure OMNIC can't freeze my program.

And yes, all the thread did was call a procedure in my main program, and when it was done it called another. There was very little code in the thread itself.

The coinitialize/couninitialze commands had to be placed just before and immediately after the thread executed. In my test program I had put them just before and after I call the routine that creates and starts the thread, ThreadStart, but when I moved it to the target program the thread took longer to execute (It was doing other things as well) and counitialize would be called before it was done. Or so I figure, and that took a while to figure out. Dynamic stuff, these threads are. I managed to get it mostly working, at least not generating the coninitialize errors. I'll try what you suggested, but for now I'm abandoning the thread idea.

This unit that TypeLibrary created for me includes various interface and co... types. Perhaps those were meant to supported threads. If it turns out I need to revisit this I'll be back to ask for pointers.

Again, thanks.
Mike
Legend
Helpful Answer (5 pts)
Correct Answer (10 pts)

Server Response from: ETNAJIVE02