8

I'm doing some debugging on a messy project I picked up where the previous developer didn't know what they were doing, and the main issue is failed attempts to multi-thread the application. I'm now cleaning up the mess and trying to figure out where things are going wrong. One of the issues is inconsistent calls to CoInitialize in order to use ADO components.

Continued from my previous question, how can I identify how many levels of CoInitialize have been called?

For example, take this code into consideration:

CoInitialize(nil);
try
  CoInitialize(nil);
  try
    //2 levels have been called, how to programatically check this?
  finally
    CoUninitialize;
  end;
finally
  CoUninitialize;
end;
Community
  • 1
  • 1
Jerry Dodge
  • 26,858
  • 31
  • 155
  • 327
  • 10
    You're not finding an answer with Google or previous questions here because there's no need to do it and so no one has asked before. The fix for *every single thread* in your app is to put the call to `CoInitialize` at the start of the thread's `Execute`, followed by a `try` block that contains the code's processing loop and ends with a `finally` that calls `CoUnitialize`. There's no need to count the number of calls, because you can be sure that every thread contains a single pair of the calls that are properly paired. (This was mentioned in your previous question, BTW.) – Ken White Jan 27 '13 at 01:51
  • 1
    Before I jump in and start changing things, I'm putting together an analysis of how fragmented the code is, and then propose a work plan to fix it, which doesn't involve the ridiculous deadline that's expected of me for this task. I will most likely re-build it from scratch, but I need to first prove how horribly it was designed. That is why I'm wanting to do this. – Jerry Dodge Jan 27 '13 at 03:36
  • 2
    You don't need to analyse, or propose a plan. There only is one plan, just as Ken says. Implement that. If analysis was needed, I'd simply make CoInitialize and CoUninitialize route to functions that logged their being called. – David Heffernan Jan 27 '13 at 08:58
  • 1
    @DavidHeffernan There are many more issues in this code than just `CoInitialize`, this is one tiny percentage of the problem. I know what you're trying to tell me. That's besides the point. I would have never even mentioned why I need it if people here hadn't pushed it out of me. – Jerry Dodge Jan 27 '13 at 13:48

4 Answers4

11

If I had to solve this problem I'd tackle it by instrumenting calls to CoInitialize, CoInitializeEx and CoUninitialize. I would hook the calls to those functions and use a thread local variable to count the calls.

You can do this by adding the following unit to your project.

unit InstrumentCOMinit;

interface

uses
  SysUtils, Windows, ComObj, ActiveX;

threadvar
  COMinitCount: Integer;

implementation

function CoInitialize(pvReserved: Pointer): HResult; stdcall; external 'ole32.dll';
function CoInitializeEx(pvReserved: Pointer; coInit: Longint): HResult; stdcall; external 'ole32.dll';
procedure CoUninitialize; stdcall; external 'ole32.dll';

function InstrumentedCoInitialize(pvReserved: Pointer): HResult; stdcall;
begin
  Result := CoInitialize(pvReserved);
  if Succeeded(Result) then
    inc(COMinitCount);
end;

function InstrumentedCoInitializeEx(pvReserved: Pointer; coInit: Longint): HResult; stdcall;
begin
  Result := CoInitializeEx(pvReserved, coInit);
  if Succeeded(Result) then
    inc(COMinitCount);
end;

procedure InstrumentedCoUninitialize; stdcall;
begin
  CoUninitialize;
  dec(COMinitCount);
end;

procedure Fail;
begin
  raise EAssertionFailed.Create('Fixup failed.');
end;

procedure PatchCode(Address: Pointer; const NewCode; Size: Integer);
var
  OldProtect: DWORD;
begin
  if not VirtualProtect(Address, Size, PAGE_EXECUTE_READWRITE, OldProtect) then begin
    Fail;
  end;
  Move(NewCode, Address^, Size);
  FlushInstructionCache(GetCurrentProcess, nil, 0);
  if not VirtualProtect(Address, Size, OldProtect, @OldProtect) then begin
    Fail;
  end;
end;

type
  PInstruction = ^TInstruction;
  TInstruction = packed record
    Opcode: Byte;
    Offset: Integer;
  end;

procedure RedirectProcedure(OldAddress, NewAddress: Pointer);
var
  NewCode: TInstruction;
begin
  NewCode.Opcode := $E9;//jump relative
  NewCode.Offset := NativeInt(NewAddress)-NativeInt(OldAddress)-SizeOf(NewCode);
  PatchCode(OldAddress, NewCode, SizeOf(NewCode));
end;

initialization
  RedirectProcedure(@ActiveX.CoInitialize, @InstrumentedCoInitialize);
  RedirectProcedure(@ActiveX.CoInitializeEx, @InstrumentedCoInitializeEx);
  RedirectProcedure(@ActiveX.CoUninitialize, @InstrumentedCoUninitialize);
  ComObj.CoInitializeEx := InstrumentedCoInitializeEx;

end.

Unlike Serg's approach, this technique will not change the semantics of your program.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
6

You can do it like this:

function CoInitializeCount: Integer;
var
  HR: HResult;
  I: Integer;

begin
  Result:= 0;
  repeat
    HR:= CoInitialize(nil);
    if (HR and $80000000 <> 0) then begin
      Result:= -1;
      Exit;
    end;
    CoUnInitialize;
    if (HR <> S_OK) then begin
      CoUnInitialize;
      Inc(Result);
    end
    else Break;
  until False;
  for I:= 0 to Result - 1 do
    CoInitialize(nil);
end;

Warning! Since the above function closes COM it cannot be used in COM applications, only to answer the particular question while debugging.

kludg
  • 27,213
  • 5
  • 67
  • 118
  • 1
    You are calling CoUnInitialize twice for each CoInitialize. This can be easily tested. Just add ` Var UI,I:Integer; Function CoUnInitialize:Integer; begin inc(UI); ActiveX.CoUninitialize; end; Function CoInitialize(p:Pointer):HRESULT; begin inc(I); Result := ActiveX.CoInitialize(p); end; to your code and try: procedure TForm1.Button2Click(Sender: TObject); begin UI := 0; I := 0; CoInitialize(nil); CoInitialize(nil); CoInitialize(nil); CoInitialize(nil); CoInitializeCount; Showmessage(IntToStr(UI)+'-'+IntToStr(i)); end; ` – bummi Jan 27 '13 at 07:57
  • @bummi - Sorry, I don't understand how your comment is related to the OP question & my answer. Sure you can simply count `CoInitialize` calls if you have full control over all code, what was asked IMO is how many times 3rdparty code called `CoInitialize`. – kludg Jan 27 '13 at 08:17
  • Your function is calling CoUninitialize to often (*2). Here: if (HR <> S_OK) then begin and here again: for I:= 0 to Result - 1 do CoInitialize(nil); My example was made only to show this. – bummi Jan 27 '13 at 08:22
  • @bummi My function calls `CoUnitialize` exact number of times as it required to answer the OP question. – kludg Jan 27 '13 at 08:26
  • 1
    @JeroenWiertPluimers - I don't know what thread safety problem you are talking about. Can you explain? – kludg Jan 27 '13 at 08:29
  • Using the obove shown wrapper for the funtions, with this call UI := 0; I := 0; CoInitialize(nil); CoInitialize(nil); CoInitialize(nil); CoInitialize(nil); Showmessage(IntToStr(CoInitializeCount)); Showmessage(IntToStr(UI)+'-'+IntToStr(i)); You will get: '5' from your funtion and '11-15' from the counters... IMHO removing for I:= 0 to Result - 1 do CoInitialize(nil); would be needed – bummi Jan 27 '13 at 08:35
  • @bummi - I just answered the OP question. Your counters answers a different question that is not asked. – kludg Jan 27 '13 at 08:39
  • 5
    @Jeroen's comment makes no sense. This code is threadsafe. However, it will quite possibly break the app because any COM objects that exist when this function is called now live in a different COM instance. And the apartment model may have changed! – David Heffernan Jan 27 '13 at 09:00
  • 1
    @DavidHeffernan +1, the code should not be used in COM applications, only to answer the particular question while debugging. – kludg Jan 27 '13 at 09:29
  • Ok, blame on me, but wouldn't dec(Result); at the end be needed. With no Coinitialize I get 1, with one 1 get 2 .... – bummi Jan 27 '13 at 09:31
  • 1
    @Serg far cleaner in my view would be to hook the CoXXX methods. – David Heffernan Jan 27 '13 at 09:36
  • Although this is a dangerous solution, it does appear to be the only real *answer* to my question. Therefore, I will keep it as the accepted answer. The other answers here are good to have though. – Jerry Dodge Jan 27 '13 at 14:09
  • @Jerry What do you mean by it being the only real answer? You have at least two other tenable answers. – David Heffernan Jan 27 '13 at 15:20
  • You guys are right, my reasoning was tht if this method is called in thread A, then a context switch goes for thread B, which calls CoInitialize, then the count would be wrong. But it won't be: as the CoInitialize count is specific for a thread. My bad. Let me know if I should delete the comment. – Jeroen Wiert Pluimers Jan 29 '13 at 13:55
2

If I should clean such project, I would create an abstract thread ancestor having Execute overriden and split into three virtual methods e.g. BeforeExecuteTask, AfterExecuteTask and abstract ExecuteTask.

I'd move COM (un)initialization into Before/After methods and delete all other occurencies (DRY). In each descendant I'd move code from the original Execute method to overriden ExecuteTask.

pf1957
  • 997
  • 1
  • 5
  • 20
  • 4
    This is not an answer to the question asked, and while it might be a good suggestion it should not be posted as an answer. (And most of what you said has already been posted as a comment to the original question, which is the proper place to post it, and Jerry made it clear he was not interested in any other information in this thread in those comments as well.) – Ken White Jan 27 '13 at 07:49
  • I have created a thread exactly like this (different naming) for another project long ago. It was a bit specific to that project though. But yes, this is a comment, not an answer. – Jerry Dodge Jan 27 '13 at 14:05
1

As far as Coinitialize and CoUninitialize would have to be counted per thread and CoUninitialize should not be called for counting as far as COM will be broken, you could use the following code for debugging.

unit CoinitCounter;

interface

uses Classes, Generics.Collections, ActiveX, SyncObjs, Windows;

Type
  TCoIniRec = Record
    ThreadID: Cardinal;
    Init: Integer;
    InvalidInit:Integer;
    CoInit: Integer;
    IsCoinitialized:Boolean;
  End;

  TCoIniList = TList<TCoIniRec>;

  TCoinitCounter = Class
  private
    FCS: TCriticalSection;
    FList: TCoIniList;

    Constructor Create;
    Destructor Destroy; override;
  public
    Function Coinitialize(p: Pointer): HRESULT;
    Procedure CoUninitialize;
    Function LeftInitCount: Integer;
    Function ValidInits: Integer;
    Function InValidInits: Integer;
    Function IsCoinitialized:Boolean;

  End;

var
  FCoinitCounter: TCoinitCounter;

implementation

{ TCoinitCounter }

function TCoinitCounter.Coinitialize(p: Pointer): HRESULT;
var
  r: TCoIniRec;
  i, x: Integer;
begin
  FCS.Enter;
  Result := ActiveX.Coinitialize(p);
  if  Succeeded(Result) then
  begin    
    x := -1;
    for i := 0 to FList.Count - 1 do
      if FList[i].ThreadID = GetCurrentThreadID then
        x := i;
    if x > -1 then
    begin
      r := FList[x];
      r.IsCoinitialized := true;
      if Result = s_OK then r.Init := r.Init + 1
      else r.InvalidInit := r.InvalidInit + 1;
      FList[x] := r;
    end
    else
    begin
      ZeroMemory(@r,SizeOf(r));
      r.ThreadID := GetCurrentThreadID;
      r.IsCoinitialized := true;
      if Result = s_OK then r.Init :=  1
      else r.InvalidInit :=  1;
      FList.Add(r);
    end;
  end;
  FCS.Leave;
end;

procedure TCoinitCounter.CoUninitialize;
var
  r: TCoIniRec;
  i, x: Integer;
begin
  FCS.Enter;
  x := -1;
  ActiveX.CoUninitialize;
  for i := 0 to FList.Count - 1 do
    if FList[i].ThreadID = GetCurrentThreadID then
      x := i;
  if x > -1 then
  begin
    r := FList[x];
    r.IsCoinitialized := false;
    r.CoInit := r.CoInit + 1;
    FList[x] := r;
  end
  else
  begin
    r.ThreadID := GetCurrentThreadID;
    r.IsCoinitialized := false;
    r.CoInit := 1;
    FList.Add(r);
  end;
  FCS.Leave;
end;

constructor TCoinitCounter.Create;
begin
  inherited;
  FCS := TCriticalSection.Create;
  FList := TCoIniList.Create;
end;

destructor TCoinitCounter.Destroy;
begin
  FCS.Free;
  FList.Free;

  inherited;
end;

function TCoinitCounter.InValidInits: Integer;
var
  i, x: Integer;
begin
  FCS.Enter;
  x := -1;
  for i := 0 to FList.Count - 1 do
    if FList[i].ThreadID = GetCurrentThreadID then
      x := i;
  if x > -1 then
    Result :=  FList[x].InvalidInit
  else
    Result := 0;
  FCS.Leave;
end;

function TCoinitCounter.LeftInitCount: Integer;
var
  i, x: Integer;
begin
  FCS.Enter;
  x := -1;
  for i := 0 to FList.Count - 1 do
    if FList[i].ThreadID = GetCurrentThreadID then
      x := i;
  if x > -1 then
    Result := FList[x].Init + FList[x].InvalidInit - FList[x].CoInit
  else
    Result := 0;
  FCS.Leave;
end;

function TCoinitCounter.IsCoinitialized: Boolean;
var
  i, x: Integer;
begin
  FCS.Enter;
  x := -1;
  for i := 0 to FList.Count - 1 do
    if FList[i].ThreadID = GetCurrentThreadID then
      x := i;
  if x > -1 then
    Result := FList[x].IsCoinitialized
  else
    Result := false;
  FCS.Leave;
end;


function TCoinitCounter.ValidInits: Integer;
var
  i, x: Integer;
begin
  FCS.Enter;
  x := -1;
  for i := 0 to FList.Count - 1 do
    if FList[i].ThreadID = GetCurrentThreadID then
      x := i;
  if x > -1 then
    Result :=  FList[x].Init
  else
    Result := 0;
  FCS.Leave;
end;

initialization

FCoinitCounter := TCoinitCounter.Create;

finalization

FCoinitCounter.Free;

end.

This

ThreadID 6968 deserved: 0 counted: 0 valid: 0 invalid 0
ThreadID 2908 deserved: 4 counted: 4 valid: 1 invalid 3
ThreadID 5184 deserved: 1 counted: 1 valid: 1 invalid 0
ThreadID 7864 deserved: 8 counted: 8 valid: 1 invalid 7
ThreadID 7284 deserved: 2 counted: 2 valid: 1 invalid 1
ThreadID 6352 deserved: 5 counted: 5 valid: 1 invalid 4
ThreadID 3624 deserved: 4 counted: 4 valid: 1 invalid 3
ThreadID 5180 deserved: 0 counted: 0 valid: 0 invalid 0
ThreadID 7384 deserved: 6 counted: 6 valid: 1 invalid 5
ThreadID 6860 deserved: 9 counted: 9 valid: 1 invalid 8

would be an example output of the following unit:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    procedure DispOnTerminate(Sender: TObject);
    { Private-Deklarationen }
  public
    { Public-Deklarationen }
  end;

var
  Form1: TForm1;

implementation
uses CoinitCounter;
{$R *.dfm}
Type
  TTestThread=Class(TThread)
    private
    FCounted,FTestCoinits:Integer;
    FValidInits: Integer;
    FInValidInits: Integer;
    protected
    Procedure Execute;override;
    public
    Constructor Create(cnt:Integer);overload;
    Property TestCoinits:Integer read FTestCoinits;
    Property Counted:Integer Read FCounted;
    Property ValidInits:Integer Read FValidInits;
    Property InivalidInits:Integer Read FInValidInits;
  End;


{ TTestThread }

constructor TTestThread.Create(cnt: Integer);
begin
  inherited Create(false);
  FTestCoinits:= cnt;
end;

procedure TTestThread.Execute;
var
 i:Integer;
begin
  inherited;
  for I := 1 to FTestCoinits  do
     FCoinitCounter.Coinitialize(nil);
  FCounted := FCoinitCounter.LeftInitCount;
  FValidInits := FCoinitCounter.ValidInits;
  FInValidInits := FCoinitCounter.InValidInits;
  for I := 1 to FCounted do
       FCoinitCounter.CoUninitialize;
end;




procedure TForm1.DispOnTerminate(Sender: TObject);
begin
  Memo1.Lines.Add(Format('ThreadID %d deserved: %d counted: %d valid: %d invalid %d'
     ,[TTestThread(Sender).ThreadID, TTestThread(Sender).TestCoinits,TTestThread(Sender).Counted,TTestThread(Sender).ValidInits,TTestThread(Sender).InivalidInits]));
end;
procedure TForm1.Button1Click(Sender: TObject);
var
 i:Integer;
begin

  for I := 1 to 10  do
      with TTestThread.Create(Random(10)) do OnTerminate := DispOnTerminate;
end;

end.
bummi
  • 27,123
  • 14
  • 62
  • 101