Exception.RaiseOuterException vs. W1035 Return value of function '%s' might be undefined
Asked Answered
C

4

7

This already is reported as RSP-25603: "Exception.RaiseOuterException can cause wrong W1035 warning".


Given the following (demo) function F, I have changed an exception raising statement to now chain exceptions:

--- before
+++ after
@@ -1,11 +1,11 @@
 function F(X: NativeInt): NativeInt;
 begin
   try
     Result := 1 div X;
   except
     on EDivByZero do
-      {ECustom}Exception.Create('...');
+      Exception.RaiseOuterException({ECustom}Exception.Create('...'));
     else
       raise;
   end;
 end;

Now, Ctrl-F9 gives the warning W1035:

[dcc32 Warning]: W1035 Return value of function 'F' might be undefined

However, all cases are handled. The compiler fails to recognize Exception.RaiseOuterException as the raise operation it is.

Unfortunately FAcquireInnerException: Boolean is private to the Exception class, not even to be set to True in derived custom classes which I could keep raising directly (raise ECustomException.Create).

Is there any way to make the compiler understand, while keeping the exceptions chained? Otherwise I can think of {$Warn No_RetVal Off}. How else could I work around this warning?

Concessionaire answered 14/11, 2020 at 14:18 Comment(5)
I often have a local subprocedure like procedure Inv; begin raise EFrogProperties.Create('Invalid frog properties.'); end; and would also love some noreturn keyword so the compiler knows that it will never return to the caller.Wrennie
I've ever liked the design of RaiseOuterException(). Why they didn't simply introduce a new constructor that captures an existing Exception or acquires the current Exception, or at least a CreateOuterException() method that returns a new Exception that can be raised separately, is behind me.Almsgiver
@RemyLebeau: Just being curious: what would you say about a noreturn keyword or attribute? (Maybe not to solve this particular issue, but in general.)Wrennie
I have created an EChainedException class that when raised always captures the inner exception (like Exception.RaiseOuterExcepotion). As you can use raise directly, the warning would be gone too, but still have the exceotions chainedZap
@Zap Looking at the implementation, private FAcquireInnerException: Boolean = False renders the whole mechanic in Exception inaccessible; i.e. protected procedure SetInnerException becomes a no-op to derieved classes, for example. Do you reimplement the mechanics? That is quite some things to keep track of to stay on par with Exception. Or do you break through FAcquireInnerException's visibility? Would you share the code of your EChainedException?Concessionaire
A
3

One way I can think of to avoid the warning, without disabling it, is to do the following instead:

function F(X: NativeInt): NativeInt;
begin
  try
    Result := 1 div X;
  except
    on E: Exception do
    begin
      if E is EDivByZero then
        Exception.RaiseOuterException({ECustom}Exception.Create('...'));
      raise;
    end;
  end;
end;

UPDATE: Another way, as stated in a comment, would be to simply define a return value that is not actually reached at runtime, eg:

function F(X: NativeInt): NativeInt;
begin
  try
    Result := 1 div X;
  except
    on E: EDivByZero do
    begin
      Exception.RaiseOuterException({ECustom}Exception.Create('...'));
      Result := 0; // <-- just to keep the compiler happy
    end;
  end;
end;
Almsgiver answered 14/11, 2020 at 17:46 Comment(1)
Of course, there are many variants. For instance, you could also get rid of the warning by assigning something to Result. You may do that after (!) the RaiseOuterException. In the OP's original code: Replace Exception.RaiseOuterException({ECustom}Exception.Create('...')); with begin Exception.RaiseOuterException({ECustom}Exception.Create('...')); Exit(0) end;Wrennie
Z
2

EChainedException solution

(as requested by Max)

Update I have put out a FR for this at Embarcadero. Please vote if you like this proposed solution. RSP-31679

By using this class, the inner exception is always recorded "as if" you had called Exception.RaiseOuterException. This allows you to use the simple raise statement, this avoids the warning message being issued by the compiler.

Useage

Just derive your custom exceptions from EChainedException instead of Exception, and use raise rather then Exception.RaiseOuterException.

Sourcecode

The relevant code is below. My complete EChainedException is a bit more complicated than this for supporting detection of fatal exceptions and stacktracing etc. If it doesn't compile, let me know what's missing and I'll add the missing part.

unit uChainedException;

interface

uses Sysutils;

