Sunday, March 17, 2013

Ada access types, part IV


Thus far we've seen just about all of the "normal" uses of Ada95 general access types, which all boil down to basically two things: using subtypes to placate the compiler into ensuring local pointers cannot be copied out into the world, and using so-called 'anonymous' access discriminants and parameters to safely copy them out if required.

We've also seen two special circumstances where the static rules are insufficient for ensuring lifetime control and thus require runtime checks inserted by the compiler to keep you honest.  The first was a function that returned an record containing access discriminants, since nothing in the syntax prevented you from returning a pointer to a local object.  The second was an explicit typecast of an access parameter which, while valid, is dangerous and not recommended.

However, there is one final circumstance (in Ada95) that requires a runtime check, and while you are unlikely to stumble across this situation through normal everyday programming, it's important in all situations to know when your program can never fail, and when it might.  This final circumstance involves generics.

Suppose we have two packages, where one passes the address of a 'global' object to the other, which saves it into a global pointer:

package P1 is

   type Int_Ptr is access all Integer;
   Global_Pointer : Int_Ptr;
  
   procedure Save (Arg : Int_Ptr) is
   begin
      Global_Pointer := Arg;
   end Save;

end P1;

package P2 is

   Global_Object : aliased Integer := 42;
  
   procedure Store is
   begin
     P1.Save (Arg => Global_Object'access); -- pass
   end Store;


end P2;

By all accounts, this is safe code.  The static check compares the lifetime of the object in question (Global_Object) with that of the access type being created (Int_Ptr), and finds they are both at the top-most library level.  Obviously the object is going to be there until P2 is finalized, which is essentially the entire life of the program, so we can copy pointers to it wherever we please.

Now suppose we make P2 generic for some reason (the reason does not matter):

generic
   type T is (<>);
package P2 is

   Global_Object : aliased Integer := 42;
  
   procedure Store is
   begin
     P1.Save (Arg => Global_Object'access);
   end Store;

end P2;

This complicates matters greatly, since we don't know where the package is going to be instantiated.  If we instantiate P2 as its own, stand-alone library package then the code is as before, Global_Object is a library-level object, and everything is hunky-dory.  However, there is nothing stopping us from doing, say, this:

procedure P is

   type Some_Type is range 0 .. 100;
   package Dangly is new p2 (T => Some_Type );
 
begin

  Dangly.Store;
end P;


Now the package is actually nested inside a procedure, so everything is one level deeper, including our previously "global" object.  When P ends, our package ceases to exist along with the our now-no-longer-global object, and P1 contains a dangling pointer.

Checking for this is complicated further by the requirement of separate compilation for generic bodies.  Some compilers implement the so-called "macro expansion" model, in which each instantiation basically recompiles the entire generic soup-to-nuts.  An alternative model, 'shared generics', compiles the body once and then reuses the same code.  In a shared generic model, there would no way to know during the compilation of the body where the instantiation would be, and therefore no way to know what the accessibility level of the object would be during the check.  Indeed, we might instantiate this generic any number of places at any number of levels, so some might be safe while others might fail.

The only way out of this problem is to insert a runtime check that verifies the actual level of our 'global' object is not deeper than the pointer being created.  If this check fails, like other checks, Program_Error is raised.  Note that any worthy "macro expansion" compiler should note the inevitable accessibility violation (since it knows where the generic is instantiated) and report a warning (to its credit, GNAT 2012 correctly generates both the warning and the exception).

And this about does it for the wild and wacky ways that Ada95 deals with general access types.  One static check, access parameters, access discriminants, and three runtime checks (as needed).  Correct and judicious use of them can only make your programs safer and better.

No comments:

Post a Comment