!standard 11.4.1(00) 02-08-28 AC95-00044/01 !class amendment 02-07-13 !status received no action 02-07-13 !subject Addition of Current_Exception function to Ada.Exceptions package !summary !appendix !topic Addition of Current_Exception function to Ada.Exceptions package !reference RM95-11.4.1 RM95-7.6.1 !from Chris Miller 02-07-10 !keywords Current_Exception, Finalization !discussion !problem There is no way within a finalization routine to find if (a) the finalization is being called due to the raising of an exception and, if so, (b) the details of the current exception occurrence. !proposal It is proposed that a new "current_exception" function be added to the Ada.Exceptions package. Also, the existing Ada.Exceptions.Null_Occurrence constant should be made aliased. The function would have the following specification package Ada.Exceptions is ... function Current_Exception return Exception_Occurrence_Access; Null_Occurrence : aliased constant Exception_Occurrence; ... end Ada.Exceptions; The new function would return either a (pointer to) the current exception occurrence or a (pointer to) the Null_Occurrence constant if there is none. !example The following example (see below) shows how this function could be used in a finalize procedure. It has been compiled and run under GNAT 3.14p on a PC. Here are some notes on the example :- 1. The new function and aliased constant have been added to a package called "Ada1".Exceptions. This is just to avoid some GNAT warnings. In practice it would be included into the existing Ada.Exceptions package. 2. The program demonstrates a cut down version of a common embedded application. There is a schedular program (schedular.ads / .adb) that loops "forever". The program implements events by calling an application procedure P1.proc1. In this example there is only a single event. P1.Proc1 is a hierarchy, it calls P2.Proc2 which in turn calls P3.Proc3. P2.Proc2 contains a controlled variable derived from the Sampled_Controlled package. A finalize procedure has been specified for this. P3.Proc3 raises various exceptions each time it is called. It also simulates part of the operation of the current_exception function by saving the latest exception_occurrence in a global location. In the example this is in a global variable. In practice it would likely be in the task control block of the current task but in any case it would be hidden from direct access and so the implementation details are not relevant. 3. The semantics of the program are such that it should never terminate due to the raising of a normal exception in an application event. Rather the exception should be logged and the next event should be delivered, and so on. However assume that there is a user defined exception called "fatal_error" (in exception_names.ads) which, when raised by application code, causes the schedular to terminate in an orderly manner and (for example) switch processing to a warm standby alternate CPU. 4. Within the finalize routine called in P2.Proc2 there is a need to know what has caused it to be invoked. If no exception has been raised, or if any exception except fatal_error has been raised, then we wish to clean up in the normal manner. This is to ensure that the system can continue to operate. However if fatal_error has been raised then the image is going to be terminated anyway and so cleaning up is a waste of time. Instead we wish to return control to the scheduler as soon as possible. Calling an operating system "exit" routine is not desired as this terminates the image immediately. Rather we require that control should pass back to the scheduler so that it can terminate the image in a controlled manner. 5. The sample code (in sample_controlled.adb) shows how the proposed current_exception function is used to determine what, if any, exception has been raised. Different processing options are then possible. 6. In the case of a fatal_error exception being raised, control returns to the scheduler, it exits the Main_loop and arranges for a graceful transition to a backup CPU. Sample code follows ------ with Scheduler; procedure Main is begin Scheduler.Start; end Main; ------ package Scheduler is procedure Start; end Scheduler; ------ with P1; with Exception_Names; with Ada.Exceptions; with Ada1.Exceptions; with Ada.Text_IO; package body Scheduler is procedure Start is begin Main_loop: -- "Forever" while true loop begin -- Just to make the demo work. Exception_Names.Latest_Exception := Ada1.Exceptions. Aliased_Null_Occurrence; -- Deliver an "event". P1.Proc1; Ada.Text_Io.Put_Line("Scheduler : Event delivered OK."); exception when Exception_Names.Fatal_Error => Ada.Text_Io.Put_Line("Scheduler : Fatal_Error raised, shutting down ..."); exit Main_Loop; when E: others => -- Perform some logging and / or cleanup, but continue processing. Ada.Text_Io.Put_Line("Scheduler : Exception " & Ada.Exceptions.Exception_Name(E) & " raised. Continuing ..."); end; Ada.Text_Io.New_Line; end loop Main_Loop; -- Perform required shutdown code, e.g. switch to an alternate CPU. -- Had exit been called in the Finalize procedure then this would -- not be possible. Ada.Text_IO.Put_Line("Scheduler : switching to backup CPU ..."); end Start; end Scheduler; ------ with Ada.Exceptions; package Exception_Names is Fatal_Error, Non_Fatal_Error : exception; -- Simulate the data required for the proposed Current_Exception -- function by storing the latest exception occurrence (or a pointer to -- it) in a global location. Note, would not be done this way in practice. Latest_Exception : Ada.Exceptions.Exception_Occurrence_Access; end Exception_Names; ------ package Ada1 is -- Dummy parent package for Ada1 tree. pragma Pure; end Ada1; ------ with Ada.Exceptions; Package Ada1.Exceptions is -- Proposed new function. In practice this would be placed into -- the *existing* Ada.Exceptions package. Here it has been put -- into a separate "Ada1".Exceptions, just to get it to compile -- without warnings. function Current_Exception return Ada.Exceptions. Exception_Occurrence_Access; -- Existing Null_Occurrence constant is not aliased. Propose that it be -- made so. For the demo, just define another constant that is aliased -- so that can have pointers to it. Aliased_Null_Occurrence : aliased constant Ada.Exceptions.Exception_Occurrence_Access := Ada.Exceptions.Save_Occurrence(Source => Ada.Exceptions.Null_Occurrence); end Ada1.Exceptions; ------ with Exception_Names; package body Ada1.Exceptions is -- This is the new proposed function. function Current_Exception return Ada.Exceptions.Exception_Occurrence_Access is begin -- This data would normally be hidden from the user. However -- for this demo program we have just used a global variable. return Exception_Names.Latest_Exception; end Current_Exception; end Ada1.Exceptions; ------ package P1 is Procedure Proc1; end P1; ------ with P2; package body P1 is procedure Proc1 is begin P2.Proc2; end Proc1; end P1; ------ package P2 is Procedure Proc2; end P2; ------ with P3; with Sample_Controlled; package body P2 is procedure Proc2 is C : Sample_Controlled.My_Controlled; begin P3.Proc3; -- Finalize called when C goes out of scope. end Proc2; end P2; ------ package P3 is Procedure Proc3; end P3; ------ with Ada.Exceptions; with Exception_Names; with Ada.Text_Io; package body P3 is Count : Integer := 0; procedure Proc3 is begin Count := Count + 1; -- Simulate some possible error conditions. if count = 1 then Ada.Text_IO.Put_Line("Proc 3. Doing normal processing"); elsif count = 2 then Ada.Text_IO.Put_Line("Proc 3. Raising Constraint_Error"); raise Constraint_Error; elsif count = 3 then Ada.Text_IO.Put_Line("Proc 3. Raising Non_Fatal_Error"); raise Exception_Names.Non_Fatal_Error; elsif count = 4 then Ada.Text_IO.Put_Line("Proc 3. Raising Fatal_Error"); raise Exception_Names.Fatal_Error; end if; exception -- *Simulate* the proposed current_exception function by saving the -- latest exception occurrence in a global location (In practice this -- data would of course not be visible to application code, most likely -- would store it in the task control block of the current task). -- Then re raise the same exception. when E : others => Exception_Names.Latest_Exception := Ada.Exceptions.Save_Occurrence(E); Ada.Exceptions.Reraise_Occurrence(E); end Proc3; end P3; ------ with Ada.Finalization; Package Sample_Controlled is type My_Controlled is new Ada.Finalization.Controlled with private; private procedure Finalize (Object : in out My_Controlled); type My_Controlled is new Ada.Finalization.Controlled with null record; end Sample_Controlled; ------ with Ada.Exceptions; with Ada1.Exceptions; with Exception_Names; with Ada.Text_Io; package body Sample_Controlled is procedure Finalize (Object : in out My_Controlled) is A : Ada.Exceptions.Exception_Occurrence_Access; use Ada.Exceptions; begin -- Finalise routine for the controlled object. We assume that if any exception, -- excluding Exception_Names.Fatal_Error, is raised then we need to do some -- cleanup for the controlled object. This may take some time. -- However if the Fatal_Error exception is raised then we wish to skip the -- cleanup and return as soon as possible to the scheduler. For example, the -- scheduler may wish to return control to the operating system and / or -- switch control to an alternate processor. Wish to have this happen as -- soon as possible. In this case doing finalization is of no use, since the -- image is going to be terminated anyway. -- -- To distinguish between these two cases we use the proposed current_exception -- function. This returns the exception occurrence (or a pointer to it) of the -- exception being handled. If no exception is being raised then a (pointer to) -- Null_Occurrence is returned. As the existing Null_Occurrence constant is not -- aliased we have had to fudge this by taking a copy of it. -- -- Calling an Operating System exit operation is not what is required. Instead -- we wish to return control to the scheduler and let it shut down the system -- in a controlled manner. If exit is called then this is not possible. A := Ada1.Exceptions.Current_Exception; if A /= Ada1.Exceptions.Aliased_Null_Occurrence then if Ada.Exceptions.Exception_Identity(A.all) = Exception_Names.Fatal_Error'Identity then Ada.Text_Io.Put_Line ("Finalize : Fatal_Error, returning to exec ASAP"); -- And so the exception just propagates. (Don't reraise it or you get -- Program_Error, ARM 7.6.1(14)). else -- Perform normal finalization for Object, may take some time. Ada.Text_Io.Put_Line("Finalize : Doing normal cleanup"); -- ... etc. end if; end if; end Finalize; end Sample_Controlled; ------ Output from program Proc 3. Doing normal processing Scheduler : Event delivered OK. Proc 3. Raising Constraint_Error Finalize : Doing normal cleanup Scheduler : Exception CONSTRAINT_ERROR raised. Continuing ... Proc 3. Raising Non_Fatal_Error Finalize : Doing normal cleanup Scheduler : Exception EXCEPTION_NAMES.NON_FATAL_ERROR raised. Continuing ... Proc 3. Raising Fatal_Error Finalize : Fatal_Error, returning to exec ASAP Scheduler : Fatal_Error raised, shutting down ... Scheduler : switching to backup CPU ... !discussion Consider a typical embedded application where there is a cyclic scheduler that continually delivers events to application code. An event is implemented as a procedure in a package. During the event, one or more subprograms are called, control then returns to the scheduler and it then selects and delivers the next event. This goes on "forever". Assume that during the processing of an event application code declares controlled variable(s). When these go out of scope their finalize procedures are called in the usual way. Within these finalise procedures it is desired to know if the routine is being called as a result of an exception being raised (and if so which one) or as a result of a normal_completion (ARM 7.6.1 (2)). The application may wish to change the processing done depending upon which case has caused the finalize to be called. For example, if there is some fatal error then there may be a need to shut down the application as soon as possible and transfer processing to another CPU. Hence doing finalization here may be a waste of time as the image is going to be shut down anyway. The general principle is that an application should be able to determine the circumstances that have caused the finalization procedure to be called and so adjust processing accordingly. The proposed current_exception function could be used to do this. Calling it would return (a pointer to) the current exception_occurrence. From this details about the exception can be determined as required. In the case of a normal_completion a (pointer to) the existing Null_Occurrence constant would be returned. For the case of abnormal completion due to a task abort (ARM 7.6.1 (2)) the current_exception call would also return Null_Occurrence. There may be other possibilities here. May even wish to have a separate means of determining if finalize was called due to an abort but this is probably another topic that is best left to the language lawyers. If there are any other cases (??) where it is not possible to access a valid current occurrence then Null_Occurrence could also be returned. These would need to be documented. The current_exception routine would be a new function and would have no impact on existing code. Making the existing Null_Occurrence constant aliased should also have no impact. In many ways the function is analogous to the existing current_task function. !summary There should be a new "current_exception" function added to the Ada.Exceptions package. Also, the existing Ada.Exceptions.Null_Occurrence constant should be made aliased. ****************************************************************