{$M+} // ensures RTTI info is present for EChainedException

type
    EChainedException = class(Exception)
    protected
      procedure RaisingException(P: system.sysutils.PExceptionRecord); override;
    end;

implementation

uses rtti;

var // rtti pointers for handling the inner exception
  vInnerExceptionOffset: NativeInt = -1;
  vAcquireInnerExceptionOffset: NativeInt = -1;
  vRunningInIDEInitialized: Boolean;
  vRunningInIDE: Boolean;

function RunningInIDE:boolean;
begin
  if not vRunningInIDEInitialized then
  begin
    vRunningInIDE:=AnsiSameText(ExtractFileName(ParamStr(0)),'BDS.EXE');
    vRunningInIDEInitialized:=True;
  end;
  Result:=vRunningInIDE;
end;


procedure EChainedException.RaisingException(P: System.sysutils.PExceptionRecord);
var
  PBoolean: ^Boolean;
  PObject : ^TObject;
begin
  if (ExceptObject<>self) and (vAcquireInnerExceptionOffset >=0)  then
  begin
    PBoolean := Pointer(NativeInt(Self)+vAcquireInnerExceptionOffset);
    PBoolean^ := PBoolean^ or not RunningInIDE;
  end;
  inherited;
  // in some rare cases (like reraise exception from another thread)
  // it may happen that the innerexception points to self
  // this is corrected here.
  if InnerException=self then
  begin
    PObject := Pointer(NativeInt(Self)+vInnerExceptionOffset);
    PObject^ := nil;
  end;
end;

procedure UnprepAutoInnerException;
begin
  vInnerExceptionOffset:=-1;
  vAcquireInnerExceptionOffset:=-1;
end;

procedure PrepAutoInnerException;
var
  lRTTIContext: TRttiContext;
  lInnerException:TRttiField;
  lAcquireInnerException:TRttiField;
  lClass: TRttiInstanceType;
begin
  try
    lRTTIContext.Create;     //Notice vRTTIContext is a record, .Create initializes properties
    try
      lClass:=lRTTIContext.GetType(Exception) as TRttiInstanceType;
      lInnerException:=lClass.GetField('FInnerException');
      vInnerExceptionOffset := lInnerException.Offset;
      lAcquireInnerException:=lClass.GetField('FAcquireInnerException');
      vAcquireInnerExceptionOffset := lAcquireInnerException.Offset;
    except
      UnprepAutoInnerException;
      raise;
    end;
  finally
    lRTTIContext.Free;
  end;
end;

initialization
  PrepAutoInnerException;
finalization
  UnprepAutoInnerException;
end.

Looking at this code I find it could use some modernizing, eg by using class vars instead of globals, and by using inline locale variables. The entire unit is back from Delphi 6 days and contains many $ifdefs, and left out because it would surpass the answer.

I still wonder why exception chaining is not the default in delphi/rad studio like it is in other languages. Most likely because it would break existing code somehow.

Zap answered 16/11, 2020 at 13:43 Comment(1)
Thank you very much for your code! +1 It wasn't difficult to figure out the missing parts, so I went ahead and added them myself to your answer, so it would compile. Your answer steered me towards yet another solution. See my answer. I hope there's something in there for you, too!!Concessionaire
H
1

At page https://www.gunsmoker.ru/2010/04/exception-delphi-2009.html is suggested this method do switch on a chaining for all exceptions:

unit ChainedExceptionsAlways;
 
interface
 
implementation
 
uses
  SysUtils;
 
var
  OldRaiseExceptObject: Pointer;
 
type
  EExceptionHack = class
  public
    FMessage: string;
    FHelpContext: Integer;
    FInnerException: Exception;
    FStackInfo: Pointer;
    FAcquireInnerException: Boolean;
  end;
 
procedure RaiseExceptObject(P: PExceptionRecord);
type
  TRaiseExceptObjectProc = procedure(P: PExceptionRecord);
begin
  if TObject(P^.ExceptObject) is Exception then
    EExceptionHack(P^.ExceptObject).FAcquireInnerException := True;
 
  if Assigned(OldRaiseExceptObject) then
    TRaiseExceptObjectProc(OldRaiseExceptObject)(P);
end;
 
initialization
  OldRaiseExceptObject := RaiseExceptObjProc;
  RaiseExceptObjProc := @RaiseExceptObject;
