Next: Mixing Elaboration Models, Previous: Treatment of Pragma Elaborate, Up: Elaboration Order Handling in GNAT
In this section we examine special elaboration issues that arise for programs that declare library level tasks.
Generally the model of execution of an Ada program is that all units are elaborated, and then execution of the program starts. However, the declaration of library tasks definitely does not fit this model. The reason for this is that library tasks start as soon as they are declared (more precisely, as soon as the statement part of the enclosing package body is reached), that is to say before elaboration of the program is complete. This means that if such a task calls a subprogram, or an entry in another task, the callee may or may not be elaborated yet, and in the standard Reference Manual model of dynamic elaboration checks, you can even get timing dependent Program_Error exceptions, since there can be a race between the elaboration code and the task code.
The static model of elaboration in GNAT seeks to avoid all such dynamic behavior, by being conservative, and the conservative approach in this particular case is to assume that all the code in a task body is potentially executed at elaboration time if a task is declared at the library level.
This can definitely result in unexpected circularities. Consider the following example
package Decls is task Lib_Task is entry Start; end Lib_Task; type My_Int is new Integer; function Ident (M : My_Int) return My_Int; end Decls; with Utils; package body Decls is task body Lib_Task is begin accept Start; Utils.Put_Val (2); end Lib_Task; function Ident (M : My_Int) return My_Int is begin return M; end Ident; end Decls; with Decls; package Utils is procedure Put_Val (Arg : Decls.My_Int); end Utils; with Text_IO; package body Utils is procedure Put_Val (Arg : Decls.My_Int) is begin Text_IO.Put_Line (Decls.My_Int'Image (Decls.Ident (Arg))); end Put_Val; end Utils; with Decls; procedure Main is begin Decls.Lib_Task.Start; end;
If the above example is compiled in the default static elaboration
mode, then a circularity occurs. The circularity comes from the call
Utils.Put_Val
in the task body of Decls.Lib_Task
. Since
this call occurs in elaboration code, we need an implicit pragma
Elaborate_All
for Utils
. This means that not only must
the spec and body of Utils
be elaborated before the body
of Decls
, but also the spec and body of any unit that is
with'ed
by the body of Utils
must also be elaborated before
the body of Decls
. This is the transitive implication of
pragma Elaborate_All
and it makes sense, because in general
the body of Put_Val
might have a call to something in a
with'ed
unit.
In this case, the body of Utils (actually its spec) with's
Decls
. Unfortunately this means that the body of Decls
must be elaborated before itself, in case there is a call from the
body of Utils
.
Here is the exact chain of events we are worrying about:
Decls
a call is made from within the body of a library
task to a subprogram in the package Utils
. Since this call may
occur at elaboration time (given that the task is activated at elaboration
time), we have to assume the worst, i.e., that the
call does happen at elaboration time.
Util
must be elaborated before
the body of Decls
so that this call does not cause an access before
elaboration.
Util
, specifically within the body of
Util.Put_Val
there may be calls to any unit with
'ed
by this package.
with
'ed package is package Decls
, so there
might be a call to a subprogram in Decls
in Put_Val
.
In fact there is such a call in this example, but we would have to
assume that there was such a call even if it were not there, since
we are not supposed to write the body of Decls
knowing what
is in the body of Utils
; certainly in the case of the
static elaboration model, the compiler does not know what is in
other bodies and must assume the worst.
Decls
must also be
elaborated before we elaborate the unit containing the call, but
that unit is Decls
! This means that the body of Decls
must be elaborated before itself, and that's a circularity.
Indeed, if you add an explicit pragma Elaborate_All
for Utils
in
the body of Decls
you will get a true Ada Reference Manual
circularity that makes the program illegal.
In practice, we have found that problems with the static model of elaboration in existing code often arise from library tasks, so we must address this particular situation.
Note that if we compile and run the program above, using the dynamic model of elaboration (that is to say use the -gnatE switch), then it compiles, binds, links, and runs, printing the expected result of 2. Therefore in some sense the circularity here is only apparent, and we need to capture the properties of this program that distinguish it from other library-level tasks that have real elaboration problems.
We have four possible answers to this question:
If we use the -gnatE switch, then as noted above, the program works.
Why is this? If we examine the task body, it is apparent that the task cannot
proceed past the
accept
statement until after elaboration has been completed, because
the corresponding entry call comes from the main program, not earlier.
This is why the dynamic model works here. But that's really giving
up on a precise analysis, and we prefer to take this approach only if we cannot
solve the
problem in any other manner. So let us examine two ways to reorganize
the program to avoid the potential elaboration problem.
Write separate packages, so that library tasks are isolated from other declarations as much as possible. Let us look at a variation on the above program.
package Decls1 is task Lib_Task is entry Start; end Lib_Task; end Decls1; with Utils; package body Decls1 is task body Lib_Task is begin accept Start; Utils.Put_Val (2); end Lib_Task; end Decls1; package Decls2 is type My_Int is new Integer; function Ident (M : My_Int) return My_Int; end Decls2; with Utils; package body Decls2 is function Ident (M : My_Int) return My_Int is begin return M; end Ident; end Decls2; with Decls2; package Utils is procedure Put_Val (Arg : Decls2.My_Int); end Utils; with Text_IO; package body Utils is procedure Put_Val (Arg : Decls2.My_Int) is begin Text_IO.Put_Line (Decls2.My_Int'Image (Decls2.Ident (Arg))); end Put_Val; end Utils; with Decls1; procedure Main is begin Decls1.Lib_Task.Start; end;
All we have done is to split Decls
into two packages, one
containing the library task, and one containing everything else. Now
there is no cycle, and the program compiles, binds, links and executes
using the default static model of elaboration.
A significant part of the problem arises because of the use of the single task declaration form. This means that the elaboration of the task type, and the elaboration of the task itself (i.e. the creation of the task) happen at the same time. A good rule of style in Ada is to always create explicit task types. By following the additional step of placing task objects in separate packages from the task type declaration, many elaboration problems are avoided. Here is another modified example of the example program:
package Decls is task type Lib_Task_Type is entry Start; end Lib_Task_Type; type My_Int is new Integer; function Ident (M : My_Int) return My_Int; end Decls; with Utils; package body Decls is task body Lib_Task_Type is begin accept Start; Utils.Put_Val (2); end Lib_Task_Type; function Ident (M : My_Int) return My_Int is begin return M; end Ident; end Decls; with Decls; package Utils is procedure Put_Val (Arg : Decls.My_Int); end Utils; with Text_IO; package body Utils is procedure Put_Val (Arg : Decls.My_Int) is begin Text_IO.Put_Line (Decls.My_Int'Image (Decls.Ident (Arg))); end Put_Val; end Utils; with Decls; package Declst is Lib_Task : Decls.Lib_Task_Type; end Declst; with Declst; procedure Main is begin Declst.Lib_Task.Start; end;
What we have done here is to replace the task
declaration in
package Decls
with a task type
declaration. Then we
introduce a separate package Declst
to contain the actual
task object. This separates the elaboration issues for
the task type
declaration, which causes no trouble, from the elaboration issues
of the task object, which is also unproblematic, since it is now independent
of the elaboration of Utils
.
This separation of concerns also corresponds to
a generally sound engineering principle of separating declarations
from instances. This version of the program also compiles, binds, links,
and executes, generating the expected output.
Let us consider more carefully why our original sample program works
under the dynamic model of elaboration. The reason is that the code
in the task body blocks immediately on the accept
statement. Now of course there is nothing to prohibit elaboration
code from making entry calls (for example from another library level task),
so we cannot tell in isolation that
the task will not execute the accept statement during elaboration.
However, in practice it is very unusual to see elaboration code
make any entry calls, and the pattern of tasks starting
at elaboration time and then immediately blocking on accept
or
select
statements is very common. What this means is that
the compiler is being too pessimistic when it analyzes the
whole package body as though it might be executed at elaboration
time.
If we know that the elaboration code contains no entry calls, (a very safe assumption most of the time, that could almost be made the default behavior), then we can compile all units of the program under control of the following configuration pragma:
pragma Restrictions (No_Entry_Calls_In_Elaboration_Code);
This pragma can be placed in the gnat.adc file in the usual
manner. If we take our original unmodified program and compile it
in the presence of a gnat.adc containing the above pragma,
then once again, we can compile, bind, link, and execute, obtaining
the expected result. In the presence of this pragma, the compiler does
not trace calls in a task body, that appear after the first accept
or select
statement, and therefore does not report a potential
circularity in the original program.
The compiler will check to the extent it can that the above restriction is not violated, but it is not always possible to do a complete check at compile time, so it is important to use this pragma only if the stated restriction is in fact met, that is to say no task receives an entry call before elaboration of all units is completed.