To do this, you write one or more init specifiers as part of a component instance definition. An init specifier names a phase of the setup or teardown process and provides a snippet of literal C++ code. The FPP translator pastes the snippet into the setup or teardown code according to the phase named in the specifier. (Strictly speaking, the init specifier should be called a "setup or teardown specifier." However, most of the code is in fact initialization code, and so FPP uses "init" as a shorthand name.)
10.3.1. Execution Phases
The FPP translator uses init specifiers when it generates code for an F Prime topology. We will have more to say about topology generation in the next section. For now, you just need to know the following:
-
A topology is a unit of an FPP model that specifies the top-level structure of an F Prime application (the component instances and their connections).
-
Each topology has a name, which we will refer to here generically as T.
-
When generating C++ code for topology T, the code generator produces files T
TopologyAc.hpp
and TTopologyAc.cpp
.
The generated code in T TopologyAc.hpp
and T TopologyAc.cpp
is divided into several phases of execution.
Table Execution Phases shows the execution phases
recognized by the FPP code generator.
In this table, T is the name of a topology and I is the
name of a component instance.
The columns of the table have the following meanings:
-
Phase: The symbol denoting the execution phase. These symbols are the enumerated constants of the enum
Fpp.ToCpp.Phases
defined inFpp/ToCpp.fpp
in the F Prime repository. -
Generated File: The generated file for topology T that contains the definition: either T
TopologyAc.hpp
(for compile-time symbols) or TTopologyAc.cpp
(for link-time symbols). -
Intended Use: The intended use of the C++ code snippet associated with the instance I and the phase.
-
Where Placed: Where FPP places the code snippet in the generated file.
-
Default Code: Whether FPP generates default code if there is no init specifier for instance I and for this phase. If there is an init specifier, then it replaces any default code.
| Phase | Generated File | Intended Use | Where Placed | Default Code |
|---|---|---|---|---|
|
T |
C++ constants for use in constructing and initializing an instance I. |
In the namespace |
None. |
|
T |
Statically declared C++ objects for use in constructing and initializing instance I. |
In the namespace |
None. |
|
T |
A constructor for an instance I that has a non-standard constructor format. |
In an anonymous (file-private) namespace. |
The standard constructor call for I. |
|
T |
Initialization code for an instance I that has a non-standard initialization format. |
In the file-private function |
The standard call to |
|
T |
Implementation-specific configuration code for an instance I. |
In the file-private function |
None. |
|
T |
Code for registering the commands of I (if any) with the command dispatcher. Required only if I has a non-standard command registration format. |
In the file-private function |
The standard call to |
|
T |
Code for reading parameters from a file. Ordinarily used only when I is the parameter database. |
In the file-private function |
None. |
|
T |
Code for loading parameter values from the parameter database. Required only if I has a non-standard parameter-loading format. |
In the file-private function |
The standard call to |
|
T |
Code for starting the task (if any) of I. |
In the file-private function |
The standard call to |
|
T |
Code for stopping the task (if any) of I. |
In the file-private function |
The standard call to |
|
T |
Code for freeing the thread associated with I. |
In the file-private function |
The standard call to |
|
T |
Code for deallocating the allocated memory (if any) associated with I. |
In the file-private function |
None. |
You will most often need to write code for configConstants
,
configObjects
, and configComponents
.
These phases often require implementation-specific input that
cannot be provided in any other way, except to write an init specifier.
In theory you should never have to write code for instances
or initComponents
— this code can be be standardized — but in practice not all F Prime components conform to the standard,
so you may have to override the default.
You will typically not have to write code for regCommands
,
readParameters
, and loadParameters
— the framework can generate
this code automatically — except that the parameter database
instance needs one line of special code for reading its parameters.
Code for startTasks
, stopTasks
,
and freeThreads
is required only if the user-written implementation of
a component instance manages its own F Prime task.
If you use a standard F Prime active component, then the framework
manages the task, and this code is generated automatically.
Code for tearDownComponents
is required only if a component
instance needs to deallocate memory or release resources on program exit.
10.3.2. Writing Init Specifiers
You may write one or more init specifiers as part of a component instance definition. The init specifiers, if any, come at the end of the definition and must be enclosed in curly braces. The init specifiers form an element sequence with a semicolon as the optional terminating punctuation.
To write an init specifier, you write the following:
-
The keyword
phase
. -
The execution phase of the init specifier.
-
A string that provides the code snippet.
It is usually convenient, but not required, to use a multiline string for the code snippet.
As an example, here is the component instance definition for the
command sequencer instance cmdSeq
from the
F Prime system reference deployment:
instance cmdSeq: Svc.CmdSequencer base id 0x0700 \
queue size Default.queueSize \
stack size Default.stackSize \
priority 100 \
{
phase Fpp.ToCpp.Phases.configConstants """
enum {
BUFFER_SIZE = 5*1024
};
"""
phase Fpp.ToCpp.Phases.configComponents """
cmdSeq.allocateBuffer(
0,
Allocation::mallocator,
ConfigConstants::cmdSeq::BUFFER_SIZE
);
"""
phase Fpp.ToCpp.Phases.tearDownComponents """
cmdSeq.deallocateBuffer(Allocation::mallocator);
"""
}
The code for configConstants
provides a constant BUFFER_SIZE
that is used in the configComponents
phase.
The code for configComponents
calls allocateBuffer
, passing
in an allocator object that is declared elsewhere.
(In the section on
implementing deployments, we will explain where this allocator
object is declared.)
The code for tearDownComponents
calls deallocateBuffer
to
deallocate the sequence buffer, passing in the allocator
object again.
As another example, here is the instance definition for the parameter
database instance prmDb
from the system reference deployment:
instance prmDb: Svc.PrmDb base id 0x0D00 \
queue size Default.queueSize \
stack size Default.stackSize \
priority 96 \
{
phase Fpp.ToCpp.Phases.instances """
Svc::PrmDb prmDb(FW_OPTIONAL_NAME("prmDb"), "PrmDb.dat");
"""
phase Fpp.ToCpp.Phases.readParameters """
prmDb.readParamFile();
"""
}
Here we provide code for the instances
phase because the constructor
call for this component is nonstandard — it takes the parameter
file name as an argument.
In the readParameters
phase, we provide the code for reading the parameters
from the file.
As discussed above, this code is needed only for the parameter database
instance.
When writing init specifiers, you may read (but not modify) a special value
state
that you define in a handwritten main function.
This value lets you pass application-specific information from the
handwritten code to the auto-generated code.
We will explain the special state
value further in the
section on implementing deployments.
For more examples of init specifiers in action, see the rest of
the file SystemReference/Top/instances.fpp
in the F Prime repository.
In particular, the init specifiers for the comDriver
instance
use the state
value that we just mentioned.
11. Defining Topologies
In F Prime, a topology or connection graph is the highest level of software architecture in a FSW application. A topology specifies what component instances are used in the application and how their port instances are connected.
An F Prime FSW application consists of a topology T; all the types, ports, and components used by T; and a small amount of top-level C++ code that you write by hand. In the section on implementing deployments, we will explain more about the top-level C++ code. In this section we explain how to define a topology in FPP.
11.1. A Simple Example
We begin with a simple example that shows how many of the pieces fit together.
port P
passive component C {
sync input port pIn: P
output port pOut: P
}
instance c1: C base id 0x100
instance c2: C base id 0x200
@ A simple topology
topology Simple {
@ This specifier says that instance c1 is part of the topology
instance c1
@ This specifier says that instance c2 is part of the topology
instance c2
@ This code specifies a connection graph C1
connections C1 {
c1.pOut -> c2.pIn
}
@ This code specifies a connection graph C2
connections C2 {
c2.pOut -> c1.pIn
}
}
In this example, we define a port P
.
Then we define a passive component C
with an input port and an output port, both of type P
.
We define two instances of
C
, c1
and c2
.
We put these instances into a topology called Simple
.
As shown, to define a topology, you write the keyword topology
,
the name of the topology, and the members of the topology
definition enclosed in curly braces.
In this case, the topology has two kinds of members:
-
Two instance specifiers specifying that instances
c1
andc2
are part of the topology. -
Two graph specifiers that specify connection graphs named
C1
andC2
.
As shown, to write an instance specifier, you write the
keyword instance
and the name of a component instance
definition.
In general the name may be a qualified name such as A.B
if the instance is defined inside a
module; in this simple
example it is not.
Each instance specifier states that the instance it names
is part of the topology.
The instances appearing in the list must be distinct.
For example, this is not correct:
topology T {
instance c1
instance c1 # Error: duplicate instance c1
}
A graph specifier specifies one or more connections between component instances. Each graph specifier has a name. By dividing the connections of a topology into named graphs, you can organize the connections in a meaningful way. For example you can have one graph group for connections that send commands, another one for connections that send telemetry, and so forth. We will have more to say about this in a later section.
As shown, to write a graph specifier, you may write the keyword connections
followed by the name of the graph; then you may list
the connections inside curly braces.
(In the next section, we will explain another way to write a graph specifier.)
Each connection consists of an endpoint, an arrow ->
,
and another endpoint.
An endpoint is the name of a component instance
(which in general may be a qualified name), a dot,
and the name of a port of that component instance.
In this example there are two connection graphs, each containing one connection:
-
A connection graph
C1
containing a connection fromc1.pOut
toc2.pIn
. -
A connection graph
C2
containing a connection fromc2.pOut
toc1.pIn
.
As shown, topologies and their members are annotatable elements. The topology members form an element sequence in which the optional terminating punctuation is a semicolon.
11.2. Connection Graphs
In general, an FPP topology consists of a list of instances and a set of named connection graphs. There are two ways to specify connection graphs: direct graph specifiers and pattern graph specifiers.
11.2.1. Direct Graph Specifiers
A direct graph specifier provides a name and a list
of connections.
We illustrated direct graph specifiers in the
previous section, where the simple topology example
included direct graph specifiers for graphs named
C1
and C2
.
Here are some more details about direct graph specifiers.
As shown in the previous section, each connection consists of an output port specifier, followed by an arrow, followed by an input port specifier. For example:
connections C {
a.p -> b.p
}
Each of the two port specifiers consists of a component instance name, followed by a dot, followed the name of a port instance. The component instance name must refer to a component instance definition and may be qualified by a module name. For example:
connections C {
M.a.p -> N.b.p
}
Here component instance a
is defined in module M
and component
instance b
is defined in module N
.
In a port specifier a.p
, the port instance name p
must refer to a
port instance of the
component definition associated with the component instance a
.
Each component instance named in a connection must be part of the
instance list in the topology.
For example, if you write a connection a.b -> c.d
inside
a topology T
, and the specifier instance a
does not
appear inside topology T
, then you will get an error — even if a
is a valid instance name for the FPP model.
The reason for this rule is that in flight code we need
to be very careful about which instances are included
in the application.
Naming all the instances also lets us check for
unconnected ports.
You may use the same name in more than one direct graph specifier in the same topology. If you do this, then all specifiers with the same name are combined into a single graph with that name. For example, this code
connections C {
a.p -> b.p
}
connections C {
c.p -> d.p
}
is equivalent to this code:
connections C {
a.p -> b.p
c.p -> d.p
}
The members of a direct graph specifier form an element sequence in which the optional terminating punctuation is a comma. For example, you can write this:
connections C { a.p -> b.p, c.p -> d.p }
The connections appearing in direct graph specifiers must obey the following rules:
-
Each connection must go from an output port instance to an input port instance.
-
The types of the ports must match, except that a serial port instance may be connected to a port of any type. In particular, serial to serial connections are allowed.
-
If a typed port P is connected to a serial port in either direction, then the port type of P may not specify a return type.
11.2.2. Pattern Graph Specifiers
A few connection patterns are so common in F Prime that they
get special treatment in FPP.
For example, an F Prime topology typically includes an
instance of the component Svc.Time
.
This component has a port timeGetPort
of type Fw.Time
that other components can use to get the system
time.
Any component that gets the system time
(and there are usually several) has a connection to
the timeGetPort
port of the Svc.Time
instance.
Suppose you are constructing a topology in which
(1) sysTime
is an instance of Svc.Time
; and (2)
each of the instances
a
, b
, c
, etc., has a
time get port
timeGetOut
port connected to sysTime.timeGetPort
,
If you used a direct graph specifier to write all these connections,
the result might look like this:
connections Time {
a.timeGetOut -> sysTime.timeGetPort
b.timeGetOut -> sysTime.timeGetPort
c.timeGetOut -> sysTime.timeGetPort
...
}
This works, but it is tedious and repetitive. So FPP provides a better way: you can use a pattern graph specifier to specify this common pattern. You can write
time connections instance sysTime
This code says the following:
-
Use the instance
sysTime
as the instance ofFw.Time
for the time connection pattern. -
Automatically construct a direct graph specifier named
Time
. In this direct graph specifier, include one connection from each component instance that has a time get port to the input port ofsysTime
of typeFw.Time
.
The result is as if you had written the direct graph specifier
yourself.
All the other rules for direct graph specifiers apply: for example,
if you write another direct graph specifier with name Time
, then
the connections in that specifier are merged with the connections
generated by the pattern specifier.
In the example above, we call time
the kind of the pattern
graph specifier.
We call sysTime
the source instance of the pattern.
It is the source of all the time pattern connections
in the topology.
We call the instances that have time get ports (and so contribute
connections to the pattern) the target instances.
They are the instances targeted by the pattern once the source
instance is named.
Table Pattern Graph Specifiers shows the pattern graph specifiers allowed in FPP. The columns of the table have the following meanings:
-
Kind: The keyword or keywords denoting the kind. When writing the specifier, these appear just before the keyword
connections
, as shown above for the time example. -
Source Instance: The source instance for the pattern.
-
Target Instances: The target instances for the pattern.
-
Graph Name: The name of the connection graph generated by the pattern.
-
Connections: The connections generated by the pattern.
The command pattern specifier generates three connection graphs:
Command
, CommandRegistration
, and CommandResponse
.
| Kind | Source Instance | Target Instances | Graph Name | Connections |
|---|---|---|---|---|
|
All connections from the unique output port of type |
|||
|
An instance of |
Each instance that has command ports. |
|
All connections from the
|
|
All connections from the
|
|||
|
An instance of |
Each instance that has an
|
|
All connections from the
|
|
An instance of |
Each instance other than the source instance
that has a unique output port of type
|
|
(1) All connections from the unique output port of type
|
|
An instance of |
Each instance that has parameter ports. |
|
(1) All connections from the
|
|
An instance of |
Each instance that has a telemetry port. |
|
All connections from the
|
|
An instance of |
Each instance that has a |
|
All connections from the
|
|
An instance of |
Each instance that has a
|
|
All connections from the
|
Here are some rules for writing graph pattern specifiers:
-
At most one occurrence of each pattern kind is allowed in each topology.
-
For each pattern, the required ports shown in the table must exist and must be unambiguous. For example, if you write a time pattern
time connections instance sysTime
then you will get an error if
sysTime
has no input ports of typeFw.Time
, You will also get an error ifsysTime
has two or more such ports.
The default behavior for a pattern is
to generate the connections for all target instances
as shown in the table.
If you wish, you may generate connections for a selected
set of target instances.
To do this, you write a list of target instances enclosed in
curly braces after the source instance.
For example, suppose a topology contains instances
a
, b
, and c
each of which has an output port
that satisfies the time pattern.
And suppose that sysTime
is an instance of Svc.Time
.
Then if you write this pattern
time connections instance sysTime
you will get a connection graph Time
containing
time connections from each of a
, b
, and c
to sysTime
.
But if you write this pattern
time connections instance sysTime {
a
b
}
then you will just get the connections from a
and b
to sysTime
.
The instances a
and b
must be valid target instances
for the pattern.
As with connections, you can write the instances a
and b
each on its own line, or you can separate them with commas:
time connections instance sysTime { a, b }
11.3. Port Numbering
As discussed in the section on defining components, each named port instance is actually an array of one or more port instances. When the size of the array exceeds one, you must specify the port number (i.e., the array index) of each connection going into or out of the port instance. In FPP, there are three ways to specify port numbers: explicit numbering, matched numbering, and general numbering.
11.3.1. Explicit Numbering
To use explicit numbering, you provide an explicit port number for a connection endpoint. You write the port number as a numeric expression in square brackets, immediately following the port name. The port numbers start at zero.
For example, the RateGroups
graph of the Ref (reference) topology in the F Prime
repository defines the rate group connections.
It contains the following connection:
rateGroupDriverComp.CycleOut[Ports.RateGroups.rateGroup1] -> rateGroup1Comp.CycleIn
rateGroup1Comp.RateGroupMemberOut[0] -> SG1.schedIn
rateGroup1Comp.RateGroupMemberOut[1] -> SG2.schedIn
rateGroup1Comp.RateGroupMemberOut[2] -> chanTlm.Run
rateGroup1Comp.RateGroupMemberOut[3] -> fileDownlink.Run
The first line says to connect the port at index
Ports.RateGroups.rateGroup1
of rateGroupDriverComp.CycleOut
to rateGroup1Comp.CycleIn
.
The symbol Ports.RateGroups.rateGroup1
is an enumerated constant, defined
like this:
module Ports {
enum RateGroups {
rateGroup1
rateGroup2
rateGroup3
}
}
The second and following lines say to connect the ports of
rateGroup1Comp.RateGroupMemberOut
at the indices 0, 1, 2, and 3
in the manner shown.
As another example, the Downlink
graph of the reference topology
contains the following connection:
downlink.framedAllocate -> staticMemory.bufferAllocate[Ports.StaticMemory.downlink]
This line says to connect downlink.framedAllocate
to
staticMemory.bufferAllocate
at index
Port.StaticMemory.downlink
.
Again the port index is a symbolic constant.
If you wish, you may write two explicit port numbers, one at each endpoint. For example:
a.b[0] -> c.d[1]
Here are some rules to keep in mind when using explicit numbering:
-
You can write any numeric expression as a port number. Each port number must be in bounds for the port (greater than or equal to zero and less than the size of the port array). If you write a port number that is out of bounds, you will get an error.
-
Use symbolic constants judiciously. Avoid scattering "magic" literal constants throughout the topology definition. For example:
-
The Ref topology uses the symbolic constants
Ports.RateGroups.rateGroup1
andPorts.StaticMemory.downlink
, as shown above. Because these constants appear in several different places, it is better to use symbolic constants here. Using literal constants would decrease readability and increase the chance of using incorrect or inconsistent numbers. -
The Ref topology uses the literal constants 0, 1, 2, and 3 to connect the ports of
rateGroup1Comp.RateGroupMemberOut
. Here there are no obvious names to associate with the numbers, the numbers go in sequence, and all the numbers appear together in one place. So there is no clear benefit to giving them names.
-
-
Remember that in F Prime, multiple connections can go to the same input port, but only one connection can go from each output port. For example, this code is allowed:
c1.p1 -> c2.p[0] c1.p2 -> c2.p[0] # OK: Two connections into c2.p[0]
But this code is incorrect:
c1.p[0] -> c2.p1 c1.p[0] -> c2.p2 # Error: Two connections out of c1.p[0]
-
Use explicit numbering as little as possible. Instead, use matched numbering or general numbering (described in the next sections) and let FPP do the numbering for you. In particular, avoid writing zero indices such as
c.p[0]
except in cases where you need to control the assignment of numbers, such as in the rate group example shown above. In other cases, writec.p
and let FPP infer the zero index. For example, this is what we did in the section on direct graph specifiers.
11.3.2. Matched Numbering
Automatic matching: After resolving explicit numbering, the FPP translator applies matched numbering. In this step, the translator numbers all pairs of matched ports.
Matched numbering is essential for resolving the command and health patterns, each of which has matched ports. You can also use matched numbering in conjunction with direct graph specifiers. For example, the Ref topology contains the following connections:
connections Sequencer {
cmdSeq.comCmdOut -> cmdDisp.seqCmdBuff
cmdDisp.seqCmdStatus -> cmdSeq.cmdResponseIn
}
connections Uplink {
...
uplink.comOut -> cmdDisp.seqCmdBuff
cmdDisp.seqCmdStatus -> uplink.cmdResponseIn
...
}
The port cmdDisp.seqCmdBuff
port of the command dispatcher receives
command input from the command sequencer or from the ground.
The corresponding command response goes out on
port cmdDisp.seqCmdStatus
.
These two ports are matched in the definition of the Command
Sequencer component.
When you use matched numbering with direct graph specifiers, you must obey the following rules:
-
When a component has the matching specifier
match p1 with p2
, for every connection betweenp1
and another component, there must be a corresponding connection between that other component andp2
. -
You can use explicit numbering, and the automatic matching will work around the numbers you supply if it can. However, you may not do this in a way that makes the matching impossible. For example, you may not connect
p1[0]
to another component andp2[1]
to the same component, because this connection forces a mismatch.
If you violate these rules, you will get an error during analysis.
Manual matching: Automatic matched numbering works when each matched pair of connections goes between the same two components, one of which has a matched pair of ports. If the matching does not follow this pattern, then automatic matched numbering will not work, and you will need to use explicit port numbers to express the matching. For example, the Ref topology contains these connections:
comm.allocate -> staticMemory.bufferAllocate[Ports.StaticMemory.uplink]
comm.$recv -> uplink.framedIn
uplink.framedDeallocate -> staticMemory.bufferDeallocate[Ports.StaticMemory.uplink]
In this case the staticMemory
instance requires that pairs of
allocation and deallocation requests for the same memory
go to the same port.
But the allocation request comes from comm
,
and the deallocation request comes from uplink
.
Since the allocation and deallocation connections go to different
component instances, we can’t used automatic matched numbering.
Instead we define a symbolic constant Ports.StaticMemory.uplink
and use that twice to do the matching by hand.
11.3.3. General Numbering
After resolving explicit numbering and matched numbering, the FPP translator applies general numbering. In this step, the translator uses the following algorithm to fill in any remaining unassigned port numbers:
-
Traverse the connections in a deterministic order. The order is fully described in The FPP Language Specification.
-
For each connection
-
If the output port number is unassigned, then set it to the lowest available port number.
-
If the input port number is unassigned, then set it to zero.
-
For example, consider the following connections:
a.p -> b.p
a.p -> c.p
After general numbering, the connections could be numbered as follows:
a.p[0] -> b.p[0]
a.p[1] -> c.p[0]
11.4. Importing Topologies
It is often useful to decompose a flight software project into several topologies. For example, a project might have the following topologies:
-
A topology for command and data handling (CDH) with components such as a command dispatcher, an event logger, a telemetry data base, a parameter database, and components for managing files.
-
Various subsystem topologies, for example power, thermal, attitude control, etc.
-
A release topology.
Each of the subsystem topologies might include the CDH topology. The release topology might include the CDH topology and each of the subsystem topologies. Further, to enable modular testing, it is useful for each topology to be able to run on its own.
In FPP, the way we accomplish these goals is to import one topology into another one. In this section of the User Guide, we explain how to do that.
11.4.1. Importing Instances and Connections
To import a topology A
into a topology B
, you write
import A
inside topology B
, like this:
topology B {
import A
...
}
You may add instances and connections as usual to B
, as shown
by the dots.
When you do this, the FPP translator does the following:
-
Resolve
A
: Resolve all pattern graph specifiers inA
, and resolve all explicit port numbers inA
. Call the resulting topologyT
. -
Form the instances of
B
: Take the union of the instances specified inT
and the instances specified inB
, counting any duplicates once. These are the instances ofB
. -
Form the connections of
B
: Take the union of the connection graphs specified inT
and the connection graphs specified inB
. If each ofT
andB
has a connection between the same ports, then each becomes a separate connection inB
. -
Resolve
B
: Resolve the pattern graph specifies ofB
. Apply matched numbering and general numbering toB
.
For example, suppose topologies A
and B
are defined
as follows:
topology A {
instance a
instance b
connections C1 {
a.p1 -> b.p
}
}
topology B {
import A
instance c
connections C1 {
a.p1 -> c.p
}
connections C2 {
a.p2 -> c.p
}
}
After import resolution, B
is equivalent to this topology:
topology B {
instance a
instance b
instance c
connections C1 {
a.p1 -> b.p
a.p1 -> c.p
}
connections C2 {
a.p2 -> c.p
}
}
Notice that the C1
connections of A
are merged with the C1
connections of B
.
11.4.2. Private Instances
Often when importing topology A
into topology B
, you
want to include one or more instances in A
that exist just
for running A
, but that you don’t want imported into B
.
For example, A
could have an instance cStub
which is a stub version of
a component c
that is fully implemented in B
.
In this case
-
When running
A
you may needcStub
; the topology may not run or may not even compile without it. -
When importing
A
intoB
you don’t want to importcStub
, because it is superseded by the real implementationc
inB
. Also, any connections tocStub
inA
should be replaced by connections toc
inB
.
To handle this case, you can make cStub
a private instance
of A
and c
an instance of B
.
When you import B
into A
, cStub
will not become an instance
of B
.
Further, no connections in A
involving cStub
will be imported
into B
.
As an example, suppose we revise topology A
from the previous
section as follows:
topology A {
instance a
instance b
private instance d
connections C1 {
a.p1 -> b.p
}
connections C2 {
a.p1 -> d.p
}
}
Notice that we have added an instance d
to topology
A
, and we have declared d
private to A
.
We have also added a new connection to d
in the
connection graph C2
.
Now suppose that we use the same definition of B
given
in the previous section.
After import resolution, B
will still be equivalent
to the topology shown at the end of the last section:
we have added an instance and a connection to A
,
but the instance is private and the connection goes
from a private instance, so neither the instance nor
the connection is imported into B
.
11.4.3. Multiple Imports
Multiple imports are allowed. For example:
topology A {
import B
import C
...
}
This has the obvious meaning: both topology B
and
topology C
are imported into topology A
, according
to the rules described above.
Each topology may appear at most once in the import list. For example, this is incorrect:
topology A {
import B
import B # Error: B imported twice
}
11.4.4. Transitive Imports
In general, transitive imports are allowed.
For example, topology A
may import topology B
,
and topology B
may import topology C
.
Resolution works bottom-up on the import graph:
for example, first we resolve C
, and then we resolve B
,
and then we resolve A
.
Cycles in the import graph are not allowed.
For example, if A
imports B
and B
imports C
and C
imports A
, you will get an error.
11.5. Include Specifiers
You can include code from another file in a topology definition. You do this by writing an include specifier. We will explain more about this in the section on include specifiers below.
12. Specifying Models as Files
The previous sections have explained the syntactic and semantic elements of FPP models. This section takes a more file-centric view: it explains how to assemble a collection of elements specified in several files into a model.
We discuss several tools for specifying and analyzing dependencies between model files. We focus on how to use the tools, and we summarize their most important features. We do not attempt to cover every feature of every tool. For more comprehensive coverage, see the FPP wiki.
12.1. Dividing Models into Files
Unlike F Prime XML, FPP does not require any particular division of model elements into files. For example, there is no requirement that each type definition reside in its own file. Nor is there any requirement that the names of files correspond to the names of the definitions they contain.
Of course you should try to adhere to good style when decomposing a large model into many files. For example:
-
Group related model elements into files, and name the files according to the purpose of the grouping.
-
Choose meaningful module names, and group all files in a single module in single directory (including its subdirectories). In the F Prime distribution, the
Fw
andSvc
directories follow this pattern, where the C++ namespacesFw
andSvc
correspond to FPP modules. -
Group files into modules and directories logically according to their function.
-
You can group files according to their role in the FPP model. For example, group types separately from ports.
-
You can group files according to their role in the FSW. For example, group framework files separately from application files.
-
-
If the definition of a constant or type is logically part of a component, then make the definition a member of the component.
There is still the usual requirement that a syntactic unit must begin and end in the same file. For example:
-
Each type definition is a syntactic unit, so each type definition must begin and end in the same file.
-
A module definition may span several syntactic units of the form
module { … }
, so a module definition may span multiple files (with each unit of the formmodule { … }
residing in a single file).
These rules are similar to the way that C++ requires a class definition
class C { … }
or a namespace block namespace N { … }
to reside in a
single file, but it allows the definition of a single namespace N
to span
multiple blocks
namespace N { … }
that can be in different files.
12.2. Include Specifiers
As part of an FPP model, you can write one or more include specifiers. An include specifier is an instruction to include FPP source elements from one file into another file. Include specifiers may occur at the top level of a model, inside a module definition, inside a component definition, or inside a topology definition.
The main purpose of include specifiers is to split up large syntactic units into several files. For example, a component definition may include a telemetry dictionary from a separate file.
To write an include specifier, you write the keyword include
followed by string denoting a file path.
The path is relative to the file in which the include specifier appears.
By convention, included FPP files end in .fppi
to distinguish
them from .fpp
files that are directly analyzed and translated.
For example, suppose that the file a.fppi
contains the definition
constant a = 0
In a file b.fppi
in the same directory, you could write this:
include "a.fppi"
constant b = a
After resolving the include specifier, the model is equivalent to the following:
constant a = 0
constant b = a
To see this, do the following:
-
Create files
a.fppi
andb.fpp
as described above. -
Run
fpp-format -i b.fpp
.
fpp-format
is a tool for formatting FPP source files.
It also can expand include specifiers.
fpp-format
is discussed further in the section on
formatting FPP source.
As mentioned above, the path is relative to the directory
of the file containing the include specifier.
So if a.fppi
is located in a subdirectory A
, you could write this:
include "A/a.fppi"
constant b = a
And if a.fppi
is located in the parent directory, you could write this:
include "../a.fppi"
constant b = a
You can write an include specifier inside a module.
In this case, any definitions in the included file are treated as occurring
inside the module.
For example, if a.fppi
contains the definition constant a = 0
,
then this source text
module M { include "a.fppi" }
defines the constant M.a
.
As an exercise, try this:
% echo "module M { constant a = 0 }" > a.fppi % fpp-check include "a.fppi" constant b = M.a ^D %
The check should pass.
In any case, an included file must contain complete syntactic
units that may legally appear at the point where the include specifier appears.
For example, an included file may contain one or more constant
definitions or type definitions.
It may not contain a bare identifier a
, as this is not a valid top-level
or module-level syntactic unit.
Nor is it valid to write an include specifier in a place where an identifier
like a
is expected.
For example, here is the result of a failed attempt to include an identifier into a constant definition:
% echo a > a.fppi % fpp-check module M { constant include "a.fppi" = 0 } constant b = M.a ^D fpp-check stdin: 1.21 module M { constant include "a.fppi" = 0 } ^ error: identifier expected %
12.3. Dependencies
Whenever a model spans two or more files, one file F may use one or more definitions appearing in other files. In order to analyze F, the tools must extract the definitions from these other files, called the dependencies of F.
For example, suppose the file a.fpp
contains the following definition:
constant a = 0
And suppose the file b.fpp
contains the following definition:
constant b = a
If you present both files to fpp-check
, like this:
% fpp-check a.fpp b.fpp
the check will pass.
However, if you present just b.fpp
, like this:
% fpp-check b.fpp
you will get an error stating that the symbol a
is undefined. (Try it and
see.)
The error occurs because the definition of a
is located in a.fpp
,
which was not included in the input to the analysis.
In this case we say that a.fpp
is a dependency of b.fpp
.
In order to analyze a file F (for example, b.fpp
), the analyzer
needs to be told where to find all the dependencies of F (for example,
a.fpp
).
For simple models, we can manage the dependencies by hand, as we did for the example above. However, for even moderately complex models, this kind of hand management becomes difficult. Therefore FPP has a set of tools and features for automatic dependency management.
In summary, dependency management in FPP works as follows:
-
You run a tool called
fpp-locate-defs
to generate location specifiers for all the definitions that could be used in a set of files F. -
You run a tool called
fpp-depend
, passing it the files F and the location specifiers generated in step 1. It emits a list of files containing definitions that are actually used in F (i.e., the dependencies of F).
These steps may occur in separate phases of development. For example:
-
You may run step 1 to locate all the type definitions available for use in the model.
-
You may run step 2 to develop ports that depend on the types. Typically you would run this step as part of a build process, e.g., the CMake build process included in the F Prime distribution.
Below we explain these steps in more detail.
12.4. Location Specifiers
A location specifier is a unit of syntax in an FPP model. It specifies the location of a definition used in the model.
Although it is possible to write location specifiers by hand, you should usually not do so. Instead, you should write definitions and let the tools discover their locations, as described in the section on locating definitions.
12.4.1. Syntax
A location specifier consists of the keyword locate
, a kind of definition,
the name of a definition, and a string representing a file path.
For example, to locate the definition of constant a
at a.fpp
,
we would write
# Locating a constant definition
locate constant a at "a.fpp"
For the current version of FPP, the kind of definition can be constant
,
type
, or port
.
To locate a type T
in a file T.fpp
, we would write the following:
# Locating a type definition
locate type T at "T.fpp"
To locate a port P
in a file P.fpp
, we write the following:
# Locating a port definition
locate port P at "P.fpp"
To locate an enum, we locate the type; the location of the enumerated constants are then implied:
# Locating an enum definition,
# including the enumerated constant definitions
locate type E at "E.fpp"
12.4.2. Path Names
As with
include specifiers,
the path name in a location specifier L is relative to the
location of the file where L appears.
For example, suppose the file b.fpp
appears in the file system in some
directory D.
Suppose also that D has a subdirectory Constants
, Constants
contains a
file a.fpp
,
and a.fpp
defines the constant a
.
Then in b.fpp
we could write this:
locate constant a at "Constants/a.fpp"
If, instead of residing in a subdirectory, a.fpp
were located one directory above
b.fpp
in the file system, we could write this:
locate constant a at "../a.fpp"
12.4.3. Definition Names
The definition name appearing after the keyword locate
may be a qualified name.
For example, suppose the file M.fpp
contains the following:
module M { constant a = 0 }
Then in file b.fpp
we could write this:
locate constant M.a at "M.fpp"
Optionally, we may enclose the location specifier in the module M
, like
this:
module M { locate constant a at "M.fpp" }
A location specifier written inside a module this way has its definition name
implicitly qualified with the module name.
For example, the name a
appearing in the example above is automatically
resolved to M.a
.
Note that this rule is different than for other uses of definitions.
For example, when using the constant M.a
in an expression inside module M
,
you may spell the constant either a
or M.a
;
but when referring to the same constant M.a
in a location specifier inside
module M
, you must write a
and not M.a
.
(If you wrote M.a
, it would be incorrectly resolved to M.M.a
.)
The purpose of this rule is to facilitate dependency analysis,
which occurs before the analyzer has complete information about
definitions and their uses.
12.4.4. Included Files
When you write a file that contains definitions and you
include that file in another file,
the location of each definition is the file where the definition is
included, not the file where the definition appears.
For example, suppose that file a.fppi
contains the
definition constant a = 0
,
and suppose that file b.fpp
contains the include specifier include "a.fppi"
.
When analyzing b.fpp
, the location of the definition of the constant a
is b.fpp
, not a.fppi
.
12.5. Locating Definitions
Given a collection of FPP source files F, you can generate location specifiers
for all the definitions in F.
The tool for doing this analysis is called fpp-locate-defs
.
As example, you can run fpp-locate-defs
to report the locations of all
the definitions in a subdirectory called Constants
that contains constant
definitions for your model.
When analyzing other files that use the constants, you can use the location
specifiers to discover dependencies on individual files within Constants
.
12.5.1. Running fpp-locate-defs
To locate definitions, do the following:
-
Collect all the FPP source files containing the definitions you want to locate. For example, run
find Constants -name '*.fpp'
. -
Run
fpp-locate-defs
with the result of step 1 as the command-line arguments. The result will be a list of location specifiers.
For example, suppose the file Constants/a.fpp
defines the constant a
.
Running
% fpp-locate-defs `find Constants -name '*.fpp'`
generates the location specifier
locate constant a at "Constants/a.fpp"
12.5.2. Location Paths
By default, the location path is relative to the current
directory.
To specify a different base directory, use the option -d
.
For example, running
% fpp-locate-defs -d Constants `find Constants -name '*.fpp'`
generates the location specifier
locate constant a at "a.fpp"
12.5.3. Included Definitions
Consider the case where you write a definition in one file and
include that file in another file via an
include specifier.
For example, suppose file Constants.fpp
looks like this:
module Constants {
constant a = 0
include "b.fppi"
}
Suppose b.fppi
contains the definition constant b = 1
.
If you run find
on this directory as described above and provide
the output to fpp-locate-defs
, then you will get the following output:
-
The definition of constant
a
is located atConstants.fpp
. -
The definition of constant
b
is also located atConstants.fpp
.
For purposes of dependency analysis, this is what you want.
You want uses of b
to depend on Constants.fpp
(where the
definition
of b
is included) rather than b.fpp
(where the definition of b
is
stated).
When running a find
command to find files containing definitions,
you should exclude any files that are included in other files.
If your main FPP files end with .fpp
and your included FPP files end with
.fppi
, then running
find . -name '*.fpp'
will pick up just the main files.
12.6. Computing Dependencies
Given files F and location specifiers L that locate the definitions used in
F, you can
generate the dependencies of F.
The tool for doing this is called fpp-depend
.
12.6.1. Running fpp-depend
To run fpp-depend
, you pass it as input (1) files F that you want to
analyze
and (2) a superset of the location specifiers for the definitions used in that
code.
The tool extracts the location specifiers for the definitions used in F, resolves
them to absolute path names (the dependencies of F), and writes the
dependencies to standard output.
For example, suppose the file a.fpp
contains the following
definition:
constant a = 0
Suppose the file b.fpp
contains the following definition:
constant b = 1
Suppose the file locations.fpp
contains the following location
specifiers:
locate constant a at "a.fpp"
locate constant b at "b.fpp"
And suppose the file c.fpp
contains the following definition of c
,
which uses the definition of b
but not the definition of a
:
constant c = b + 1
Then running fpp-depend locations.fpp c.fpp
produces the output
[path-prefix]/b.fpp
.
The dependency output contains absolute path names, which will vary from system
to system.
Here we represent the system-dependent part of the path as [path-prefix]
.
% fpp-depend locations.fpp c.fpp [path-prefix]/b.fpp
As usual with FPP tools, you can provide input as a set of files or on standard input. So the following is equivalent:
% cat locations.fpp c.fpp | fpp-depend [path-prefix]/b.fpp
12.6.2. Transitive Dependencies
fpp-depend
computes dependencies transitively.
This means that if A depends on B and B
depends on C, then A depends on C.
For example, suppose again that locations.fpp
contains the following location specifiers:
locate constant a at "a.fpp"
locate constant b at "b.fpp"
Suppose the file a.fpp
contains the following definition:
constant a = 0
Suppose the file b.fpp
contains the following definition:
constant b = a
And suppose that file c.fpp
contains the following definition:
constant c = b
Notice that there is a direct dependency of c.fpp
on b.fpp
and a transitive dependency of c.fpp
on a.fpp
.
The transitive dependency occurs because there is a direct dependency
of c.fpp
on b.fpp
and a direct dependency of b.fpp
on a.fpp
.
Running fpp-depend
on locations.fpp
and c.fpp
produces both dependencies:
% fpp-depend locations.fpp c.fpp [path-prefix]/a.fpp [path-prefix]/b.fpp
12.6.3. Missing Dependencies
Suppose we construct the files locations.fpp
and a.fpp
, b.fpp
, and c.fpp
as described in the previous section, but then we temporarily remove b.fpp
.
Then the following facts are true:
-
fpp-depend
can see the direct dependency ofc.fpp
onb.fpp
. -
fpp-depend
can see thatb.fpp
does not exist. In this case we say thatb.fpp
is a missing dependency. -
fpp-depend
cannot see thatb.fpp
depends ona.fpp
(that dependency occurred in the missing file) and therefore it cannot see thatc.fpp
depends ona.fpp
.
In this case, by default, fpp-depend
does the best that it can:
it reports the dependency of c.fpp
on b.fpp
.
% fpp-depend locations.fpp c.fpp [path-prefix]/b.fpp
The philosophy behind fpp-depend
is to be as permissive and enabling as
possible.
It doesn’t assume that something is wrong because a dependency is missing:
for example, that dependency could be created later, as part of a code-generation
step.
However, you may want to know about missing dependencies, either to issue
a warning or error because something really is wrong, or to identify files to
generate.
To record missing dependencies, use the -m
option.
It takes as an argument the name of a file, and it writes missing dependencies
(if any)
to that file.
For example, the command
fpp-depend -m missing.txt locations.fpp c.fpp
writes the missing dependency [path-prefix]/b.fpp
to missing.txt
in
addition to writing
the dependency [path-prefix]/b.fpp
to standard output.
12.6.4. Included Files
Suppose file a.fpp
contains the
include specifier
include "b.fppi"
.
Then there are two options for computing the dependencies of a.fpp
:
-
a.fpp
does not depend onb.fppi
. -
a.fpp
does depend onb.fppi
.
Option 1 is what you want for assembling the input
to FPP analysis and translation tools such as fpp-check
.
In this case, when analyzing a.fpp
, the tool will resolve the include
specifier and include the contents of b.fppi
. So b.fppi
should
not be included as a separate input to the analysis.
On the other hand, suppose you are constructing a list of dependencies
for a build system such as the F Prime CMake system.
In this case, the build system doesn’t know anything about FPP include specifiers.
However, it needs to know that a.fpp
does depend on b.fppi
in the sense that
if b.fppi
is modified, then a.fpp
should be analyzed or translated again.
So in this case we want option 2.
By default, fpp-depend
provides option 1:
% echo 'include "b.fppi"' > a.fpp % rm -f b.fppi % touch b.fppi % fpp-depend a.fpp
To get option 2, use the -i
option to fpp-depend
.
It takes as an argument the name of a file, and it writes the included dependencies
(if any) to that file.
% echo 'include "b.fppi"' > a.fpp % rm -f b.fppi % touch b.fppi % fpp-depend -i included.txt a.fpp % cat included.txt [path-prefix]/b.fppi
In practice, you usually run fpp-depend
with the -i
file option
enabled.
Then option 1 corresponds to the output of the tool, and option 2 corresponds
to the output plus the contents of file.
12.6.5. Dependencies Between Build Modules
As discussed
above, the standard output of fpp-depend
reports transitive dependencies.
This is ordinarily what you want (a) for computing the input to an FPP
analysis tool and (b) for managing dependencies between files in a build.
For example, suppose that a.fpp
depends on b.fpp
and b.fpp
depends on c.fpp
.
When running analysis or code generation on a.fpp
, you will need to import
b.fpp
and c.fpp
(see the
next section
for an example).
Further, if you have a build rule for translating a.fpp
to XML, then you probably want to
re-run that rule if c.fpp
changes.
Therefore you need to report a dependency of a.fpp
on c.fpp
.
However, suppose that your build system divides the FPP files into groups
of files called build modules, and it manages dependencies between
the modules.
This is how the F Prime CMake system works.
In this case, assuming there is no direct dependency from a.fpp
to c.fpp
,
you may not want to report a dependency from a.fpp
to c.fpp
to the build system:
-
If
a.fpp
andc.fpp
are in the same build module, then they are in the same node of the dependency graph. So there is no dependency to manage. -
Otherwise, it suffices to report the file dependencies (a) from
a.fpp
tob.fpp
and (b) fromb.fpp
toc.fpp
. We can let the build system infer (a) the direct dependency from the module containinga.fpp
to the module containingb.fpp
; (b) the direct dependency from the module containingb.fpp
to the module containingc.fpp
; and (c) the transitive dependency from the module containinga.fpp
to the module containingc.fpp
.
To compute direct dependencies, run fpp-depend
with the option
-d
file.
The tool will write a list of direct dependencies to file.
Because direct dependencies are build dependencies,
any
included files
will appear in the list.
For this purpose, an included file is (a) any file included by an
input file to fpp-depend
; or (b) any file included
by such a file, and so forth.
When setting up a build based on build modules, you will typically
use fpp-depend
as follows, for each module M in the build:
-
Let S be the list of source files in M.
-
Run
fpp-depend -m missing.txt -d direct.txt
S and use the output as follows:-
The standard output reports the FPP source files to import when running FPP analysis tools on the module.
-
missing.txt
reports missing dependencies. -
direct.txt
reports direct dependencies. Use those to construct module dependencies for the build system.
-
You can also use the -g
option to identify generated files;
we discuss this option
below.
Note that we do not use the -i
option to fpp-depend
, because the relevant
included files are already present in direct.txt
.
12.6.6. Framework Dependencies
Certain FPP constructs imply dependencies on parts of the F Prime framework that may not be available on all platforms. For example, use of a guarded input port requires that an operating system provides a mutex lock.
To report framework dependencies, run fpp-depend
with the option
-f
file, where file is the name of an output file.
The currently recognized framework dependencies are as follows:
-
Fw_Comp
if the FPP model defines a passive component. -
Fw_CompQueued
if the model defines a queued or active component. -
Os
if the model defines a queued or active component or uses a guarded input port specifier.
Each dependency corresponds to a build module (i.e., a
statically compiled library) of the F Prime framework.
fpp-depend
writes the dependencies in the order that they must
be provided to the linker.
12.7. Locating Uses
Given a collection of files F and their dependencies D, you can generate the locations of the definitions appearing in D and used in F. This information is not necessary for doing analysis and translation — for that it is sufficient to know the file dependencies D. However, by reporting dependencies on individual definitions, this analysis provides an additional level of detail that may be helpful.
The tool for doing this analysis is called fpp-locate-uses
.
As an example, you can run fpp-locate-uses
to report the locations of all the
type definitions used in a port definition.
To locate uses, run fpp-locate-uses -i
D F, where D is a comma-separated
list and F is a space-separated list.
The -i
option stands for import: it says that the files D are to be read
for their
definitions, but not to be included in the results of the analysis.
For example, suppose a.fpp
defines constant a
, b.fpp
defines constant
b
,
and c.fpp
uses a
but not b
.
Then fpp-locate-uses -i a.fpp,b.fpp c.fpp
generates the output locate a at
"a.fpp"
Note that unlike in the case of
dependency analysis,
the inputs D and F to fpp-locate-uses
must form a complete model.
There must be no name used in D or in F that is not defined somewhere in
D or in F.
If D is the output of running fpp-depend
on F, and there are no
missing dependencies,
then this property should hold.
With fpp-locate-uses
, you can automatically derive the equivalent of the import
declarations that you construct by hand when writing F Prime XML.
For example, suppose you have specified a port P that uses a type T.
To specify P in F Prime XML, you would write an import
statement that
imports T into P. In FPP you don’t do this. Instead, you can do the following:
-
Run
fpp-locate-defs
to generate location specifiers L for all the type definitions. You can do this as needed, or you can do it once and check it in as part of the module that defines the types. -
Run
fpp-depend
on L and P to generate the dependencies D of P. -
Run
fpp-locate-uses -i
D P.
The result is a location specifier that gives the location of T. If you wish, you can check the result in as part of the source code that defines P. Doing this provide as a kind of "import statement," if that is desired to make the dependencies explicit in the code. Or you can just use the procedure given above to generate the "import statement" whenever desired, and see the dependencies that way.
As with fpp-locate-defs
, you can use -d
to specify a base directory
for the location specifiers.
13. Analyzing and Translating Models
The previous section explained how to specify an FPP model as a collection of files: how to divide a model into source files and how to compute the dependencies of one or more files on other files. This section explains the next step: how to perform analysis and translation on part or all of an FPP model, after specifying the model and computing its dependencies.
13.1. Checking Models
It is often useful to check a model for correctness, without
doing any translation.
The tool for checking models is called fpp-check
.
If you provide one or more files as arguments, fpp-check
will attempt to read those files.
For example:
% fpp-check file1.fpp file2.fpp
If there are no arguments, then fpp-check
reads from standard input.
For example:
% cat file1.fpp file2.fpp | fpp-check
If you run fpp-check
with no arguments on the command line,
it will block and wait for standard input.
This is useful for interactive sessions, where you want
to type simple model text into the console and immediately check it.
fpp-check
will keep reading input until (1) it encounters a parse error (more
on this below); or (2) you terminate the input with control-D (which must be
the first character in a line); or (3)
you terminate the program with control-C.
For larger models, the usual procedure for running fpp-check
is as follows:
-
Identify one or more files F that you want to check.
-
Compute the dependencies D of F.
-
Run
fpp-check
D F.
All the files D and all the files F are specified as file arguments, separated by spaces.
When you run fpp-check
, the following occurs:
-
The tool parses all the input files, recursively resolving include specifiers as it goes. If there are any parse errors or any problems resolving include files (for example, a missing file), it prints an error message to standard error and halts with nonzero status.
-
If parsing succeeds, then the tool runs semantic analysis. If everything checks out, the tool silently returns zero status. Otherwise it prints an error message to standard error and halts with nonzero status.
Checking for unconnected port instances: It is often useful to check for port instances that appear in a topology but that have no connections. For example, the following is a useful procedure for adding component instances and connections to a topology:
-
Add the component instances. In general this will introduce new port instances, which will initially be unconnected.
-
Check for unconnected port instances.
-
Add some or all of the connections identified in step 2.
-
Rerun steps 2 and 3 until there are no more missing connections, or you are certain that the missing connections are valid for your design.
To check for unconnected port instances (step 2 in the procedure above),
run fpp-check
with the option -u
file, where file is
the name of an output file.
fpp-check
will write the names of all unconnected port instances
to the file.
For this purpose, a port instance array is considered unconnected
if none of its port numbers are connected.
For example:
% fpp-check -u unconnected.txt
port P
passive component C {
sync input port pIn: P
output port pOut: [2] P
}
instance c: C base id 0x100
topology T1 {
instance c
}
topology T2 {
instance c
connections C {
c.pOut -> c.pIn
}
}
^D
% cat unconnected.txt
Topology T1:
c.pIn
c.pOut
In this example, component instance c
has the following port instances:
-
Two output port instances
c.pOut[0]
andc.pOut[1]
. -
One input port instance
c.pIn
.
Topology T1
uses instance c
and does not connect any port number of
c.pOut
or c.pIn
.
So the output written to unconnected.txt
reports that fact.
On the other hand, in topology T2
, both c.pOut
and c.pIn
are considered connected (so not reported as unconnected)
even though c.Out
has two ports and only one of them is connected.
13.2. Generating XML
There are two reasons for generating XML from FPP:
-
It is needed to support the F Prime GDS.
-
It is needed to support C++ code generation, until we fully replace the XML-to-C++ code generation with FPP-to-C++ code generation (see the next section).
XML file names: The table XML File Names shows how FPP definitions are translated to F Prime XML files.
| FPP Definition | F Prime XML File |
|---|---|
Array A outside any component |
A |
Array A in component C |
C |
Enum E outside any component |
E |
Enum E in component C |
C |
Struct S outside any component |
S |
Struct S in component C |
C |
Port P |
P |
Component C |
C |
Topology T |
T |
For example, consider the FPP array definition
array A = [3] U32
Outside of any component definition, this definition is translated to
an XML array with name A
defined in a file AArrayAi.xml
.
Inside the definition of component C
, it is translated to
an XML array with name C_A
defined in the file C_AArrayAi.xml
.
In either case the namespace in the XML file is given by the enclosing
FPP modules, if any.
For example, the following code
module M {
array A = [3] U32
}
becomes an array with name A
and namespace M
in file
AArrayAi.xml
.
Tool name: The tool for translating FPP definitions to XML files
is called fpp-to-xml
.
Procedure:
The usual procedure for running fpp-to-xml
is as follows:
-
Identify one or more files F that you want to translate.
-
Compute the dependencies D of F.
-
If D is empty, then run
fpp-to-xml
F. -
Otherwise run
fpp-to-xml -i
D1,
…,
Dn F, where Di are the names of the dependencies.
For example, suppose you want to generate XML for the definitions in c.fpp
,
If c.fpp
has no dependencies, then run
% fpp-to-xml c.fpp
On the other hand, if c.fpp
depends on a.fpp
and b.fpp
, then run
% fpp-to-xml -i a.fpp,b.fpp c.fpp
Notice that you provide the dependencies as a comma-separated list of
arguments to the option -i
.
-i
stands for "import."
This option tells the tool that you want to read the files in D for their symbols,
but you don’t want to translate them.
Only the files F provided as arguments are translated.
Tool behavior: When you run fpp-to-xml
, the following occurs:
-
The tool runs the same analysis as for
fpp-check
. If there is any problem, the tool prints an error message to standard error and halts with nonzero status. -
If the analysis succeeds, then the tool generates XML files, one for each definition appearing in F, with names as shown in the table above. The files are written to the current directory.
Generated import paths:
When one FPP definition A
depends on another definition B
,
the generated XML file for A
contains an XML node that imports
the generated XML file for B
.
The tool constructs the import path from the
location of the imported FPP symbol.
For example, suppose the file [path prefix]/A/A.fpp
contains the following
definition, where [path prefix]
represents the path prefix of directory
A
starting from the root of the file system:
array A = [3] B
And suppose the file [path prefix]/B/B.fpp
contains the following definition:
array B = [3] U32
If you run this command in directory [path prefix]/A
% fpp-to-xml -i ../B/B.fpp A.fpp
then in that directory the tool will generate a file AArrayAi.xml
containing
the following line:
<import_array_type>[path prefix]/B/BArrayAi.xml</import_array_type>
Removing path prefixes: Usually when generating XML we don’t want to include the system-specific part of the path prefix. Instead, we want the path to be specified relative to some known place, for example the root of the F Prime repository or a project repository.
To remove the prefix prefix from generated paths, use the option
-p
prefix .
To continue the previous example, running
fpp-to-xml -i ../B/B.fpp -p [path prefix] A.fpp
generates a file AArrayAi.xml
containing the line
<import_array_type>B/BArrayAi.xml</import_array_type>
Notice that the path prefix [path prefix]/
has been removed.
To specify multiple prefixes, separate them with commas:
fpp-to-xml -p prefix1,prefix2, ...
For each generated path, the tool will delete the longest prefix that matches a prefix in the list.
More options: The following additional options are available
when running fpp-to-xml
:
-
-d
dir : Use dir instead of the current directory as the output directory for writing files. For example,fpp-to-xml -d xml ...
writes output files to the directory
xml
(which must already exist). -
-n
file : Write the names of the generated XML files to file. This is useful for collecting autocoder build dependencies. -
-s
size : Specify a default string size. For example,fpp-to-xml -s 40 ...
FPP allows string types with no specified size, and F Prime XML does not. So when generating code we need to provide a default size to use when FPP doesn’t specify the size. If you don’t specify the
-s
option, then the tool uses an automatic default of 80.
Standard input: Instead of providing named files as arguments,
you can provide FPP source on standard input, as described
for fpp-check
.
XML limitations: The XML translation has several limitations. For more information, see the text marked XML limitations elsewhere in this manual.
13.3. Generating C Plus Plus
To date we have implemented all of the FPP to C++ translation except for the generation of component unit test harnesses.
Tool name: The tool for translating FPP to C++ is called
fpp-to-cpp
.
Procedure:
The usual procedure for running fpp-to-cpp
is as follows:
-
Identify one or more files F that you want to translate.
-
Compute the dependencies D of F.
-
If D is empty, then run
fpp-to-cpp
F. -
Otherwise run
fpp-to-cpp -i
D1,
…,
Dn F, where Di are the names of the dependencies.
Except for the tool name, this procedure is identical to the one given for generating XML. See that section for examples of the procedure.
Input: As with the tools described above, you can provide input to
fpp-to-cpp
either through named files or through standard input.
13.3.1. Constant Definitions
fpp-to-cpp
extracts constant definitions
from the source files F.
It generates files FppConstantsAc.hpp
and FppConstantsAc.cpp
containing C++ translations of the constants.
By including and/or linking against these files,
you can use constants defined in the FPP model
in your FSW implementation code.
To keep things simple, only numeric, string, and Boolean constants are translated; struct and array constants are ignored. For example, the following constant is not translated, because it is an array:
constant a = [ 1, 2, 3 ]
To translate array constants, you must expand them to values that are translated, like this:
constant a0 = 1
constant a1 = 2
constant a2 = 3
constant a = [ a0, a1, a2 ]
Constants are translated as follows:
-
Integer constants become enumeration constants.
-
Floating-point constants become
const
floating-point variables. -
bool
point constants becomeconst bool
variables. -
string
constants becomeconst char* const
variables initialized with string literals.
As an example, try this:
% fpp-to-cpp @ Constant a constant a = 1 @ Constant b constant b = 2.0 @ Constant c constant c = true @ Constant d constant d = "abcd" ^D
You should see files FppConstantsAc.hpp
and FppConstantsAc.cpp
in the current directory.
Examine them to confirm your understanding of how the translation
works.
Notice how the FPP annotations are translated to comments.
(We also remarked on this in the section on
writing annotations.)
Constants defined inside components:
As noted in the section on
defining components,
when you define a constant c
inside a component C
,
the name of the corresponding constant in the generated C++
code is C_c
.
As an example, run the following code through fpp-to-cpp
and examine the results:
passive component C {
constant c = 0
}
Generated header paths:
The option -p
path-prefixes removes the longest of one or more
path prefixes from any generated header paths (for example,
the path to FppConstants.hpp
that is included in FppConstants.cpp
).
To specify multiple prefixes, separate them with commas (and no spaces).
This is similar to the -p
option for
fpp-to-xml
.
The include guard prefix: By default, the include guard
for FppConstantsAc.hpp
is guard-prefix _FppConstantsAc_HPP
,
where guard-prefix is the absolute path of the current
directory, after replacing non-identifier characters with underscores.
For example, if the current directory is /home/user
, then
the guard prefix is _home_user
, and the include guard is
_home_user_FppConstantsAc_HPP
.
The -p
option, if present, is applied to the guard
prefix.
For example, if you run fpp-to-cpp -p $PWD …
then
the guard prefix will be empty.
In this case, the guard is FppConstantsAc_HPP
.
If you wish to use a different prefix entirely, use the option
-g
guard-prefix.
For example, if you run fpp-to-cpp -g Commands …
,
then the include guard will be Commands_FppConstantsAc_HPP
.
More options: The following additional options are available
when running fpp-to-cpp
:
-
-d
dir : Use dir instead of the current directory as the output directory for writing files. This is similar to the-d
option forfpp-to-xml
. -
-n
file : Write the names of the generated XML files to file. This is similar to the-n
option forfpp-to-xml
.
13.3.2. Topology Definitions
fpp-to-cpp
also extracts topology definitions
from the source files.
For each topology T defined in the source files, fpp-to-cpp
writes files T TopologyAc.hpp
and T TopologyAc.cpp
.
These files define two public functions:
setup
for setting up the topology, and
teardown
, for tearing down the topology.
The function definitions come from the definition of T and
from the
init specifiers
for the component instances used in T.
You can call these functions from a handwritten main
function.
We will explain how to write this main
function in the
section on
implementing deployments.
As an example, you can do the following:
-
On the command line, run
fpp-to-cpp -p $PWD
. -
Copy the text of the simple topology example and paste it into the terminal.
-
Press return, control-D, and return.
-
Examine the generated files
SimpleTopologyAc.hpp
andSimpleTopologyAc.cpp
.
You can examine the files RefTopologyAc.hpp
and RefTopologyAc.cpp
in the F Prime repository.
Currently these files are checked in at Ref/Top
.
Once we have integrated FPP with CMake, these files will be auto-generated
by CMake and will be located at Ref/build-fprime-automatic-native/F-Prime/Ref/Top
.
Options:
When translating topologies,
the -d
, -n
, and -p
options work in the same way as for
translating constant definitions.
The -g
option is ignored, because
the include guard prefix comes from the name of the topology.
13.3.3. Types, Ports, and Components
C++ code generation for types, ports, and components works similarly to the corresponding code generation from XML. This code generation is described in the F Prime User’s Guide.
13.3.4. Component Implementation and Unit Test Code
fpp-to-cpp
has two more options -t
and -u
.
These options are for generating component "templates" or
partial implementations and for generating unit test code.
Here we cover the mechanics of using these options.
For more information on implementing and testing components in F Prime, see
the F
Prime User’s Guide.
Generating implementation templates:
When you run fpp-to-cpp
with option -t
and without option -u
,
it generates a partial implementation for
each component definition C in the input.
The generated files are called C .template.hpp
and C .template.cpp
.
You can fill in the blanks in these files to provide the concrete
implementation of C.
Generating unit test harness code:
When you run fpp-to-cpp
with option -u
and without option -t
,
it generates all the code necessary for building and running unit tests.
This code consists of (1) the autocoded C++ for constants, ports,
types, components, and
topologies described in the previous sections; and (2)
support code for testing each component definition C in the input.
The unit test support code resides in the following files:
-
C
TesterBase.hpp
and CTesterBase.cpp
. These files define a class CTesterBase
. This class contains helper code for unit testing C, for example an input port and history corresponding to each output port of C. -
C
GTestBase.hpp
and CGTestBase.cpp
. These files define a class CGTestBase
derived from C. This class uses the Google Test framework to provide additional helper code. It is factored into a separate class so that you can use CTesterBase
without CGTestBase
if you wish.
Generating unit test templates:
When you run fpp-to-cpp
with both the -u
and the -t
options,
it generates a template or partial implementation of the unit tests
for each component C in the input.
The generated code consists of the following files:
-
C
Tester.hpp
and CTester.cpp
. These files partially define a class CTester
that is derived from CGTestBase
. You can fill in the partial definition to provide unit tests for C. If you are not using Google Test, then you can modify CTester
so that it is derived from CTesterBase
. -
C
TesterHelpers.cpp
. This file provides helper functions called by the functions defined inTester.cpp
. These functions are factored into a separate file so that you can redefine them if you wish. To redefine them, omit CTesterHelpers.cpp
from your F Prime unit test build. -
C
TestMain.cpp
. This file provides a minimal main function for unit testing, including a sample test. You can add your top-level test code to this file.
13.4. Identifying Generated Files
As discussed in the previous section, the -n
option
of fpp-to-xml
and fpp-to-cpp
lets you collect the names of
files generated from an FPP model as those files are generated.
However, sometimes you need to know the names of the generated
files up front.
For example, the CMake build tool writes out a Makefile rule
for every generated file, and it does this as an initial step
before generating any files.
There are two ways to collect the names of generated files:
using fpp-filenames
and using fpp-depend
.
13.4.1. Using fpp-filenames
Like fpp-check
, fpp-filenames
reads the files
provided as command-line arguments if there are any;
otherwise it reads from standard input.
The FPP source presented to fpp-filenames
need not be a complete
model (i.e., it may contain undefined symbols).
When run with no options, tool parses the FPP source that you give it.
It identifies all definitions in the source that would cause
XML files to be generated when running fpp-to-xml
or would cause C++ files to be generated when running
fpp-to-cpp
.
Then it writes the names of those files to standard output.
For example:
% fpp-filenames array A = [3] U32 ^D AArrayAi.xml
% fpp-filenames constant a = 0 ^D FppConstantsAc.cpp FppConstantsAc.hpp
You can run fpp-filenames
with the -u
option, with the -t
option,
or with both options.
In these cases fpp-filenames
writes out the names of
the files that would be generated by running fpp-to-cpp
with the
corresponding options.
For example:
% fpp-filenames -t array A = [3] U32 passive component C {} ^D C.template.cpp C.template.hpp
% fpp-filenames -u array A = [3] U32 passive component C {} ^D array A = [3] U32 passive component C {} AArrayAc.cpp AArrayAc.hpp AArrayAi.xml CComponentAc.cpp CComponentAc.hpp CComponentAi.xml CGTestBase.cpp CGTestBase.hpp CTesterBase.cpp CTesterBase.hpp
% fpp-filenames -u -t array A = [3] U32 passive component C {} ^D CTestMain.cpp CTester.cpp CTester.hpp CTesterHelpers.cpp
13.4.2. Using fpp-depend
Alternatively, you can use
fpp-depend
to write out the names of generated files during dependency analysis.
The output is the same as for fpp-filenames
, but this way you can
run one tool (fpp-depend
) instead of two (fpp-depend
and
fpp-filenames
).
Running one tool may help your build go faster.
fpp-depend
provides the following options:
-g
file: Write the names of the generated autocode files
to the file file.
-t
file: Write the names of the generated component implementation
template files to file.
For example:
% fpp-depend -g generated.txt array A = [3] U32 passive component C {} ^D % cat generated.txt AArrayAc.cpp AArrayAc.hpp AArrayAi.xml CComponentAc.cpp CComponentAc.hpp CComponentAi.xml
% fpp-depend -t template.txt array A = [3] U32 passive component C {} ^D % cat template.txt C.template.cpp C.template.hpp
You can also specify these options together with the -u
option.
In this case fpp-depend
writes the names of autocoded
or template unit test files.
For example:
% fpp-depend -u -g generated.txt array A = [3] U32 passive component C {} ^D % cat generated.txt AArrayAc.cpp AArrayAc.hpp AArrayAi.xml CComponentAc.cpp CComponentAc.hpp CComponentAi.xml CGTestBase.cpp CGTestBase.hpp CTesterBase.cpp CTesterBase.hpp
% fpp-depend -u -t template.txt array A = [3] U32 passive component C {} ^D % cat template.txt CTestMain.cpp CTester.cpp CTester.hpp CTesterHelpers.cpp
13.5. Translating XML to FPP
The FPP tool suite provides a capability to translate F Prime XML files to FPP. Its purpose is to address the following case:
-
You have already developed an F Prime model in XML.
-
You wish to translate the model to FPP in order to use FPP as the source language going forward.
The XML-to-FPP translation is designed to do most of the work in translating an XML model into FPP. As discussed below, some manual effort will still be required, because the FPP and XML representations are not identical. The good news is that this is a one-time effort: you can do it once and use the FPP version thereafter.
Tool name: The tool for translating XML to FPP is called
fpp-from-xml
.
Tool behavior:
Unlike the tools described above, fpp-from-xml
does not read
from standard input.
To use it, you must name one or more XML files on the command line.
The reason is that the XML parsing library used by the tool requires
named files.
The tool reads the XML files you name, translates them, and
writes the result to standard output.
As an example, try this:
% fpp-to-xml struct S { x: U32, y: F32 } ^D % fpp-from-xml SSerializableAi.xml struct S { x: U32 y: F32 }
Default values: There are two issues to note in connection with translating default values.
First, in FPP, every definition has a default value, but
the default value need not be given explicitly:
if you provide no explicit default value, then an implicit default is used.
By contrast, in F Prime XML, (1) you must supply default values for array
elements, and (2) you may supply default values for struct members
or enumerations.
To keep the translation simple, if default values are present in the XML
representation, then fpp-from-xml
translates them to explicit values,
even if they could be made implicit.
Here is an example:
% fpp-to-xml array A = [3] U32 ^D % fpp-from-xml AArrayAi.xml array A = [3] U32 default [ 0 0 0 ]
Notice that the implicit default value [ 0, 0, 0 ]
becomes
explicit when translating to XML and back to FPP.
Second, to keep the translation simple, only literal numeric values,
literal string values, literal Boolean values, and C++ qualified identifiers
(e.g., a
or A::B
) are translated.
Other values (e.g., values specified with C++ constructor calls), are not translated.
The reason is that the types of these values cannot be easily inferred from the
XML representation.
When a default value is not translated, the translator inserts an annotation
identifying what was not translated, so that you can do the translation
yourself.
For example, try this:
% fpp-to-xml type T array A = [3] T ^D % fpp-from-xml AArrayAi.xml @ FPP from XML: could not translate array value [ T(), T(), T() ] array A = [3] T
The tool cannot translate the value T()
.
So it adds an annotation stating that.
In this case, T()
is the default value associated with the
abstract type T
, so using the implicit default is correct.
So in this case, just delete the annotation.
Here is another example:
% fpp-to-xml array A = [2] U32 array B = [2] A default [ [ 1, 2 ], [ 3, 4 ] ] ^D % fpp-from-xml BArrayAi.xml @ FPP from XML: could not translate array value [ A(1, 2), A(3, 4) ] array B = [2] A
Here the XML representation of the array values [ 1, 2 ]
and [ 3, 4 ]
uses the C++ constructor calls A(1, 2)
and A(3, 4)
.
When translating BArrayAi.xml
, fpp-from-xml
doesn’t know how to translate
those values, because it doesn’t have any information about the type A
.
So it omits the FPP default array value and reports the XML default element
values in the annotation.
That way, you can manually construct a default value in FPP.
Inline enum definitions: The following F Prime XML formats may include inline enum definitions:
-
In the Serializable XML format, enumerations may appear as member types.
-
In the Port XML format, enumerations may appear as the types of arguments or as the return type.
-
In the XML formats for commands and for events, enumerations may appear as the types of arguments.
-
In the XML formats for telemetry channels and for parameters, enumerations may appear as the types of data elements.
In each case, the enumerated constants are specified as part of the definition of the member, argument, return type, etc.
FPP does not represent these inline enum definitions directly.
In FPP, enum definitions are always named, so they can be reused.
Therefore, when translating an F Prime XML file that contains inline enum
definitions, fpp-to-xml
does the following: (1) translate
each inline definition to a named FPP enum; and (2) use the corresponding named
types in the translated FPP struct or port.
For example, here is an F Prime Serializable XML type
N::S1
containing a member m
whose type is an enum
E
with three enumerated constants A
, B
, and C
:
cat > S1SerializableAi.xml <serializable namespace="N" name="S1"> <members> <member name="m" type="ENUM"> <enum name="E"> <item name="A"/> <item name="B"/> <item name="C"/> </enum> </member> </members> </serializable> ^D
(The formula cat >
file lets us enter input to
the console and have it written to file.)
Running fpp-from-xml
on this file yields the following:
% fpp-from-xml S1SerializableAi.xml module N { enum E { A = 0 B = 1 C = 2 } struct S1 { m: E } }
Notice the following:
-
The tool translates namespace
N
in XML to moduleN
in FPP. -
The tool translates Serializable type
S1
in namespaceN
to struct typeS1
in moduleN
. -
The tool generates an enum type
N.E
to represent the type of memberm
of structN.S1
. -
The tool assigns member
m
of structN.S1
the typeN.E
.
If you wish to translate an XML model to FPP, and that model contains inline enums, then we suggest the following procedure:
-
Run
fpp-from-xml
on the XML model as described above to convert all of the inline definitions to named XML types. -
Refactor your XML model and FSW implementation to use the XML types generated in step 1. This may require changes to your C++ code. For example, inline XML enums and XML enum types generate slightly different code. Therefore, you will need to revise any uses of the old inline enums to match the new format. Do this step incrementally, making sure that all your regression tests pass at each step.
-
Once you have the XML model in the required form, run
fpp-from-xml
again to generate an FPP model M. If you have done step 2 correctly, then you should be able to replace your handwritten XML with the result of runningfpp-to-xml
on M.
Format strings:
fpp-from-xml
translates XML format strings to FPP
format strings, if it can.
Here is an example:
% fpp-to-xml array A = [3] F32 format "{f}" ^D
This will generate a file AArrayAi.xml
containing the line
<format>%f</format>
which is the XML representation of the format.
Now try this:
% fpp-from-xml AArrayAi.xml array A = [3] F32 default [ 0.0 0.0 0.0 ] format "{f}"
The XML format %f
is translated back to the FPP format {f}
.
If the tool cannot translate the format, it will insert an annotation
stating that. For example, %q
is not a format recognized by
FPP, so a format containing this string won’t be translated:
% cat > AArrayAi.xml <array name="A"> <type>F32</type> <size>1</size> <format>%q</format> <default> <value>0.0</value> </default> </array> ^D % fpp-from-xml AArrayAi.xml @ FPP from XML: could not translate format string "%q" array A = [1] F32 default [ 0.0 ]
Import directives:
XML directives that import symbols (such as import_port_type
)
are ignored in the translation.
These directives represent dependencies between XML files, which
become dependencies between FPP source files in the FPP translation.
Once the XML-to-FPP translation is done, you can handle these
dependencies in the ordinary way for FPP, as discussed in the
section on specifying models as files.
XML directives that import XML dictionaries are translated
to
include specifiers.
For example, suppose that CComponentAi.xml
defines component C
and contains the directive
<import_dictionary>Commands.xml</import_dictionary>
Running fpp-from-xml
on CComponentAi.xml
produces an
FPP definition of a component C
; the component definition
contains the include specifier
include "Commands.fppi"
Separately, you can use fpp-to-xml
to translate Commands.xml
to Commands.fppi
.
13.6. Formatting FPP Source
The tool fpp-format
accepts FPP source files as input
and rewrites them as formatted output.
You can use this tool to put your source files into
a standard form.
For example, try this:
% fpp-format array A = [3] U32 default [ 1, 2, 3 ] ^D array A = [3] U32 default [ 1 2 3 ]
fpp-format
has reformatted the default value so that each array
element is on its own line.
By default, fpp-format
does not resolve include specifiers.
For example:
% echo 'constant a = 0' > a.fppi % fpp-format include "a.fppi" ^D include "a.fppi"
The -i
option causes fpp-format
to resolve include specifiers.
For example:
% echo 'constant a = 0' > a.fpp % fpp-format -i include "a.fppi" ^D constant a = 0
fpp-format
has one big limitation: it goes through
the FPP parser, so it deletes all
comments
from the program
(annotations
are preserved).
To preserve comments on their own lines that precede
annotatable elements, you can run this script:
#!/bin/sh
sed 's/^\( *\)#/\1@ #/' | fpp-format $@ | sed 's/^\( *\)@ #/\1#/'
It converts comments to annotations, runs fpp-format
, and converts the
annotations back to comments.
13.7. Visualizing Topologies
When FPP translates a topology T to XML, as discussed in the section on
generating XML,
it adds special comments that can be read by a companion tool called
fprime-layout
.
fprime-layout
reads a topology XML file and produces a
topology visualization, i.e., a graphical rendering of the topology
in which the component instances are shapes, the ports are smaller shapes,
and the connections are arrows between the ports.
fprime-layout
produces one visualization for each
connection graph in the
topology.
Topology visualization is an important part of the FPP work flow:
-
It provides a graphical representation of the instances and connections in each connection graph. This graphical representation is a useful complement to the textual representation provided by the FPP source.
-
It makes explicit information that is only implicit in the FPP source, e.g., the auto-generated port numbers of the connections and the auto-generated connections of the pattern graph specifiers.
Using fprime-layout
, you can do the following:
-
Extract the connection graphs from a topology XML file.
-
Render the connection graphs as EPS (Encapsulated PostScript), generating one EPS file for each connection graph.
-
Generate a set of layouts, one for each connection graph, and view the layouts in a browser.
See the fprime-layout
repository for more details.
13.8. Generating JSON
FPP provides a tool called fpp-to-json
for converting FPP models to
JavaScript Object Notation (JSON) format.
Using this tool, you can import FPP models into programs written
in any language that has a library for reading JSON, e.g., JavaScript,
TypeScript, or Python.
Generating and importing JSON may be convenient if you need to develop
a simple analysis or translation tool for FPP models, and you don’t
want to develop the tool in Scala.
For more complex tools, we recommend that you develop in Scala
against the FPP compiler data structures.
Procedure:
The usual procedure for running fpp-to-json
is as follows:
-
Identify one or more files F that you want to analyze.
-
Compute the dependencies D of F.
-
Run
fpp-to-json
D F. Note that D may be empty.
If you are using fpp-to-json
with the -s
option (see below),
then you can run fpp-to-json
F, without computing dependencies.
Tool behavior: When you run fpp-to-json
, the tool checks the
syntax and semantics of the source model, reporting any errors that occur.
If everything checks out, it generates three files:
-
fpp-ast.json
: The abstract syntax tree (AST). This is a tree data structure that represents the source syntax. It contains AST nodes, each of which has a unique identifier. -
fpp-loc-map.json
: The location map. This object is a map from AST node IDs to the source locations (file, line number, and column number) of the corresponding AST nodes. -
fpp-analysis.json
: The Analysis data structure. This object contains semantic information inferred from the source model, e.g., the types of all the expressions and the constant values of all the numeric expressions. Only output data is included in the JSON; temporary data structures used during the analysis algorithm are omitted. For more information on the Analysis data structure, see the FPP wiki.
JSON format: To understand this subsection, you need to know a little bit about case classes in Scala. For a primer, see this wiki page.
The JSON translation uses a Scala library called Circe. In general the translation follows a set of standard rules, so the output format can be easily inferred from the types of the data structures in the FPP source code:
-
A Scala case class
C
is translated as follows, unless it extends a sealed trait (see below). A valuev
of typeC
becomes a JSON dictionary with the field names as keys and the field values as their values. For example a valueC(1,"hello")
of typecase class C(n: Int, s: String)
becomes a JSON value{ "n": 1, "s": "String" }
. -
A Scala case class
C
that extends a sealed traitT
represents a named variant of typeT
. In this case a valuev
of typeC
is wrapped in a dictionary with one key (the variant nameC
) and one value (the valuev
). For example, a valueC(1)
of typecase class C(n: Int) extends T
becomes a JSON value{ "C" : { "n" : 1 } }
, while a valueD("hello")
of typecase class D(s: String) extends T
becomes a JSON value{ "D" : { "s" : "hello" } }
. In this way each variant is labeled with the variant name. -
A Scala list becomes a JSON array, and a Scala map becomes a JSON dictionary.
There are a few exceptions, either because the standard translation does not work, or because we need special behavior for important cases:
-
We streamline the translation of the Scala Option type, translating
Some(v)
as{ "Some" : v }
andNone
as"None"
. -
In the AST, we translate the type AstNode as if it were a variant type, i.e., we translate
AstNode([data], [id])
to"AstNode" : { "data" : [data], "id" : [id] } }
. TheAstNode
keys identify the AstNode objects. -
In the AST, to reduce clutter we skip over the
node
field of module, component, and topology member lists. This field is an artifact of the way the Scala code is written; deleting it does not lose information. -
In the Analysis data structure, to avoid repetition, we translate AstNode values as
{ "astNodeId" : [node id] }
, eliminating the data field of the node. We also omit annotations from annotated AST nodes. The data fields and the annotations can be looked up in the AST, by searching for the node ID. -
When translating an FPP symbol (i.e., a reference to a definition), we provide the information in the Symbol trait (the node ID and the unqualified name). All symbols extend this trait. We omit the AST node information stored in the concrete symbol. This information can be looked up with the AST node ID.
-
When translating a component instance value, we replace the component stored in the value with the corresponding AST node ID.
-
When the keys of a Scala map cannot easily be converted to strings, we convert the map to a list of pairs, represented as an array of JSON arrays. For example, this is how we translate the PortNumberMap in the Analysis data structure, which maps Connection objects to integers.
Options: The following options are available
when running fpp-to-xml
:
-
-d
dir : Similar to the corresponding option offpp-to-xml
. -
-s
: Analyze syntax only: With this option,fpp-to-json
generates the AST and the location map only; it doesn’t generate the Analysis data structure. Because semantic analysis is not run, you don’t have to present a complete or syntactically correct FPP model to the tool.
14. Writing C Plus Plus Implementations
When constructing an F Prime deployment in C++, there are generally four kinds of implementations you have to write:
-
Implementations of abstract types. These are types that are named in the FPP model but are defined directly in C++.
-
Implementations of components.
-
Implementations of any libraries used by the component implementations, e.g., algorithm libraries or hardware device driver libraries.
-
A top-level implementation including a
main
function for running the FSW application.
Implementing a component involves filling out the API provided by the C++ component base class. This process is covered in detail in the F Prime user’s guide; we won’t cover it further here. Similarly, implementing libraries is unrelated to FPP, so we won’t cover it in this manual. Here we focus on items (1) and (4): implementing abstract types and implementing deployments.
14.1. Implementing Abstract Types
Except for a few built-in types (see below), when translating to XML and then C++, an
abstract type definition
represents a C++ class that you write directly in C++.
When you use an abstract type T in an FPP definition D (for example, as the
member type of an array definition)
and you translate D to C++, then the generated C++ for D contains an
include
directive that includes a header file for T.
As an example, try this:
% fpp-to-cpp -p $PWD type T array A = [3] T ^D
Notice that we used the option -p $PWD
.
This is to make the generated include path relative to the current directory.
Now run
% cat AArrayAc.hpp
You should see the following line in the generated C++:
#include "T.hpp"
This line says that in order to compile AArrayAc.cpp
,
a header file T.hpp
must exist in the current directory.
It is up to you to provide that header file.
When implementing an abstract type T
in C++, you must define
a class that extends Fw::Serializable
from the F Prime framework.
Your class definition must include the following:
-
An implementation of the virtual function
Fw::SerializeStatus T::serialize(Fw::SerializeBufferBase&) const
that specifies how to serialize a class instance (i.e., convert a class instance to a byte string).
-
An implementation of the function
Fw::SerializeStatus T::deserialize(Fw::SerializeBufferBase&)
that specifies how to deserialize a class instance (i.e., reconstruct a class instance from a byte string).
-
A constant
T::SERIALIZED_SIZE
that specifies the size in bytes of a byte string serialized from the class. -
A zero-argument constructor
T()
. -
An overloaded equality operator
bool operator==(const T& that) const;
Here is a minimal complete implementation of an abstract type T
.
It has one member variable x
of type U32
and no methods other than
those required by F Prime.
We have made T
a C++ struct (rather than a class) so that
all members are public by default.
// A minimal implementation of abstract type T #ifndef T_HPP #define T_HPP // Include Fw/Types/Serializable.fpp from the F Prime framework #include "Fw/Types/Serializable.hpp" struct T : public Fw::Serializable { // Extend Fw::Serializable // Define some shorthand for F Prime types typedef Fw::SerializeStatus SS; typedef Fw::SerializeBufferBase B; // Define the constant SERIALIZED_SIZE enum Constants { SERIALIZED_SIZE = sizeof(U32) }; // Provide a zero-argument constructor T() : x(0) { } // Define a comparison operator bool operator==(const T& that) const { return this->x == that.x; } // Define the virtual serialize method SS serialize(B& b) const { return b.serialize(x); } // Define the virtual deserialize method SS deserialize(B& b) { return b.deserialize(x); } // Provide some data U32 x; }; #endif
Built-in types: The following types are abstract in the FPP model but are known to the C++ translator:
FwBuffSizeType FwChanIdType FwEnumStoreType FwEventIdType FwIndexType FwOpcodeType FwPacketDescriptorType FwPrmIdType FwSizeType FwTimeBaseStoreType FwTimeContextStoreType FwTlmPacketizeIdType NATIVE_INT_TYPE NATIVE_UINT_TYPE POINTER_CAST
Each of these types is an alias for a C++ integer type, and each has default value zero.
The F Prime framework provides the C++ definitions
for these types.
It also provides the corresponding abstract type definitions
in the FPP model; for a typical F Prime project, these
definitions are located at config/FpConfig.fpp
.
You don’t have to define header files for these types.
14.2. Implementing Deployments
At the highest level of an F Prime implementation, you write two units of C++ code:
-
Application-specific definitions visible both to the
main
function and to the auto-generated topology code. -
The
main
function.
We describe each of these code units below.
14.2.1. Application-Specific Definitions
As discussed in the section on
generating C++ topology definitions, when you translate an FPP
topology T to C++, the result goes into files
T TopologyAc.hpp
and T TopologyAc.cpp
.
The generated file T TopologyAc.hpp
includes a file
T TopologyDefs.hpp
.
The purpose of this file inclusion is as follows:
-
T
TopologyDefs.hpp
is not auto-generated. You must write it by hand as part of your C++ implementation. -
Because T
TopologyAc.cpp
includes TTopologyAc.hpp
and TTopologyAc.hpp
includes TTopologyDefs.hpp
, the handwritten definitions in TTopologyDefs.hpp
are visible to the auto-generated code in TTopologyAc.hpp
andTopologyAc.cpp
. -
You can also include T
TopologyDefs.hpp
in your main function (described in the next section) to make its definitions visible there. That waymain
and the auto-generated topology code can share these custom definitions.
T TopologyDefs.hpp
must be located in the same directory where the topology T is defined.
When writing the file T TopologyDefs.hpp
, you should
follow the description given below.
Topology state:
T TopologyDefs.hpp
must define a type
TopologyState
in the C++ namespace
corresponding to the FPP module where the topology T is defined.
For example, in SystemReference/Top/topology.fpp
in the
F Prime system reference deployment, the FPP topology SystemReference
is defined in the FPP
module SystemReference
, and so in
SystemReference/Top/SystemReferenceTopologyDefs.hpp
, the type TopologyState
is defined in the namespace SystemReference
.
TopologyState
may be any type.
Usually it is a struct or class.
The C++ code generated by FPP passes a value state
of type TopologyState
into
each of the functions for setting up and tearing down topologies.
You can read this value in the code associated with your
init specifiers.
In the F Prime system reference example, TopologyState
is a struct with two member variables: a C-style string
hostName
that stores a host name and a U32
value portNumber
that stores a port number.
The main function defined in Main.cpp
parses the command-line
arguments to the application, uses the result to create an object
state
of type TopologyState
, and passes the state
object
into the functions for setting up and tearing down the topology.
The startTasks
phase for the comDriver
instance uses the state
object in the following way:
phase Fpp.ToCpp.Phases.startTasks """
// Initialize socket server if and only if there is a valid specification
if (state.hostName != nullptr && state.portNumber != 0) {
Os::TaskString name("ReceiveTask");
// Uplink is configured for receive so a socket task is started
comDriver.configure(state.hostName, state.portNumber);
comDriver.startSocketTask(
name,
true,
ConfigConstants::comDriver::PRIORITY,
ConfigConstants::comDriver::STACK_SIZE
);
}
"""
In this code snippet, the expressions state.hostName
and state.portNumber
refer to the hostName
and portNumber
member variables of the
state object passed in from the main function.
The state
object is passed in to the setup and teardown functions
via const
reference.
Therefore, you may read, but not write, the state
object in the
code associated with the init specifiers.
Health ping entries:
If your topology uses an instance of the standard component Svc::Health
for
monitoring
the health of components with threads, then T TopologyDefs.hpp
must define the health ping entries used by the health component instance.
The health ping entries specify the time in seconds to wait for the
receipt of a health ping before declaring a timeout.
For each component being monitored, there are two timeout intervals:
a warning interval and a fatal interval.
If the warning interval passes without a health ping, then a warning event occurs.
If the fatal interval passes without a health ping, then a fatal event occurs.
You must specify the health ping entries in the namespace corresponding to the FPP module where T is defined. To specify the health ping entries, you do the following:
-
Open a namespace
PingEntries
. -
In that namespace, open a namespace corresponding to the name of each component instance with health ping ports.
-
Inside namespace in item 2, define a C++ enumeration with the following constants
WARN
andFATAL
. SetWARN
equal to the warning interval for the enclosing component instance. SetFATAL
equal to the fatal interval.
For example, here are the health ping entries from
SystemReference/Top/SystemReferenceTopologyDefs.hpp
in the F Prime system reference repository:
namespace SystemReference {
...
// Health ping entries
namespace PingEntries {
namespace blockDrv { enum { WARN = 3, FATAL = 5 }; }
namespace chanTlm { enum { WARN = 3, FATAL = 5 }; }
namespace cmdDisp { enum { WARN = 3, FATAL = 5 }; }
namespace cmdSeq { enum { WARN = 3, FATAL = 5 }; }
namespace eventLogger { enum { WARN = 3, FATAL = 5 }; }
namespace fileDownlink { enum { WARN = 3, FATAL = 5 }; }
namespace fileManager { enum { WARN = 3, FATAL = 5 }; }
namespace fileUplink { enum { WARN = 3, FATAL = 5 }; }
namespace imageProcessor { enum {WARN = 3, FATAL = 5}; }
namespace prmDb { enum { WARN = 3, FATAL = 5 }; }
namespace processedImageBufferLogger { enum {WARN = 3, FATAL = 5}; }
namespace rateGroup1Comp { enum { WARN = 3, FATAL = 5 }; }
namespace rateGroup2Comp { enum { WARN = 3, FATAL = 5 }; }
namespace rateGroup3Comp { enum { WARN = 3, FATAL = 5 }; }
namespace saveImageBufferLogger { enum {WARN = 3, FATAL = 5}; }
}
}
Other definitions:
You can put any compile-time definitions you wish into T TopologyAc.hpp
If you need link-time definitions (e.g., to declare variables with storage),
you can put them in T TopologyAc.cpp
, but this is not required.
For example, SystemReference/Top/SystemReferenceTopologyAc.hpp
declares
a variable SystemReference::Allocation::mallocator
of type Fw::MallocAllocator
.
It provides an allocator used in the setup and teardown
of several component instances.
The corresponding link-time symbol is defined in SystemReferenceTopologyDefs.cpp
.
14.2.2. The Main Function
You must write a main function that performs application-specific
and system-specific tasks such as parsing command-line arguments,
handling signals, and returning a numeric code to the system on exit.
Your main code can use the following public interface provided
by T TopologyAc.hpp
:
// ----------------------------------------------------------------------
// Public interface functions
// ----------------------------------------------------------------------
//! Set up the topology
void setup(
const TopologyState& state //!< The topology state
);
//! Tear down the topology
void teardown(
const TopologyState& state //!< The topology state
);
These functions reside in the C++ namespace corresponding to the FPP module where the topology T is defined.
On Linux, a typical main function might work this way:
-
Parse command-line arguments. Use the result to construct a
TopologyState
objectstate
. -
Set up a signal handler to catch signals.
-
Call T
::setup
, passing in thestate
object, to construct and initialize the topology. -
Start the topology running, e.g., by looping in the main thread until a signal is handled, or by calling a start function on a timer component (see, e.g.,
Svc::LinuxTimer
). The loop or timer typically runs until a signal is caught, e.g., when the user presses control-C at the console. -
On catching a signal
-
Set a flag that causes the main loop to exit or the timer to stop. This flag must be a volatile and atomic variable (e.g.,
std::atomic_bool
) because it is accessed concurrently by signal handlers and threads. -
Call T
::teardown
, passing in thestate
object, to tear down the topology. -
Wait some time for all the threads to exit.
-
Exit the main thread.
-
For an example like this, see SystemReference/Top/Main.cpp
in the
F Prime system reference repository.
14.2.3. Public Symbols
The header file T TopologyAc.hpp
declares several public
symbols that you can use when writing your main function.
Instance variables:
Each component instance used in the topology is declared as
an extern
variable, so you can refer to any component instance
in the main function.
For example, the main function in the SystemReference
topology
calls the method callIsr
of the blockDrv
(block driver)
component instance, in order to simulate an interrupt service
routine (ISR) call triggered by a hardware interrupt.
Helper functions:
The auto-generated setup
function calls the following auto-generated
helper functions:
void initComponents(const TopologyState& state);
void configComponents(const TopologyState& state);
void setBaseIds();
void connectComponents();
void regCommands();
void readParameters();
void loadParameters();
void startTasks(const TopologyState& state);
The auto-generated teardown
function calls the following
auto-generated helper functions:
void stopTasks(const TopologyState& state);
void freeThreads(const TopologyState& state);
void tearDownComponents(const TopologyState& state);
The helper functions are declared as public symbols in T
TopologyAc.hpp
, so if you wish, you may write your own versions
of setup
and teardown
that call these functions directly.
The FPP modeling is designed so that you don’t have to do this;
you can put any custom C++ code for setup or teardown into
init specifiers
and let the FPP translator generate complete setup
and teardown
functions that you simply call, as described above.
Using init specifiers generally produces cleaner integration between
the model and the C++ code: you write the custom
C++ code once, any topology T that uses an instance I will pick
up the custom C++ code for I, and the FPP translator will automatically
put the code for I into the correct place in T TopologyAc.cpp
.
However, if you wish to write the custom code directly into your main
function, you may.