end.

But in comments there have a mention to unexpected behaviour of chaining in case of stacked try/finally blocks. Stored inner exception pointer can point to already unmanaged memory of handled and released exception object.

Humdinger answered 27/2 at 10:26 Comment(0)
C
0

I (also) answer my own question as I will take yet another approach. It provides for the following requirements:

  • I like to keep the raise statements, as they initially were,
    • so there won't be any necessary code changes here, and
    • which also means there won't be newly introduced warnings like W1035 or W1036.
  • I don't want to rebuild the inner RTL mechanics, however
  • I want to interfere with the RTL mechanics as little as possible.
  • I want to be flexible in controlling for chaining exceptions
    • sometimes forced or by default, on the exception implementation side, as well as
    • sometimes by argument, on the exception usage side, to extend functionality.

In my solution:

  • I accept to break through the Exception fields' visibility, FAcquireInnerException specifically.
  • I rely on RTTI to verify the fields' alignment (in ExceptionFields, according to Exception).

Here I provide a condensed implementation to copy-and-paste:

EException's constructor showcases the use of ExceptionFields:

ExceptionFields(Self).FAcquireInnerException := True;

-- to be used in any Exception-derived exception, and it will trigger the RTL mechanics to set the InnerException while it is raising the exception. Also, EException may serve as a common root for custom exception classes, if desired. Some constructors are reintroduced to be extended with const AcquireInnerException: Boolean = True, to hand-over the control to the caller while providing a default for the desired chaining.

Run ExceptionFields.VerifyFieldAlignments, if you want to verify the alignments of

  • the ("re-") declared externally accessible fields in ExceptionFields and
  • their (private) counterparts in Exception.

If it cannot verify this, it will raise an exception. It is run in EException's class constructor. Move it as propriate to you, if you do not use EException, yet want to keep the verification.

(Condensed) implementation:

unit Exceptions;

interface

uses
  System.SysUtils;

type
  EException = class (Exception)
  public
    class constructor Create;
    constructor Create(const Msg: String; const AcquireInnerException: Boolean = True);
    constructor CreateFmt(const Msg: String; const Args: array of const; const AcquireInnerException: Boolean = True); overload;
    constructor CreateRes(const Msg: PResStringRec; const AcquireInnerException: Boolean = True);
    constructor CreateResFmt(const Msg: PResStringRec; const Args: array of const; const AcquireInnerException: Boolean = True); overload;
  end;

type
  ExceptionFields = class (TObject)
  {$Hints Off} // H2219
  strict private
    FMessage: String;
    FHelpContext: Integer;
    FInnerException: Exception;
    FStackInfo: Pointer;
  {$Hints On}

  public
    FAcquireInnerException: Boolean;

  private
    class procedure VerifyFieldAlignments;
  end;

implementation

uses
  System.Generics.Collections,
  System.RTTI,
  System.TypInfo;

{ ExceptionFields }

class procedure ExceptionFields.VerifyFieldAlignments;

  procedure RaiseTypeNotFound(const ClassName: String);
  begin
    raise Exception.CreateFmt(
      'Typ nicht gefunden: %s',
      [ClassName]
    );
  end;

  procedure RaiseFieldNotFound(const ClassName, FieldName: String);
  begin
    raise Exception.CreateFmt(
      'Feld nicht gefunden: %s.%s',
      [ClassName, FieldName]
    );
  end;

  procedure RaiseFieldNotAligned(const LeftClassName: String; const LeftField: TPair<String, Integer>; const RightClassName: String; const RightField: TRTTIField);
  begin
    raise Exception.CreateFmt(
      'Feld nicht ausgerichtet: %s.%s+%d (tatsächlich) vs. %s.%s+%d (erwartet)',
      [
        LeftClassName,
        LeftField.Key,
        LeftField.Value,
        RightClassName,
        RightField.Name,
        RightField.Offset
      ]
    );
  end;

  type
    TMemberVisibilities = set of TMemberVisibility;

  function GetDeclaredFields(const RTTIContext: TRTTIContext; const &Class: TClass; const IncludedVisibilities: TMemberVisibilities = [mvPublic, mvPublished]): TArray<TPair<String, Integer>>;
  var
    RTTIType: TRTTIType;
    RTTIFields: TArray<TRTTIField>;
    Index: NativeInt;
    RTTIField: TRTTIField;
  begin
    RTTIType := RTTIContext.GetType(&Class);
    if not Assigned(RTTIType) then
      RaiseTypeNotFound(&Class.ClassName);
    RTTIFields := RTTIType.GetDeclaredFields;
    SetLength(Result, Length(RTTIFields));
    Index := 0;
    for RTTIField in RTTIFields do
      if RTTIField.Visibility in IncludedVisibilities then
      begin
        Result[Index] := TPair<String, Integer>.Create(
          RTTIField.Name,
          RTTIField.Offset
        );
        Inc(Index);
      end;
    SetLength(Result, Index);
  end;

