53 votes

COM est-il cassé dans XE2, et comment puis-je le contourner ?

Mise à jour : La mise à jour 2 de XE2 corrige le bogue décrit ci-dessous.

Le programme ci-dessous, découpé à partir du programme réel, échoue avec une exception dans XE2. Il s'agit d'une régression depuis 2010. Je n'ai pas XE pour tester mais je m'attends à ce que le programme fonctionne bien sous XE (merci à Primož pour avoir confirmé que le code fonctionne bien sous 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.

Dans XE2 32 bit l'erreur est :

Le projet COMbug.exe a soulevé une exception de classe $C000001D avec le message 'exception système (code 0xc000001d) à 0x00dd6f3e'.

L'erreur se produit lors de la deuxième exécution de UsedRange.Columns .

Dans XE2 64 bit l'erreur est :

Le projet COMbug.exe a soulevé une exception de classe $C0000005 avec le message 'c0000005 ACCESS_VIOLATION'.

Encore une fois, je pense que l'erreur se produit lors de la deuxième exécution de UsedRange.Columns Mais le débogueur 64 bits parcourt le code d'une manière un peu bizarre, donc je n'en suis pas sûr à 100%.

J'ai soumis un Rapport CQ pour la question.

J'ai l'impression que quelque chose dans la pile COM/automatisation/interface de Delphi est complètement cassé. C'est un véritable coup d'arrêt pour mon adoption de XE2.

Quelqu'un a-t-il une expérience de ce problème ? Quelqu'un a-t-il des conseils à me donner sur la façon de contourner le problème ? Le débogage de ce qui se passe réellement ici ne relève pas de mon domaine d'expertise.

82voto

gabr Points 20458

Solution de rechange

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;

Cela fonctionne également (et peut vous aider à trouver une solution de contournement dans des cas d'utilisation plus complexes) :

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;

Analyse

Il se bloque dans System.Win.ComObj dans DispCallByID lors de l'exécution de _Release en

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

Bien que la version PUREPASCAL de cette même procédure dans Delphi XE (XE utilise une version assembleur) soit différente ...

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

... le code assembleur dans les deux cas est le même (EDIT : pas vrai, voir mes notes à la fin) :

@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

Il est intéressant de noter que ce crash...

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

... et ce n'est pas le cas.

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

La raison en est l'utilisation de variables locales cachées. Dans le premier exemple, le code se compile en ...

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

... et qui est appelé deux fois.

Dans le deuxième exemple, deux emplacements temporaires différents sur la pile sont utilisés (décalages 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

Le bogue se produit lorsqu'une interface interne stockée dans cet emplacement temporaire est effacée, ce qui ne se produit que lorsque le cas "for" est exécuté pour la deuxième fois, car il y a déjà quelque chose de stocké dans cette interface - il a été placé là lorsque "for" a été appelé pour la première fois. Dans le deuxième exemple, deux emplacements sont utilisés de sorte que cette interface interne est toujours initialisée à 0 et que Release n'est pas du tout appelé.

Le véritable problème est que cette interface interne contient des déchets et que lorsque Release est appelé, rien ne se passe.

Après avoir creusé un peu plus, j'ai remarqué que le code assembleur qui libère l'ancienne interface n'est pas le même - la version XE2 manque une instruction "mov eax, [eax]". C'EST À DIRE,

IDispatch(Result)._Release;

est une erreur et il devrait vraiment être

IDispatch(Result.VDispatch)._Release;

Un méchant bug RTL.

1voto

SilentD Points 476

La plupart de ces questions me dépassent, mais je me suis demandé si un appel à CoInitialize était nécessaire. En cherchant CoInitialize sur le Web, j'ai trouvé cette page :

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

On dirait presque que cette page décrit le problème du PO et de l'analyse de gabr, car il est lié à un appel à .Release. Déplacer la fonctionnalité du code dans sa propre procédure pourrait aider. Je n'ai pas XE ou XE2 à tester.

Edit : Rats - je voulais ajouter ceci comme commentaire à ce qui précède.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X