web-dev-qa-db-ja.com

XE2でCOMが壊れていますか?どのように回避できますか?

更新:XE2 Update 2は、以下に説明するバグを修正します。

以下のプログラムは、実際のプログラムからのカットダウンであり、XE2の例外を除いて失敗します。これは2010年からの回帰です。テストするXEはありませんが、プログラムはXEで正常に動作することを期待しています(コードがXEで正常に動作することを確認してくれたPrimožに感謝します)。

program COMbug;

{$APPTYPE CONSOLE}

uses
  SysUtils, Variants, Windows, Excel2000;

var
  Excel: TExcelApplication;
  Book: ExcelWorkbook;
  Sheet: ExcelWorksheet;
  UsedRange: ExcelRange;
  Row, Col: Integer;
  v: Variant;

begin
  Excel := TExcelApplication.Create(nil);
  try
    Excel.Visible[LOCALE_USER_DEFAULT] := True;
    Book := Excel.Workbooks.Add(EmptyParam, LOCALE_USER_DEFAULT) as ExcelWorkbook;
    Sheet := Book.Worksheets.Add(EmptyParam, EmptyParam, 1, EmptyParam, LOCALE_USER_DEFAULT) as ExcelWorksheet;

    Sheet.Cells.Item[1,1].Value := 1.0;
    Sheet.Cells.Item[2,2].Value := 1.0;
    UsedRange := Sheet.UsedRange[LOCALE_USER_DEFAULT] as ExcelRange;
    for Row := 1 to UsedRange.Rows.Count do begin
      for Col := 1 to UsedRange.Columns.Count do begin
        v := UsedRange.Item[Row, Col].Value;
      end;
    end;
  finally
    Excel.Free;
  end;
end.

XE2 32ビットでは、エラーは次のとおりです。

プロジェクトCOMbug.exeは、メッセージ 'システム例外(コード0xc000001d)at0x00dd6f3e'で例外クラス$ C000001Dを発生させました。

エラーは、UsedRange.Columnsの2回目の実行で発生します。

XE2 64ビットでは、エラーは次のとおりです。

プロジェクトCOMbug.exeは、メッセージ「c0000005ACCESS_VIOLATION」で例外クラス$ C0000005を発生させました

繰り返しになりますが、エラーはUsedRange.Columnsの2回目の実行で発生すると思いますが、64ビットデバッガーは少し奇妙な方法でコードをステップ実行するため、100%確信はありません。

この問題について QCレポート を提出しました。

Delphi COM /オートメーション/インターフェイススタックの何かが包括的に壊れているように私には非常に見えます。これは、私のXE2採用の完全なショーストッパーです。

誰かがこの問題の経験がありますか?問題を回避する方法について、誰かがヒントやアドバイスを持っていますか?ここで実際に起こっていることをデバッグすることは、私の専門分野の外です。

53
David Heffernan

Workaround

rowCnt := UsedRange.Rows.Count;
colCnt := UsedRange.Columns.Count;
for Row := 1 to rowCnt do begin
  for Col := 1 to colCnt do begin
    v := UsedRange.Item[Row, Col].Value;
  end;
end;

これも機能します(より複雑なユースケースで回避策を見つけるのに役立つ場合があります)。

function ColCount(const range: ExcelRange): integer;
begin
  Result := range.Columns.Count;
end;

for Row := 1 to UsedRange.Rows.Count do begin
  for Col := 1 to ColCount(UsedRange) do begin
    v := UsedRange.Item[Row, Col].Value;
  end;
end;

分析

で_Releaseを実行すると、DispCallByIDのSystem.Win.ComObjでクラッシュします。

varDispatch, varUnknown:
  begin
    if PPointer(Result)^ <> nil then
      IDispatch(Result)._Release;
    PPointer(Result)^ := Res.VDispatch;
  end;

Delphi XE(XEはアセンブラバージョンを使用)のこの同じ手順のPUREPASCALバージョンは異なりますが...

varDispatch, varUnknown:
  begin
    if PPointer(Result)^ <> nil then
      IDispatch(Result.VDispatch)._Release;
    PPointer(Result)^ := Res.VDispatch;
  end;

...どちらの場合もアセンブラコードは同じです(編集:正しくありません。最後のメモを参照してください):

@ResDispatch:
@ResUnknown:
        MOV     EAX,[EBX]
        TEST    EAX,EAX
        JE      @@2
        Push    EAX
        MOV     EAX,[EAX]
        CALL    [EAX].Pointer[8]
@@2:    MOV     EAX,[ESP+8]
        MOV     [EBX],EAX
        JMP     @ResDone

興味深いことに、これはクラッシュします...

for Row := 1 to UsedRange.Rows.Count do begin
  for Col := 1 to UsedRange.Columns.Count do begin
  end;
end;

...そしてこれはそうではありません。

row := UsedRange.Rows.Count;
col := UsedRange.Columns.Count;
col := UsedRange.Columns.Count;

この理由は、非表示のローカル変数を使用しているためです。最初の例では、コードは次のようにコンパイルされます...

00564511 6874465600       Push $00564674
00564516 6884465600       Push $00564684
0056451B A12CF35600       mov eax,[$0056f32c]
00564520 50               Push eax
00564521 8D8508FFFFFF     lea eax,[ebp-$000000f8]
00564527 50               Push eax
00564528 E8933EEAFF       call DispCallByIDProc

...そしてそれは2回呼び出されます。

2番目の例では、スタック上の2つの異なる一時的な場所が使用されます(ebp-????オフセット)。

00564466 6874465600       Push $00564674
0056446B 6884465600       Push $00564684
00564470 A12CF35600       mov eax,[$0056f32c]
00564475 50               Push eax
00564476 8D8514FFFFFF     lea eax,[ebp-$000000ec]
0056447C 50               Push eax
0056447D E83E3FEAFF       call DispCallByIDProc
...
0056449B 6874465600       Push $00564674
005644A0 6884465600       Push $00564684
005644A5 A12CF35600       mov eax,[$0056f32c]
005644AA 50               Push eax
005644AB 8D8510FFFFFF     lea eax,[ebp-$000000f0]
005644B1 50               Push eax
005644B2 E8093FEAFF       call DispCallByIDProc

このバグは、この一時的な場所に保存されている内部インターフェイスがクリアされているときに発生します。これは、このインターフェイスにすでに保存されているものがあるため、「for」ケースが2回実行された場合にのみ発生します。初めて。 2番目の例では、2つの場所が使用されているため、この内部インターフェースは常に0に初期化され、Releaseはまったく呼び出されません。

本当のバグは、この内部インターフェースにガベージが含まれていて、Releaseが呼び出されると、sh!tが発生することです。

さらに掘り下げてみると、古いインターフェイスを解放するアセンブラコードが同じではないことに気付きました。XE2バージョンには「moveax、[eax]」命令が1つありません。 IOW、

IDispatch(Result)._Release;

間違いであり、それは本当にあるべきです

IDispatch(Result.VDispatch)._Release;

厄介なRTLバグ。

82
gabr

これのほとんどは私の頭を超えていますが、CoInitializeへの呼び出しが必要かどうか疑問に思いました。 CoInitializeをWebで検索すると、次のページが返されました。

http://chrisbensen.blogspot.com/2007/06/delphi-tips-and-tricks.html

そのページは、.Releaseの呼び出しに関連しているため、OPとgabrの分析からの問題を説明しているように見えます。コードの機能を独自のプロシージャに移動すると役立つ場合があります。テストするXEまたはXE2がありません。

編集:ラット-上記へのコメントとしてこれを追加することを意味します。

1
user496736