const
  Left: TClass = ExceptionFields;
  Right: TClass = Exception;
var
  RTTIContext: TRTTIContext;
  DeclaredFields: TArray<TPair<String, Integer>>;
  RTTIType: TRTTIType;
  DeclaredField: TPair<String, Integer>;
  RTTIField: TRTTIField;
begin
  RTTIContext := TRTTIContext.Create;
  try
    DeclaredFields := GetDeclaredFields(RTTIContext, Left);
    RTTIType := RTTIContext.GetType(Right);
    if not Assigned(RTTIType) then
      RaiseTypeNotFound(Right.ClassName);
    for DeclaredField in DeclaredFields do
    begin
      RTTIField := RTTIType.GetField(DeclaredField.Key);
      if not Assigned(RTTIField) then
        RaiseFieldNotFound(Right.ClassName, DeclaredField.Key);
      if DeclaredField.Value <> RTTIField.Offset then
        RaiseFieldNotAligned(
          Left.ClassName, DeclaredField,
          RTTIType.Name, RTTIField
        );
    end;
  finally
    RTTIContext.Free;
  end;
end;

{ EException }

class constructor EException.Create;
begin
  inherited;
  ExceptionFields.VerifyFieldAlignments;
end;

constructor EException.Create(const Msg: String;
  const AcquireInnerException: Boolean);
begin
  inherited Create(Msg);
  ExceptionFields(Self).FAcquireInnerException := AcquireInnerException;
end;

constructor EException.CreateFmt(const Msg: String;
  const Args: array of const;
  const AcquireInnerException: Boolean);
begin
  inherited CreateFmt(Msg, Args);
  ExceptionFields(Self).FAcquireInnerException := AcquireInnerException;
end;

constructor EException.CreateRes(const Msg: PResStringRec;
  const AcquireInnerException: Boolean);
begin
  inherited CreateRes(Msg);
  ExceptionFields(Self).FAcquireInnerException := AcquireInnerException;
end;

constructor EException.CreateResFmt(const Msg: PResStringRec;
  const Args: array of const;
  const AcquireInnerException: Boolean);
begin
  inherited CreateResFmt(Msg, Args);
  ExceptionFields(Self).FAcquireInnerException := AcquireInnerException;
end;

end.

And a demo:

program ExceptionsDemo;

{$AppType Console}

{$R *.res}

uses
  System.SysUtils,
  Exceptions in 'Exceptions.pas';

type
  EDemoException = class (EException)
  end;

begin
  try
    try
      try
        raise EZeroDivide.Create('Level 3');
      except
        raise EException.Create('Level 2', False);
      end;
    except
      raise EDemoException.Create('Level 1');
    end;
  except
    on E: Exception do
    begin
      WriteLn(E.ClassName, ': ', E.Message);
      while Assigned(E.InnerException) do
      begin
        E := E.InnerException;
        WriteLn(E.ClassName, ': ', E.Message);
      end;
    end;
  end;
  ReadLn;
end.

Output -- the last line is only there on raise EException.Create('Level 2', True):

EDemoException: Level 1
EException: Level 2
EZeroDivide: Level 3

Thank you to all repliers!

Concessionaire answered 18/11, 2020 at 14:51 Comment(2)
This is an interesting solution to get around the RTTI solution. I went mostly for the lightweight using pointers (and not TrttiField.SetValue) because of performance. Also, like you I'll know immediately if Emb decides to change.rename FInnerException since RTTI wont be able to find them. Come to think of it: Did you consider the use of a helper class to do the hacky stuff?Zap
Anyway, be sure to check out quality.embarcadero.com/browse/RSP-31679Zap

© 2022 - 2024 — McMap. All rights reserved.