Page 1 of 2 1 2 LastLast
Results 1 to 15 of 19

Thread: Simpele maar doeltreffende stackview bij exception

  1. #1
    Reader
    Join Date
    May 2002
    Location
    Holland
    Posts
    3,382

    Simpele maar doeltreffende stackview bij exception

    Ik geloof dat ik met enkele regels code een leuke oplossing heb om de stack weer te geven bij een exception. Het maakt gebruik van het automatisch vrijgeven van interfaces door Delphi.
    Wel moet iedere method of procedure voorzien worden van een extra regel code ("Enter"). Voorzien van een "ifdef" of desnoods een "if Tracing" omdat het natuurlijk enigszins vertragend werkt.
    Commentaar welkom.

    testcode:
    Code:
    procedure DoIets;
    begin
      {$ifdef tracing} Enter('DoeIets'); {$endif}
      raise Exception.Create('Fake Exception');
    end;
    
    procedure TForm1.Test;
    begin
      {$ifdef tracing} Enter('TForm1.Test'); {$endif}
      DoIets;
    end;
    
    procedure TForm1.Button1Click;
    begin
      {$ifdef tracing} Enter('TForm1.Button1Click'); {$endif}
      Test;
    end;
    Dit is de unit:

    Code:
    uses
      Classes, Forms, SysUtils, Generics.Collections, Dialogs;
    
    type
      IMethod = interface
        ['{4C49BB29-4B74-48DC-80E6-224F8007B0D4}']
      end;
    
      TMethodObj = class(TInterfacedObject, IMethod)
      private
        fName: string;
      public
        constructor Create(const S: string);
        destructor Destroy; override;
      end;
    
    function Enter(const S: string): IMethod;
    function StackString: string;
    
    implementation
    
    var
      SL: TList<string>;
      CatchedException: TObject;
    
    function IsClass(Obj: TObject; Cls: TClass): Boolean;
    var
      Parent: TClass;
    begin
      Parent := Obj.ClassType;
      while (Parent <> nil) and (Parent.ClassName <> Cls.ClassName) do
        Parent := Parent.ClassParent;
      Result := Parent <> nil;
    end;
    
    procedure CatchException(O: TObject);
    var
      S: string;
    begin
      if CatchedException = O then
        Exit;
      CatchedException := O;
      S := 'Unknown Error';
      if IsClass(O, Exception) then
        if not IsClass(O, EAbort) then
           S := Exception(O).Message + #13 + #13 + StackString;
      ShowMessage(S);
    end;
    
    constructor TMethodObj.Create(const S: string);
    begin
      if SL = nil then
        SL := TList<string>.Create;
      fName := S;
      SL.Add(S);
    end;
    
    destructor TMethodObj.Destroy;
    var
      O: TObject;
    begin
      if SL = nil then
        Exit;
    
      O := ExceptObject;
      if O <> nil then
        CatchException(O);
    
      if (SL.Count > 0) and (SL[Sl.Count - 1] <> fName) then
      begin
        ShowMessage('stack error');
        inherited;
        Exit;
      end;
    
      SL.Delete(Sl.Count - 1);
      if SL.Count = 0 then
      begin
        FreeAndNil(SL);
      end;
      inherited;
    end;
    
    function Enter(const S: string): IMethod;
    begin
      Result := TMethodObj.Create(S);
    end;
    
    function StackString: string;
    var
      i: Integer;
    begin
      Result := '';
      if SL = nil then
        Exit;
      for i := 0 to SL.Count - 1 do
      begin
        if Result <> '' then Result := Result + ' -> ';
        Result := Result + Sl[i];
      end;
    end;
    
    end.

  2. #2
    mov rax,marcov; push rax marcov's Avatar
    Join Date
    Apr 2004
    Location
    Ehv, Nl
    Posts
    10,357
    Wat wordt het precies geacht te doen?

    Verder lijken er aannames te zijn wanneer een interface vrijgeven wordt. Aangezien je de reference die je krijgt van "ENTER" niet ergens stouwt en later gebruikt, kan die in principe direct vrijgegeven worden? Volgens de regels van de taal kan een interface opgeruimd worden het statement NA er niets meer naar verwijst. (dit is om te vermijden dat tijdens het creeren van b.v. een argument list een tijdelijke variable direct opgeruimd wordt)

    Dat kan vanaf dat moment tot aan het eind van de functie, en op alle statement overgangen daar tussen in.

    Verschillende compilers (FPC,Delphi) en versies daarvan kunnen dus anders reageren hierop, mogelijk ook nog afhankelijk van optimalizatie settings.

    Er lijken ook aannames gemaakt te worden over de volgorde van de interface TEMP's op de stack. Dat is ook fout, die kunnen in elke willekeurige volgorde aangemaakt en opgeruimd worden.

  3. #3
    Reader
    Join Date
    May 2002
    Location
    Holland
    Posts
    3,382
    Het wordt geacht (zie CatchException) de error weer te geven en de stackstring van procedures.

    Het is inderdaad zo dat ik *niets* met het resultaat van het Enter-result doe.
    Een vreemd ding inderdaad, maar als het werkt werkt het.
    Je kan natuurlijk ook LocalEnter := Enter('....') schrijven.

    Het aardige vond ik juist (maar als het fout gaat zie ik dat nog wel) dat je niet dit hoeft te doen:

    Code:
    procedure xxx;
    var
      M: IMethod;
    begin
      M  := Enter('xxx');
      try
        // code
      finally
        M := nil;
      end;
    end;
    Extended syntax moet i.i.g aan staan.
    En optimalisatie zal inderdaad wel gevolgen hebben, maar dat is te ifdeffen.

    De IMethod interface wordt volgens mij - maar jij weet meer van de interne compiler - in een verborgen try-finally in de procedure/functie/method vrijgegeven en dus lijkt mij dat de aanname wat betreft de stack niet fout is.

  4. #4
    mov rax,marcov; push rax marcov's Avatar
    Join Date
    Apr 2004
    Location
    Ehv, Nl
    Posts
    10,357
    Het probleem is heel eenvoudig te illustreren aan de hand van jouw voorbeeld.

    Je weet niet dat je "code" IN die try finally komt te staan. Er is niks wat dat verplicht.

    Het kan dus best

    delphi Code:
    1. try
    2.  // leeg
    3. finally
    4.   m:=nil;
    5. end;
    6.  
    7. //code;

    zijn. De compiler is daar vrij in. Zulke constructs gaan fout in FPC, die er een handje van heeft eerder te zijn dan Delphi. Maar ook onder Delphi kunnen er vreemde dingen gebeuren, maar dat meer in geval van ingewikkelde constructs en optimalizatie instellingen.

    De enige vaste regel die er is, is dat het statement waar de reference nil wordt, nog niet gedealloceert wordt. En dat is het "ENTER" statement.

    Denk eraan, "het werkt" en "het is correct" zijn totaal verschillende dingen. Dit soort dingen is heel erg leuk in kleine voorbeeldjes, maar als je het grootschalig eens een klein geval hebt waar het niet werkt, of een toekomstige Delphi compiler het vanwege een nieuw soort optimalizatie het NET anders doet, dan kan je je hele programma op assembler niveau gaan napluizen. En dit zijn rotdingen om te vinden.

    Overigens is de "try" in het voorbeeld ook relatief. In theorie kan de compiler b.v. een interface variabele in die alleen in een bepaalde tak van een if..then gebruikt wordt zo localiseren dat die daar gealloceert en gedealloceert wordt.

    In praktijk wordt dat niet gedaan omdat het huidige Delphi systeem iets efficienter is in het in een keer afhandelen van meerdere initializaties en finalizaties. Maar in b.v. zg leaf procedures met een frame in een if then zou dit in theorie kunnen
    Last edited by marcov; 29-Jul-11 at 16:28.

  5. #5
    Ik gebruik de debug spulle van JCL. Je kunt een map-file genereren tijdens het builden. Daarin staat dan alle informatie over de addressen van je functies etc. Bij een exception kan jcl op basis van het adres in de exception bepalen wat de call stack is geweest. Je krijgt dan dezelfde informatie die je ook in de IDE in je call stack window krijgt.

    Die map file kun je bovendien aan je executable toevoegen. Die wordt wel wat groter, maar dat is een kleine prijs voor het beschikbaar hebben van debug informatie zonder dat je applicatie zelf trager wordt.

    De (iets uitgeklede en generieker gemaakte) unit die ik hiervoor gebruik:

    Delphi Code:
    1. {
    2.  Unit    : uStackTrace
    3.  Purpose : Helper class for logging stacktraces upon unhandled exceptions. When
    4.            this unit is included in the first unit for the project, it will use
    5.            the stacktrace logging exposed through the JclDebugger. For this unit
    6.            to work, the jvcl must be installed, the option "insert JCL debug data"
    7.            must be enabled under the project menu and in the project options
    8.            "debug information" and "detailed map file" must be checked.
    9. }
    10.  
    11. unit uStackTrace;
    12.  
    13. interface
    14.  
    15. uses
    16.   Windows, Classes, SysUtils,
    17.   uEnvironmentFactory, uAppUtils, uIAppConst, uInitializationHandler,
    18.   IdException, JclDebug, JclHookExcept, JclFileUtils, uXMLOutput, uIUserSession;
    19.  
    20.  
    21. procedure LogException(ExceptObj   : TObject;
    22.                        ExceptAddr  : Pointer;
    23.                        OSException : Boolean);
    24. procedure AddIgnoreException(ExceptionClass: ExceptClass);
    25.  
    26. implementation
    27.  
    28. var
    29.   InternalIgnoreList: TList = nil;
    30.  
    31. {
    32.   Procedure : AddIgnoreException
    33.   Purpose   : Add exception classes to be ignored by the stack tracer.
    34. }
    35. procedure AddIgnoreException(ExceptionClass: ExceptClass);
    36. begin
    37.   if not Assigned(InternalIgnoreList) then
    38.     InternalIgnoreList := TList.Create;
    39.  
    40.   if InternalIgnoreList.IndexOf(ExceptionClass) = -1 then
    41.     InternalIgnoreList.Add(ExceptionClass);
    42. end;
    43.  
    44. {
    45.   Procedure : IsIgnoreException
    46.   Purpose   : Check if the given class should be ignored by the stack tracer.
    47. }
    48. function IsIgnoreException(ExceptionClass: TClass): Boolean;
    49. begin
    50.   Result := Assigned(InternalIgnoreList) and (InternalIgnoreList.IndexOf(ExceptionClass) > -1)
    51.   Result := False;
    52. end;
    53.  
    54. {
    55.   Procedure : LogException
    56.   Purpose   : LogException is called by JclDebug, when an unhandled exception is
    57.               thrown. We log the accompanying stacktrace here.
    58. }
    59. procedure LogException(ExceptObj  : TObject;
    60.                        ExceptAddr : Pointer;
    61.                        OSException: Boolean);
    62. var
    63.   StackTrace       : TStringList;
    64.   ExceptionMessage : String;
    65.   LogName          : String;
    66.   XMLUtil          : TXMLUtils;
    67.   XMLSession       : String;
    68.   Session          : IUserSession;
    69. begin
    70.   if IsIgnoreException(ExceptObj.ClassType) then
    71.     Exit;
    72.  
    73.   StackTrace   := TStringList.Create;
    74.   try
    75.     LogName := FormatDateTime('yyyymmddhhnnsszzz', Now);
    76.     JclLastExceptStackListToStrings(StackTrace, True, True, True);
    77.  
    78.     // Try to obtain the exception message
    79.     try
    80.       ExceptionMessage := (ExceptObj as Exception).Message
    81.     except
    82.       ExceptionMessage := 'Unknown uncaught exception';
    83.     end;
    84.  
    85.     StackTrace.Add('');
    86.     StackTrace.Add('Caused by: (' + ExceptObj.ClassName + ') ' + ExceptionMessage );
    87.  
    88.     // Always write exceptions to an file, the environment may not have been initialized yet,
    89.     // which would make the default logger unavailable
    90.     StackTrace.SaveToFile(GetApplicationPath + '\logs\exception-' + LogName + '.log');
    91.   finally
    92.     StackTrace.Free;
    93.   end;
    94. end;
    95.  
    96. initialization
    97.   JclStackTrackingOptions := JclStackTrackingOptions + [stRawMode];
    98.   JclStackTrackingOptions := JclStackTrackingOptions + [stExceptFrame];
    99.   JclStackTrackingOptions := JclStackTrackingOptions + [stStaticModuleList];
    100.   JclStartExceptionTracking;
    101.   JclAddExceptNotifier(LogException);
    102.  
    103. finalization
    104.   JclStopExceptionTracking;
    105.   JclRemoveExceptNotifier(LogException);
    106.  
    107.   FreeAndNil(InternalIgnoreList);
    108. end.
    Deze logt simpelweg elke exception + call stack in een aparte file. Er is de mogelijkheid om specifieke exceptions als uitzondering toe te voegen.

    De debug information kun je dus in je executable opnemen door 'Insert JCL Debug Data' aan te vinken in de JCL opties, maar ik heb dat zelf gedaan door een apart tooltje te runnen. In een project kun je batch files runnen na het builden. Daarvan maak ik gebruik om het volgende tooltje te runnen:
    Delphi Code:
    1. program InsertJclDebugInfoInExe;
    2. {$APPTYPE CONSOLE}
    3.  
    4. uses
    5.   Forms,
    6.   SysUtils,
    7.   JclDebug;
    8.  
    9. {$R *.res}
    10.  
    11. var
    12.   FileName: String;
    13.   MapFileName: String;
    14.   MapFileSize: Integer;
    15.   JclDebugDataSize: Integer;
    16. begin
    17.   Application.Initialize;
    18.   try
    19.     if ParamCount = 0 then
    20.     begin
    21.       WriteLn('Inserts a map file into an executable to be used by JclDebug.');
    22.       WriteLn('Map file must have the same name as executable, but with .map extension.');
    23.       WriteLn('Usage: ');
    24.       WriteLn(ExtractFileName(ParamStr(0)) + ' "c:\PathTo\Executable.exe"');
    25.       Exit;
    26.     end;
    27.  
    28.     FileName := ParamStr(1);
    29.  
    30.     if not FileExists(FileName) then
    31.     begin
    32.       WriteLn('Executable does not exist: ' + FileName);
    33.       Halt(1);
    34.     end;
    35.  
    36.     MapFileName := ChangeFileExt(FileName, '.map');
    37.     if not FileExists(FileName) then
    38.     begin
    39.       WriteLn('Map file does not exist: ' + FileName);
    40.       Halt(2);
    41.     end;
    42.  
    43.     InsertDebugDataIntoExecutableFile(PAnsiChar(FileName), PAnsiChar(MapFileName), MapFileSize, JclDebugDataSize);
    44.  
    45.     WriteLn('Map file added to executable. ' + FileName);
    46.   except
    47.     on e: Exception do
    48.       WriteLn(e.Message);
    49.   end;
    50.   //Application.Run;
    51. end.
    De kern van dit tooltje is, zoals je ziet, de functie 'InsertDebugDataIntoExecutableFile'. De rest is extra. Als je wel de J(V)CL experts gebruikt, kun je natuurlijk net zo goed gewoon het vinkje aanzetten.

    Voorheen had ik een batch file in m'n project, maar tegenwoordig kan dat ook met Build Events. Op het Post Build Event van het project heb ik staan:
    Code:
    $(PROJECTDIR)\InsertJclDebugInfoInExe.exe "$(OUTPUTPATH)"
    Ik gebruik deze constructie om een ISAPI-dll beter te kunnen debuggen. Tenslotte kun je daar wat minder makkelijk doorheen steppen. De mapfile + exception loggin staan ook gewoon aan in de productieversie. Dit levert geen merkbare vertraging op.
    1+1=b

  6. #6
    Reader
    Join Date
    May 2002
    Location
    Holland
    Posts
    3,382
    Nee da's waar. Maar er zijn wel meer aannames tijdens het programmeren.

    Er zijn in ieder geval twee aannames:
    1) De interface wordt vrijgegeven als ie out of scope gaat
    2) "Enter" wordt in het begin van de procedure aangeroepen en meer niet.

    In een OnDrawItem van de een of andere component wordt ook aangenomen dat je niet een "Canvas.Free" doet.

  7. #7
    Reader
    Join Date
    May 2002
    Location
    Holland
    Posts
    3,382
    @GolezTrol: ik zal er weer eens naar kijken. Maar de laatste keer dat ik JCL daarvoor gebruikte maakte deze een totale puinhoop van de stacktrace. Volgens mij ging het mis omdat ik runtime packages gebruikte.

  8. #8
    Dat zou ik niet weten, want die gebruik ik niet. Het is in ieder geval belangrijk dat je een kloppende map-file hebt. Het zou zomaar kunnen dat die bij alleen compileren niet opnieuw gegenereerd wordt...
    1+1=b

  9. #9
    mov rax,marcov; push rax marcov's Avatar
    Join Date
    Apr 2004
    Location
    Ehv, Nl
    Posts
    10,357
    Quote Originally Posted by EricLang View Post
    Nee da's waar. Maar er zijn wel meer aannames tijdens het programmeren.

    Er zijn in ieder geval twee aannames:
    1) De interface wordt vrijgegeven als ie out of scope gaat
    Ergens tussen "ongebruikt" en "out of scope". Waar precies is ongedefinieerd.

    Met de ondrawitem opmerking kan ik niks. Dat is een fout tegen de regels van de betreffende bibliotheek, en is 1) een formele breken van die regel en 2) niet compiler gerelateerd.

    In dit geval neem jij een regel aan die doorgaans wel geldt, maar niet altijd. Dat is op eigen risico, en je bent gewaarschuwd. En daar wil ik het verder bij laten.

  10. #10
    Quote Originally Posted by EricLang View Post
    Nee da's waar. Maar er zijn wel meer aannames tijdens het programmeren.

    Er zijn in ieder geval twee aannames:
    1) De interface wordt vrijgegeven als ie out of scope gaat
    2) "Enter" wordt in het begin van de procedure aangeroepen en meer niet.

    In een OnDrawItem van de een of andere component wordt ook aangenomen dat je niet een "Canvas.Free" doet.
    Dat is wel een verschil. Het niet vrijgeven van het canvas is een regel. Hou je je daar niet aan, dan gaat je programma fout.
    De regel bij interfaces is dat deze in ieder geval blijven bestaan tot het betreffende statement is afgerond. Daarna is het niet zeker. Het zal afhangen van de compiler en mogelijk allerlei omgevingsvariabelen wanneer ze precies opgeruimd worden. Delphi kan het opruim-statement aan het eind van de procedure plakken, of direct na het ENTER statement, of misschien is er zelfs een aparte garbage collector die op de achtergrond werkt. Hoe dan ook; je weet niet hoe het precies werkt (het is niet gedocumenteerd), en het enige dat je weet is dat je nergens van uit mag gaan.

    Als jouw code dus per ongeluk in een bepaald geval niet blijkt te werken, heb je dat net zo aan jezelf te danken, als wanneer je een AV krijgt omdat je het canvas vrijgeeft. De pest is dat je op deze manier per ongeluk onjuiste informatie kunt teruggeven, en dat de gebruiker van jouw trucje zich dan het apelazerus zoekt naar de oorzaak van z'n exceptions.
    1+1=b

  11. #11
    Reader
    Join Date
    May 2002
    Location
    Holland
    Posts
    3,382
    In dit geval neem jij een regel aan die doorgaans wel geldt, maar niet altijd. Dat is op eigen risico, en je bent gewaarschuwd.
    Bedankt daarvoor.

  12. #12
    mov rax,marcov; push rax marcov's Avatar
    Join Date
    Apr 2004
    Location
    Ehv, Nl
    Posts
    10,357
    Quote Originally Posted by GolezTrol View Post
    of misschien is er zelfs een aparte garbage collector die op de achtergrond werkt.
    Extra biertje voor Goleztrol.

    Dit is een hele mooie, want als door de GC de eigenlijke destructie uitgestelt wordt, dan kan de deallocatie/destructor call misschien zelfs buiten de functie plaatsvinden.

    En het is niet theoretisch, er zijn mensen die geexperimenteerd hebben met het gebruik maken van een Boehm allocator met delphi/FPC. Echt detail gegevens heb ik echter niet.

    GC en destructors is een probleem, dat hebben we wel met Delphi.NET gezien (IDisposible vertraagt nog al).
    Last edited by marcov; 29-Jul-11 at 17:02.

  13. #13
    Reader
    Join Date
    May 2002
    Location
    Holland
    Posts
    3,382
    Ik heb het niet over een of ander framework dat onder alle omstandigheden op alle platforms zou moeten werken, maar over een simpel tooltje dat - onder voor mij normale omstandigheden - gewoon werkt.

  14. #14
    mov rax,marcov; push rax marcov's Avatar
    Join Date
    Apr 2004
    Location
    Ehv, Nl
    Posts
    10,357
    Die GC is maar een geintje. Dat is zo'n hack daar hoef je je niet voor voor te bereiden.

    Maar ik zou er geen eed op durven doen dat dit zelfs maar in een specifieke Delphi compiler werkt onder alle omstandigheden. En zelfs dan, doe jij er een eed op dat je eeuwig deze Delphi versie blijft gebruiken?

    De vergelijkingen met FPC is omdat het daar vlotter fout gaat, en als gevolg daarvan dit probleem uitgezocht is (en daarom weet ik het) niet dat Delphi dit altijd goed doet.

    FPC heeft toen besloten dergelijke code als "fout" te beschouwen, en daar kwam toen een stortvloed van "heeft altijd gewerkt" reacties op. Maar na wat rommelen kwam er IIRC ook voorbeelden naar voren waar Delphi het niet goed deed. Die werden door de tegenstanders natuurlijk als "synthetisch" afgeschilderd.

    Daar kan je over discussieren, maar het geeft wel aan, dat ook de Delphi compiler devels dit NIET als een harde regel zien.

    Een van de pakketten die hier erg last van had is InstantObjects. Ook "decal" had er last van.

    Een van de grote problemen van dit soort "features" is dat de meeste mensen bij problemen nooit de vinger op de exacte plek kunnen leggen. Er wordt gerommeld, of regeltjes over compiler opties worden opgesteld ("don't touch" mentaliteit).

  15. #15
    Reader
    Join Date
    May 2002
    Location
    Holland
    Posts
    3,382
    Ik snap het probleem. Maar interfaces moeten wel bruikbaar zijn. Als ik nu uitga van mijn 2 eerder genoemde uitgangspunten zou dan de volgende code wel "veilig" zijn?
    Ik denk dat een compiler optimization nooit zou mogen bepalen of ik "al dan niet iets doe" met de interface.

    En ik ga er hier even van uit dat het geen inline procedure betreft.

    Delphi Code:
    1. Procedure xxx;
    2. var
    3.   M: IMethod
    4. begin
    5.   M := Enter('xxx'); // uitgangspunt 2, Enter aan het begin van proc.
    6.  
    7.   // whatever code
    8.  
    9.   M := nil;
    10.  
    11. end;
    Last edited by GolezTrol; 01-Aug-11 at 09:33.

Page 1 of 2 1 2 LastLast

Thread Information

Users Browsing this Thread

There are currently 1 users browsing this thread. (0 members and 1 guests)

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •