Up to this point, we have pretended that when abuild builds an item, it recursively reads the interface files of all its dependencies. Although this is the effect of what the interface system does, it is not exactly what happens. In this section, we will explain what really happens.
Internally, abuild implements an
Interface
object and an
InterfaceParser
object. Each
InterfaceParser
instance contains one
Interface
object. We use one
InterfaceParser
instance to load each
Abuild.interface
file and all of its
after-build
files. The scope of
reset
and reset-all
statements is the InterfaceParser
instance.
Internally, an Interface
object maintains
a list of variables, each of which has a declaration and a list
of assignments. Each declaration and assignment is marked with
the file location (file name, line number, and column number) at
which it appeared. Additionally, assignment information includes
any flag that the assignment may be conditional upon. Abuild
does not actually maintain the value of a variable. It only
maintains the list of assignments. Values of variables are
computed on the fly as they are needed. For list variables, all
assignment statements are maintained. For scalar variables, we
store first all fallback assignments in the opposite of the order
in which they appeared (with later fallback assignments being
pushed onto the beginning of the history), the one normal
assignment (as more than one normal assignment is an error), and
then all override assignments in the order in which they appear
(with later assignments added to the end of the history). When
we perform a reset
operation on an interface
variable, we do not store the reset
operation (other than to record that it happened for purposes of
showing it in the --dump-interfaces
output).
Rather, we actually clear out the variable's assignment history.
We discuss this further momentarily.
When a build item or another Interface
object attempts to retrieve the value of a variable, abuild
determines what flags, if any, are in effect and filters out any
assignments that are connected with flags that are not set.
Then, for list variables, the results of each remaining
assignment are appended or prepended to the list, depending upon
whether the list was declared as append or
prepend. For scalar variables, only the last item
in the assignment history is used. In this way, if there were
only fallback assignments, the first fallback assignment would be
at the end of the list. If there were any override assignments,
the last override assignment would be at the end of the list. If
there were only normal assignments, the normal assignment would
be there. It is important that we maintain all of this
information because we might filter out some assignments based on
flags. We discuss this in more depth below.
One Interface
object may
import other
Interface
objects. When one
Interface
object imports another, the
object merges the imported object's variable history with its
own. Any declarations or assignments that are exactly duplicated
(that is, they have the same file location as a previously seen
operation) are ignored. This is important since we may import
the same interface file through more than one path.
The import process is the only part of the interface system
implementation that is affected by the scope of a variable
(whether the variable is a normal recursive variable or was
declared non-recursive or local).
Specifically, when importing an interface, if the variable was
declared as local, the declaration and assignments
are both ignored by the import process. If the variable was
declared as non-recursive, the declaration is always
imported, but only assignments that were made in the item that
owns the interface are actually imported. For example, suppose
A
imports B
's interface
which in turn imports C
's interface. In
this case, A
would not see the affect of any
assignments to non-recursive variables that were made in
C
since it does not directly import
C
's interface. It would also not see
declarations or local variable assignments to any local variables
in either B
or C
.
There is a subtle aspect of how reset
works
in connection with loading interfaces as a result of the fact
that a reset
actually clears the assignment
history of a variable at the time of the reset operation rather
than storing the reset
as part of the
history. For example, suppose you have interfaces
Q
and R
and that
R
imports Q
,
Q
assigns to variable
A
, and R
resets
variable A
. If interface
S
imports just R
,
it will not see Q
's assignment to
A
because that assignment is not part of
R
. On the other hand, if
S
imports both Q
and R
in any order, it
will see Q
's
assignment to A
. If the reset operation were
actually part of the assignment history rather than being a local
operation, then whether or not S
saw
Q
's assignment to A
would be dependent upon the order in which
S
loaded Q
and
R
. For items that are not in each other's
dependency chains, the order is not deterministic. This could
cause very strange side effects: if one build item depended on
other, it could sometimes not see all of that item's interface
because of some third item that did a reset. Note also that
abuild uses a single interface parser to load a given interface
file and any after-build files, so a reset in an after-build
actually does effectively remove the effect of any assignments to
that variable in the file that loads it. Since a reset in an
after-build file is not visible to the item itself, this is a
useful construct for clearing interface variables that a build
item means to set for its own use but not for its dependencies.
For an example of this construct, see Section 27.1, “Opaque Wrapper Example”.
When a variable assignment is prefixed by a
flag
statement, the assignment entry that
goes into the variable's assignment history is associated with
the name of the build item and the flag. When a variable value
is retrieved, abuild filters out any assignments that are
marked with a flag that is not set. This makes it possible for
abuild to store exactly one representation of each interface
object rather than having to keep track of different instances
for each possible combination of flags. It also makes it
possible for different build items to actually see different
results for the same interface objects depending upon what flags
they are requesting.
Abuild only turns on interface flags when it retrieves variable
values for export into the automatically generated file used by
the back end (the dynamic output file,
first introduced in Section 17.1, “Abuild Interface Functionality Overview”). It
does not have any flags set when it references variables inside
of other Abuild.interface
files. For
example, if A
does this:
declare X string declare Y string X = v1 flag f1 override X = v2 Y = $(X)
the value of Y
will
always be v1
in every
build item's dynamic output file regardless of whether or not
that build item sets the f1
flag in its
dependency on A
. This is because that is
the value that X
had at the time when
Y
was assigned since the flag was not in
effect during the parsing of the interface file. The value of
X
in the dynamic output files
will be dependent upon whether the flag is
in effect for the dependency on A
because
abuild does set flags before generating the dynamic output
files. This makes sense when you consider that abuild reads
each Abuild.interface
file once for each
platform and that values of variables are not computed until they
are needed.