== 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 <>. These are types that are named in the FPP model but are defined directly in {cpp}. . Implementations of <>. . Implementations of <>. . 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 <> 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::SerialBufferBase&, Fw::Endianness = Fw::Endianness::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::SerialBufferBase&, Fw::Endianness = Fw::Endianness::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 <>. 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::SerialBufferBase`. ---- // 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::SerialBufferBase B; using E = Fw::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 <>. In this case you can implement `T` as a class that extends `Fw::SerialBufferBase`. 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::SerialBufferBase` 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 <> 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 <>, 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 <>. 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 <> 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 <> 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 <> is serialized member-by-member, in the order that the members appear in the FPP struct definition, with no padding. .. Except for <>, each member is serialized using these rules. .. Each member array is serialized as stated in rule 5. . A value of <> is converted to a primitive integer value of the <> specified in the enum definition. This value is serialized as stated in rule 1. . A value of <> is serialized according to its <>.