XE2中的COM是否存在问题,我该如何解决?

54

更新:XE2 Update 2修复了下面描述的bug。

下面这个程序是从真实程序中剪裁出来的,它在XE2中会出现异常。这是自2010年以来的退化。我没有XE进行测试,但我期望该程序在XE上运行良好(感谢Primož确认代码在XE上可以正常运行)。

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 位版本中,错误是:

  

Project COMbug.exe raised exception class $C000001D with message 'system exception (code 0xc000001d) at 0x00dd6f3e'.

该错误发生在第二次执行 UsedRange.Columns 时。

在 XE2 64 位版本中,错误是:

  

Project COMbug.exe raised exception class $C0000005 with message 'c0000005 ACCESS_VIOLATION'

同样,我认为错误发生在第二次执行 UsedRange.Columns 时,但是 64 位调试器的代码步进方式稍微有些奇怪,所以我不能百分之百确定。

我已经针对该问题提交了一个QC报告

对我来说,很明显 Delphi 的 COM/automation/interface 栈中出现了严重问题。这对于我使用 XE2 是一个完全的停机现象。

有人遇到过这个问题吗?有人有任何提示和建议可以尝试解决这个问题吗?调试这里正在发生什么排除问题超出了我的专业领域。


5
确认 - 在XE版本中可运行,在XE2中会崩溃。 - gabr
2
它在DispCallByIDProc内部崩溃了。很丑陋。 - gabr
1
不需要将 v := UsedRange.Item[Row, Col].Value 赋值给变量 - 即使没有赋值也会崩溃。 - gabr
1
如果错误出现在DispCallByIDProc中,您可以尝试使用DWScript中的COMConnector来绕过它,因为它实现了类似的功能。如果这也不起作用,那么可能是编译器处理COM调用约定的错误。 - Eric Grange
3
听起来与这里发生的情况完全相同:https://dev59.com/TlzUa4cB1Zd3GeqP4IYL - Arnaud Bouchez
显示剩余4条评论
2个回答

82

解决方法

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时,在System.Win.ComObj的DispCallByID中崩溃。

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

尽管 Delphi 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

...并被调用了两次。

在第二个示例中,使用了栈上两个不同的临时位置 (ebp - ???? offsets):

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
当一个内部接口存储在临时位置被清除时,就会出现这个bug,这仅发生在第二次执行“for”情况时,因为在第一次调用“for”时已经将某些内容存储在此接口中。在第二个示例中,使用了两个位置,因此该内部接口始终初始化为0,并且根本不会调用Release。
进一步研究后,我注意到释放旧接口的汇编代码不同- XE2版本缺少一个“mov eax,[eax]”指令。换言之,
IDispatch(Result)._Release;

这是个错误,实际上应该是

IDispatch(Result.VDispatch)._Release;

不好的RTL错误。


2
这绝对是编译器回归问题,需要在质量中心进行处理。 - Warren P
11
编译器是一项非常了不起的工作。它有缺陷,将来也会有。但它仍然是最令人难以置信的语言和编译技术。 (* 不,我不为 Embarcadero 工作。*) - Warren P
2
@gabr 非常感谢您在这里的出色工作。我认为现在我已经有足够的东西来解决这个错误了。让我们希望Emba在下一个XE2更新中修复它。 - David Heffernan
4
从技术上讲,这不是编译器的bug而是RTL的问题,这是准确的吗?这到底是怎么发生的?为什么会更改那段代码,并且允许以引入如此严重的错误的方式进行更改?渴望知道答案的人们需要解答。 - Deltics
5
@Deltics说:"不会让你参加XE2的测试计划"。显然,他们给已注册XE用户优先权,而我没有。为什么他们不想让我免费测试和调试他们的产品超出了我的理解。我本可以发现这个错误(和许多其他错误)。对我来说完全没有意义。 - David Heffernan
显示剩余8条评论

1

大部分内容都超出了我的理解范围,但我想知道是否需要调用CoInitialize。在网上搜索CoInitialize返回了这个页面:

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

它几乎像是描述了与调用.Release相关的问题,从OP和gabr的分析中提出了解决方案。将代码功能移到自己的过程中可能会有所帮助。我没有XE或XE2进行测试。

编辑:糟糕 - 打算把这个作为对上面的评论。


嗯,我在我的简单示例中确实错过了那个,谢谢。不过这没关系,可能是因为Excel正在运行out-of-proc。即使你添加它,相同的行为也会发生。当然,从这个示例中提取的原始代码已经正确初始化了COM。 - David Heffernan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接