fpp/docs/users-guide/Writing-C-Plus-Plus-Implementations.adoc
2025-10-10 10:27:42 -05:00

565 lines
21 KiB
Plaintext

== Writing C Plus Plus Implementations
When constructing an F Prime deployment in {cpp}, there are generally
five kinds of implementations you have to write:
. Implementations of
<<Defining-Types_Abstract-Type-Definitions,abstract types>>.
These are types that are named in the FPP model but are defined
directly in {cpp}.
. Implementations of <<Defining-State-Machines_Writing-a-State-Machine-Definition,
external state machines>>.
. Implementations of
<<Defining-Components,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 (item 3) involves filling out the API provided by
the {cpp} 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 (item 4) is unrelated to FPP, so we
won't cover it in this manual.
Here we focus on items 1, 2, and 5: implementing abstract types,
implementing external state machines, and implementing deployments.
We also discuss *serialization* of data values, i.e., representing
FPP data values as binary data for storage and transmission.
=== Implementing Abstract Types
When translating to {cpp}, an
<<Defining-Types_Abstract-Type-Definitions,abstract type definition>>
represents a {cpp} class that you write directly in {cpp}.
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 {cpp}, then the generated {cpp} 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 {cpp}:
[source,cpp]
----
#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.
*General implementations:*
In most cases, when implementing an abstract type `T` in {cpp}, you
will define
a class that extends `Fw::Serializable` from the F Prime framework.
Your class definition should include the following:
* An implementation of the virtual function
+
----
Fw::SerializeStatus T::serializeTo(Fw::SerializeBufferBase&, Fw::Serialization::Endianness = Fw::Serialization::BIG) const
----
+
that specifies how to *serialize* a class instance to a buffer
(i.e., convert a class instance to a byte string stored in a buffer).
* An implementation of the function
+
----
Fw::SerializeStatus T::deserializeFrom(Fw::SerializeBufferBase&, Fw::Serialization::Endianness = Fw::Serialization::BIG)
----
+
that specifies how to *deserialize* a class instance from a
buffer (i.e., reconstruct a class instance from a byte string stored in a
buffer).
* 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;
----
For more on serialization, see the section on
<<Writing-C-Plus-Plus-Implementations_Serialization-of-FPP-Values,
serialization of FPP values>>.
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 {cpp} struct (rather than a class) so that
all members are public by default.
To implement `serializeTo`, we use the `serializeFrom` function
provided by `Fw::SerializeBufferBase`.
----
// A minimal implementation of abstract type T
#ifndef T_HPP
#define T_HPP
// Include Fw/Types/Serializable.hpp from the F Prime framework
#include "Fw/Types/Serializable.hpp"
struct T final : public Fw::Serializable { // Extend Fw::Serializable
// Define some shorthand for F Prime types
using SS = Fw::SerializeStatus SS;
using B = Fw::SerializeBufferBase B;
using E = Fw::Serialization::Endianness E;
// 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 serializeTo method
SS serializeTo(B& b, E e) const final { return b.serializeFrom(x, e); }
// Define the virtual deserializeFrom method
SS deserializeFrom(B& b, E e) final { return b.deserializeTo(x, e); }
// Provide some data
U32 x;
};
#endif
----
*Serializable buffers used in ports:*
In some cases, you may want to define an abstract type `T` that represents
a data buffer and that is used only in <<Defining-Ports,port definitions>>.
In this case you can implement
`T` as a class that extends `Fw::SerializeBufferBase`.
Instead of implementing the `serializeTo` and `deserializeFrom` functions
directly, you override functions that get the address and the capacity
(allocated size) of the buffer; the base class `Fw::SerializeBufferBase`
uses these functions to implement `serializeTo` and `deserializeFrom`.
For an example of how to do this, see the files `Fw/Cmd/CmdArgBuffer.hpp`
and `Fw/Cmd/CmdArgBuffer.cpp` in the F Prime repository.
Be careful, though: if you implement an abstract type `T` this way
and you try to use the type `T` outside of a port definition,
the generated {cpp} may not compile.
=== Implementing External State Machines
An <<Defining-State-Machines_Writing-a-State-Machine-Definition,
external state machine>> refers to a state machine implementation
supplied outside the FPP model.
To implement an external state machine, you can use
the https://github.com/JPLOpenSource/STARS/tree/main[State Autocoding for
Real-Time Systems (STARS)]
tool.
STARS provides several ways to specify state machines, and it
provides several {cpp} back ends.
The F Prime back end is designed to work with FPP code generation.
For an example of an external state machine implemented in STARS,
see `FppTest/state_machine` in the F Prime repository.
=== Implementing Deployments
At the highest level of an F Prime implementation, you write
two units of {cpp} 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.
==== Application-Specific Definitions
As discussed in the section on
<<Analyzing-and-Translating-Models_Generating-C-Plus-Plus_Topology-Definitions,
generating {cpp} topology definitions>>, when you translate an FPP
topology _T_ to {cpp}, 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 {cpp} implementation.
. Because _T_ `TopologyAc.cpp` includes _T_ `TopologyAc.hpp`
and _T_ `TopologyAc.hpp` includes _T_ `TopologyDefs.hpp`,
the handwritten definitions in _T_ `TopologyDefs.hpp` are visible
to the auto-generated code in _T_ `TopologyAc.hpp` and
`TopologyAc.cpp`.
. You can also include _T_ `TopologyDefs.hpp` in your main
function (described in the next section) to make its
definitions visible there.
That way `main` 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 {cpp} namespace
corresponding to the FPP module where the topology _T_ is defined.
For example, in `SystemReference/Top/topology.fpp` in the
https://github.com/fprime-community/fprime-system-reference/blob/main/SystemReference/Top/topology.fpp[F Prime system reference deployment], the FPP topology `SystemReference` is defined in the FPP
module `SystemReference`, and so in
https://github.com/fprime-community/fprime-system-reference/blob/main/SystemReference/Top/SystemReferenceTopologyDefs.hpp[`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 {cpp} 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
<<Defining-Component-Instances_Init-Specifiers,
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:
[source,fpp]
--------
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::SystemReference_comDriver::PRIORITY,
ConfigConstants::SystemReference_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 {cpp} enumeration with
the following constants `WARN` and `FATAL`.
Set `WARN` equal to the warning interval for the enclosing
component instance.
Set `FATAL` 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:
[source,cpp]
----
namespace SystemReference {
...
// Health ping entries
namespace PingEntries {
namespace SystemReference_blockDrv { enum { WARN = 3, FATAL = 5 }; }
namespace SystemReference_chanTlm { enum { WARN = 3, FATAL = 5 }; }
namespace SystemReference_cmdDisp { enum { WARN = 3, FATAL = 5 }; }
namespace SystemReference_cmdSeq { enum { WARN = 3, FATAL = 5 }; }
namespace SystemReference_eventLogger { enum { WARN = 3, FATAL = 5 }; }
namespace SystemReference_fileDownlink { enum { WARN = 3, FATAL = 5 }; }
namespace SystemReference_fileManager { enum { WARN = 3, FATAL = 5 }; }
namespace SystemReference_fileUplink { enum { WARN = 3, FATAL = 5 }; }
namespace SystemReference_imageProcessor { enum {WARN = 3, FATAL = 5}; }
namespace SystemReference_prmDb { enum { WARN = 3, FATAL = 5 }; }
namespace SystemReference_processedImageBufferLogger { enum {WARN = 3, FATAL = 5}; }
namespace SystemReference_rateGroup1Comp { enum { WARN = 3, FATAL = 5 }; }
namespace SystemReference_rateGroup2Comp { enum { WARN = 3, FATAL = 5 }; }
namespace SystemReference_rateGroup3Comp { enum { WARN = 3, FATAL = 5 }; }
namespace SystemReference_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`.
==== 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`:
[source,cpp]
----
// ----------------------------------------------------------------------
// 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 {cpp} 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` object `state`.
. Set up a signal handler to catch signals.
. Call _T_ `::setup`, passing in the `state` 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 the `state` 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.
==== 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:
[source,cpp]
----
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:
[source,cpp]
----
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 {cpp} code for setup or teardown into
<<Defining-Component-Instances_Init-Specifiers,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 {cpp} code: you write the custom
{cpp} code once, any topology _T_ that uses an instance _I_ will pick
up the custom {cpp} 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.
=== Serialization of FPP Values
Every value represented in FPP can be *serialized*, i.e., converted into a
machine-independent sequence of bytes.
Serialization provides a consistent way to store data (e.g.,
to onboard storage) and to transmit data (e.g., to or from the ground).
The F Prime framework also uses serialization to pass data through asynchronous
port invocations.
The data is serialized when it is put on a message queue
and then *deserialized* (i.e., converted from a byte sequence to
a {cpp} representation)
when it is taken off the queue for processing.
F Prime uses the following rules for serializing data:
. Values of primitive integer type are serialized as follows:
.. A value of unsigned integer type (`U8`, `U16`, `U32`, or `U64`)
is serialized into big-endian order (most significant byte first) by default,
using the number of bytes implied by the bit width.
For example, the `U16` value 10 (decimal) is serialized as the
two bytes `00` `0A` (hex). If little-endian order is desired, the optional mode parameter can be specified as `Fw::Serialization::LITTLE`. This stores the data least significant byte order. The `U16` value 10 (decimal) is serialized in little-endian as the two bytes `0A` `00` (hex).
.. A value of signed integer type (`I8`, `I16`, `I32`, or `I64`)
is serialized by first converting the value to an unsigned value of the same bit
width and then serializing the unsigned value as stated in rule 1.a.
If the value is nonnegative, then the unsigned value is
the same as the signed value.
Otherwise the unsigned value is the two's complement of the signed value.
For example:
... The `I16` value 10 (decimal) is serialized as two bytes, yielding the bytes `00` `0A` (hex) in big-endian and `0A` `00` (hex) in little-endian.
... The `I16` value -10 (decimal) is serialized by
(1) computing the `U16` value 2^16^ - 10 = 65526
and (2) serializing that value as two bytes in the selected byte order,
yielding the bytes `FF` `F6` (hex) big-endian and `F6` `FF` (hex) little-endian.
. A value of floating-point type (`F32` or `F64`)
is serialized in the selected byte order according to the IEEE
standard for representing these values.
. A value of Boolean type is serialized as a single byte.
The byte values used to represent `true` and `false`
are `FW_SERIALIZE_TRUE_VALUE` and `FW_SERIALIZE_FALSE_VALUE`,
which are defined in the F Prime configuration header `FpConfig.h`.
. A value of string type is serialized as a size followed
by the string characters in string order.
.. The size is serialized according to rule 1 for primitive
integer types.
The F Prime type definition `FwSizeStoreType` specifies the type to use
for the size.
This type definition is user-configurable; it is found in the
F Prime configuration file `FpConfig.fpp`.
.. There is one byte for each character of the string, and there is
no null terminator.
Each string character is serialized as an `I8` value according to rule 1.b.
. A value of <<Defining-Types_Array-Type-Definitions,array type>>
is serialized as a sequence of serialized values, one for each array
element, in array order.
Each value is serialized using these rules.
. A value of <<Defining-Types_Struct-Type-Definitions,struct type>>
is serialized member-by-member, in the order
that the members appear in the FPP struct definition,
with no padding.
.. Except for
<<Defining-Types_Struct-Type-Definitions_Member-Arrays,member arrays>>,
each member is serialized using these rules.
.. Each member array is serialized as stated in rule 5.
. A value of <<Defining-Enums,enum type>> is converted to a primitive
integer value of the <<Defining-Enums_The-Representation-Type,representation
type>> specified in the enum definition.
This value is serialized as stated in rule 1.
. A value of <<Defining-Types_Abstract-Type-Definitions,abstract type>> is
serialized according to its
<<Writing-C-Plus-Plus-Implementations_Implementing-Abstract-Types,
{cpp} implementation>>.