USER’S GUIDE

INTRODUCTION

The C++ framework allows the programmer to create an instance of the Xapp object which then can be used as a foundation for the application. The Xapp object provides a message level interface to the RIC Message Router (RMR), including the ability to register callback functions which the instance will drive as messages are received; much in the same way that an X-windows application is driven by the window manager for all activity. The xApp may also choose to use its own send/receive loop, and thus is not required to use the callback driver mechanism provided by the framework.

Termonology

To avoid confusion the term xAPP is used in this document to refer to the user’s application code which is creating Xapp, and related objects provided by the framework. The use of framework should be taken to mean any of the classes and/or support functions which are provided by the ricxfcpp library.

THE FRAMEWORK API

The C++ framework API consists of the creation of the xApp object, and invoking desired functions via the instance of the object. The following paragraphs cover the various steps involved to create an xApp instance, wait for a route table to arrive, send a message, and wait for messages to arrive.

The Namespace

Starting with version 2.0.0 the framwork introduces a namespace of xapp for the following classes and types:

  • Alarm

  • Jhash

  • Message

  • Msg_component

This is a breaking change and as such the major version was bumpped from 1 to 2.

Creating the xApp instance

The creation of the xApp instance is as simple as invoking the object’s constructor with two required parameters:

port

A C string (pointer to char) which defines the port that

RMR will open to listen for connections.


wait

A Boolean value which indicates whether or not the

initialization process should wait for the arrival of a

valid route table before completing. When true is

supplied, the initialization will not complete until RMR

has received a valid route table (or one is located via

the RMR_SEED_RT environment variable).

The following code sample illustrates the simplicity of creating the instance of the xApp object.

#include <memory>
#include <ricxfcpp/xapp.hpp>
int main( ) {
    std::unique_ptr<Xapp> xapp;
    char* listen_port = (char *) "4560";    //RMR listen port
    bool  wait4table = true;            // wait for a route table

    xapp = std::unique_ptr<Xapp>(
          new Xapp( listen_port, wait4table ) );
}

Figure 1: Creating an xAPP instance.

From a compilation perspective, the following is the simple compiler invocation string needed to compile and link the above program (assuming that the sample code exists in a file called man_ex1.cpp.

g++ man_ex1.cpp -o man_ex1 -lricxfcpp -lrmr_si -lpthread

The above program, while complete and capable of being compiled, does nothing useful. When invoked, RMR will be initialized and will begin listening for a route table; blocking the return to the main program until one is received. When a valid route table arrives, initialization will complete and the program will exit as there is no code following the instruction to create the object.

LISTENING FOR MESSAGES

The program in the previous example can be extended with just a few lines of code to enable it to receive and process messages. The application needs to register a callback function for each message type which it desires to process.

Once registered, each time a message is received the registered callback for the message type will be invoked by the framework.

Callback Signature

As with most callback related systems, a callback must have a well known function signature which generally passes event related information and a “user” data pointer which was registered with the function. The following is the prototype which callback functions must be defined with:

void cb_name( xapp::Message& m, int mtype, int subid,
      int payload_len, xapp::Msg_component payload,
      void* usr_data );

Figure 2: Callback function signature

The parameters passed to the callback function are as follows:

m

A reference to the Message that was received.


mtype

The message type (allows for disambiguation if the

callback is registered for multiple message types).


subid

The subscription ID from the message.


payload len

The number of bytes which the sender has placed into the

payload.


payload

A direct reference (smart pointer) to the payload. (The

smart pointer is wrapped in a special class in order to

provide a custom destruction function without burdening

the xApp developer with that knowledge.)


user data

A pointer to user data. This is the pointer that was

provided when the function was registered.

To illustrate the use of a callback function, the previous code example has been extended to add the function, register it for message types 1000 and 1001, and to invoke the Run() function in the framework (explained in the next section).

#include <memory>
#include <ricxfcpp/xapp.hpp>
long m1000_count = 0;    // message counters, one for each type
long m1001_count = 0;

// callback function that will increase the appropriate counter
void cbf( xapp::Message& mbuf, int mtype, int subid, int len,
            xapp::Msg_component payload,  void* data ) {
    long* counter;

    if( (counter = (long *) data) != NULL ) {
        (*counter)++;
    }
}

int main( ) {
    std::unique_ptr<Xapp> xapp;
    char* listen_port = (char *) "4560";
    bool  wait4table = false;

    xapp = std::unique_ptr<Xapp>(
          new Xapp( listen_port, wait4table ) );

    // register the same callback function for both msg types
    xapp->Add_msg_cb( 1000, cbf, (void *) &m1000_count );
    xapp->Add_msg_cb( 1001, cbf, (void *) &m1001_count );

    xapp->Run( 1 );        // start the callback driver
}

Figure 3: Callback function example.

As before, the program does nothing useful, but now it will execute and receive messages. For this example, the same function can be used to increment the appropriate counter simply by providing a pointer to the counter as the user data when the callback function is registered. In addition, a subtle change from the previous example has been to set the wait for table flag to false.

For an xApp that is a receive only application (never sends) it is not necessary to wait for RMR to receive a table from the Route Manager.

Registering A Default Callback

The xApp may also register a default callback function such that the function will be invoked for any message that does not have a registered callback. If the xAPP does not register a default callback, any message which cannot be mapped to a known callback function is silently dropped. A default callback is registered by providing a generic message type of xapp->DEFAULT_CALLBACK on an Add_msg_cb call.

The Framework Callback Driver

The Run() function within the Xapp object is invoked to start the callback driver, and the xApp should not expect the function to return under most circumstances. The only parameter that the Run() function expects is the number of threads to start. For each thread requested, the framework will start a listener thread which will allow received messages to be processed in parallel. If supplying a value greater than one, the xApp must ensure that the callback functions are thread safe as it is very likely that the same callback function will be invoked concurrently from multiple threads.

SENDING MESSAGES

It is very likely that most xApps will need to send messages and will not operate in “receive only” mode. Sending the message is a function of the message object itself and can take one of two forms:

  • Replying to the sender of a received message

  • Sending a message (routed based on the message type and subscription ID)

When replying to the sender, the message type and subscription ID are not used to determine the destination of the message; RMR ensures that the message is sent back to the originating xApp. The xApp may still need to change the message type and/or the subscription ID in the message prior to using the reply function.

To provide for both situations, two reply functions are supported by the Message object as illustrated with the following prototypes.

bool Send_response(  int mtype, int subid, int response_len,
     std:shared_ptr<unsigned char> response );

bool Send_response(  int response_len, std::shared_ptr<unsigned char> response );

Figure 4: Reply function prototypes.

In the first prototype the xApp must supply the new message type and subscription ID values, where the second function uses the values which are currently set in the message. Further, the new payload contents, and length, are supplied to both functions; the framework ensures that the message is large enough to accommodate the payload, reallocating it if necessary, and copies the response into the message payload prior to sending. Should the xApp need to change either the message type, or the subscription ID, but not both, the NO_CHANGE constant can be used as illustrated below.

msg->Send_response( xapp::Message::NO_CHANGE, xapp::Message::NO_SUBID,
    pl_length, (unsigned char *) payload );

Figure 5: Send response prototype.

In addition to the two function prototypes for Send_response() there are two additional prototypes which allow the new payload to be supplied as a shared smart pointer. The other parameters to these functions are identical to those illustrated above, and thus are not presented here.

The Send_msg() set of functions supported by the Message object are identical to the Send_response() functions and are shown below.

bool Send_msg( int mtype, int subid, int payload_len,
    std::shared_ptr<unsigned char> payload );

bool Send_msg( int mtype, int subid, int payload_len,
    unsigned char* payload );

bool Send_msg( int payload_len,
    std::shared_ptr<unsigned char> payload );

bool Send_msg( int payload_len, unsigned char* payload );

Figure 6: Send function prototypes.

Each send function accepts the message, copies in the payload provided, sets the message type and subscription ID (if provided), and then causes the message to be sent. The only difference between the Send_msg() and Send_response() functions is that the destination of the message is selected based on the mapping of the message type and subscription ID using the current routing table known to RMR.

Direct Payload Manipulation

For some applications, it might be more efficient to manipulate the payload portion of an Xapp Message in place, rather than creating it and relying on a buffer copy when the message is finally sent. To achieve this, the xApp must either use the smart pointer to the payload passed to the callback function, or retrieve one from the message using Get_payload() when working with a message outside of a callback function. Once the smart pointer is obtained, the pointer’s get() function can be used to directly reference the payload (unsigned char) bytes.

When working directly with the payload, the xApp must take care not to write more than the actual payload size which can be extracted from the Message object using the Get_available_size() function.

When sending a message where the payload has been directly altered, and no extra buffer copy is needed, a NULL pointer should be passed to the Message send function. The following illustrates how the payload can be directly manipulated and returned to the sender (for simplicity, there is no error handling if the payload size of the received message isn’t large enough for the response string, the response is just not sent).

Msg_component payload;  // smart reference
int pl_size;            // max size of payload

payload = msg->Get_payload();
pl_size = msg->Get_available_size();
if( snprintf( (char *) payload.get(), pl_size,
    "Msg Received\\n" ) < pl_size ) {
  msg->Send_response( M_TYPE, SID, strlen( raw_pl ), NULL );
}

Figure 7: Send message without buffer copy.

Sending Multiple Responses

It is likely that the xApp will wish to send multiple responses back to the process that sent a message that triggered the callback. The callback function may invoke the Send_response() function multiple times before returning.

After each call, the Message retains the necessary information to allow for a subsequent invocation to send more data. It should be noted though, that after the first call to {Send_response() the original payload will be lost; if necessary, the xApp must make a copy of the payload before the first response call is made.

Message Allocation

Not all xApps will be “responders,” meaning that some xApps will need to send one or more messages before they can expect to receive any messages back. To accomplish this, the xApp must first allocate a message buffer, optionally initialising the payload, and then using the message’s Send_msg() function to send a message out. The framework’s Alloc_msg() function can be used to create a Message object with a desired payload size.

FRAMEWORK PROVIDED CALLBACKS

The framework itself may provide message handling via the driver such that the xApp might not need to implement some message processing functionality. Initially, the C++ framework will provide a default callback function to handle the RMR based health check messages. This callback function will assume that if the message was received, and the callback invoked, that all is well and will reply with an OK state. If the xApp should need to override this simplistic response, all it needs to do is to register its own callback function for the health check message type.

JSON SUPPORT

The C++ xAPP framework provides a very lightweight json parser and data hash facility. Briefly, a json hash (Jhash) can be established by creating an instance of the Jhash object with a string of valid json. The resulting object’s functions can then be used to read values from the resulting hash.

Creating The Jhash Object

The Jhash object is created simply by passing a json string to the constructor.

#include <ricxfcpp/Jhash.hpp>

std::string jstring = "{ \\"tag\\": \\"Hello World\\" }";
Jhash*  jh;

jh =  new Jhash( jstring.c_str() );

Figure 8: The creation of the Jhash object.

Once the Jhash object has been created any of the methods described in the following paragraphs can be used to retrieve the data:

Json Blobs

Json objects can be nested, and the nesting is supported by this representation. The approach taken by Jhash is a “directory view” approach, where the “current directory,” or current blob, limits the scope of visible fields.

As an example, the json contained in figure 9, contains a “root” blob and two sub-blobs (address and lease_info).

{
    "lodge_name": "Water Buffalo Lodge 714",
    "member_count": 41,
    "grand_poobah": "Larry K. Slate",
    "attendance":   [ 23, 14, 41, 38, 24 ],
    "address": {
        "street":    "16801 Stonway Lane",
        "suite":     null,
        "city":      "Bedrock",
        "post_code": "45701"
    },
    "lease_info": {
        "owner":    "Stonegate Properties",
        "amount":   216.49,
        "due":      "monthly",
        "contact:"  "Kyle Limestone"
    }
}

Figure 9: Sample json with a root and two blobs.

Upon creation of the Jhash object, the root fields, lodge_name, member_count, and grand_poobah are immediately available. The fields in the sub-blobs are available only when the correct blob is selected. The code sample in figure 10 illustrates how a sub-blob is selected.

jh->Set_blob( (char *) "address" );     // select address
jh->Unset_blob();                       // return to root
jh->Set_blob( (char *) "lease_info" );  // select the lease blob

Figure 10: Blob selection example.

Currently, the selected blob must be unset in order to select a blob at the root level; unset always sets the root blob. Attempting to use the Set_blob function will attempt to select the named blob from the current blob, and not the root.

Simple Value Extraction

Simple values are the expected data types string, value, and boolean. This lightweight json parser treats all values as floating point numbers and does not attempt to maintain a separate integer type. A fourth type, null, is supported to allow the user to expressly check for a field which is defined but has no value; as opposed to a field that was completely missing from the data. The following are the prototypes for the functions which allow values to be extracted:

std::string String( const char* name );
float Value( const char* name );
bool Bool( const char* name );

Each of these functions returns the value associated with the field with the given name. If the value is missing, the following default values are returned:

String

An empty string (.e.g “”).


Value

Zero (e.g 0.0)


bool

false

If the user needs to disambiguate between a missing value and the default value either the Missing or Exists function should be used first.

Testing For Existing and Missing Fields

Two functions allow the developer to determine whether or not a field is included in the json. Both of these functions work on the current blob, therefore it is important to ensure that the correct blob is selected before using either of these functions. The prototypes for the Exists and Missing functions are below:

bool Exists( const char* name );
bool Is_missing( const char* name );

The Exists function returns true if the field name exists in the json and false otherwise. Conversely, the Missing function returns true when the field name does not exist in the json.

Testing Field Type

The Exists and Missing functions might not be enough for the user code to validate the data that it has. To assist with this, several functions allow direct type testing on a field in the current blob. The following are the prototypes for these functions:

bool Is_bool( const char* name );
bool Is_null( const char* name );
bool Is_string( const char* name );
bool Is_value( const char* name );

Each of these functions return true if the field with the given name is of the type being tested for.

Arrays

Arrays are supported in the same manner as simple field values with the addition of the need to supply an array index when fetching values from the object. In addition, there is a length function which can be used to determine the number of elements in the named array. The prototypes for the array based functions are below:

int Array_len( const char* name );

bool Is_bool_ele( const char* name, int eidx );
bool Is_null_ele( const char* name, int eidx );
bool Is_string_ele( const char* name, int eidx );
bool Is_value_ele( const char* name, int eidx );

bool Bool_ele( const char* name, int eidx );
std::string String_ele( const char* name, int eidx );
float Value_ele( const char* name, int eidx );

For each of these functions the eidx is the zero based element index which is to be tested or selected.

Arrays of Blobs

An array containing blobs, rather than simple field value pairs, the blob must be selected prior to using it, just as a sub-blob needed to be selected. The Set_blob_ele function is used to do this and has the following prototype:

bool Set_blob_ele( const char* name, int eidx );

As with selecting a sub-blob, an unset must be performed before selecting the next blob. Figure 11 illustrates how these functions can be used to read and print values from the json in figure 12.

"members": [
    { "name": "Fred Flinstone", "member_num": 42 },
    { "name": "Barney Rubble", "member_num": 48 },
    { "name": "Larry K Slate", "member_num": 22 },
    { "name": "Kyle Limestone", "member_num": 49 }
]

Figure 11: Json array containing blobs.

std::string mname;
float mnum;
int len;

len = jh->Array_len( (char *) "members" );
for( i = 0; i < len; i++ ) {
    jh->Set_blob_ele( (char *) "members", i );  // select blob

    mname = jh->String( (char *) "name" );      // read values
    mnum = jh->Value( (char *) "member_num" );
    fprintf( stdout, "%s is member %d\\n", mname.c_str(), (int) mnum );

    jh->Unset_blob();                           // back to root
}

Figure 12: Code to process the array of blobs.

ALARM MANAGER INTERFACE

The C++ framework provides an API which allows the xAPP to easily construct and generate alarm messages. Alarm messages are a special class of RMR message, allocated in a similar fashion as an RMR message through the framework’s Alloc_alarm() function.

The API consists of the following function types:

Raise

Cause the alarm to be assigned a severity and and sent via

RMR message to the alarm collector process.


Clear

Cause a clear message to be sent to the alarm collector.


Raise Again

Cause a clear followed by a raise message to be sent to

the alarm collector.

Allocating Alarms

The xapp function provided by the framework is used to create an alarm object. Once the xAPP has an alarm object it can be used to send one, or more, alarm messages to the collector.

The allocation function has three prototypes which allow the xAPP to create an alarm with an initial set of information as is appropriate. The following are the prototypes for the allocate functions:

std::unique_ptr<xapp::Alarm> Alloc_alarm( );
std::unique_ptr<xapp::Alarm> Alloc_alarm( std::string meid );
std::unique_ptr<xapp::Alarm> Alloc_alarm( int prob_id, std::string meid );

Figure 13: Alarm allocation prototypes.

Each of the allocation functions returns a unique pointer to the alarm. In the simplest form (1) the alarm is initialised with an empty meid (managed element ID) string, and unset problem ID (-1). The second prototype allows the xAPP to supply the meid, and in the third form both the problem ID and the meid are used to initialise the alarm.

Raising An Alarm

Once an alarm has been allocated, its Raise() function can be used to cause the alarm to be sent to the collector. The raise process allows the xAPP to perform the following modifications to the alarm before sending the message:

  • Set the alarm severity

  • Set the problem ID value

  • Set the alarm information string

  • Set the additional information string

The following are the prototypes for the Raise() functions of an alarm object: ….. In its simplest form (1) the Raise() function will send the alarm without making any changes to the data. The final two forms allow the xAPP to supply additional data which is added to the alarm before sending the message. Each of the raise functions returns true on success and false if the alarm message could not be sent.

Severity

The severity is one of the SEV_ constants listed below. These map to alarm collector strings and insulate the xAPP from any future alarm collector changes. The specific meaning of these severity types are defined by the alarm collector and thus no attempt is made to guess what their actual meaning is. These constants are available by including alarm.hpp.

SEV_MAJOR
SEV_MINOR
SEV_WARN
SEV_DEFAULT

Figure 14: Severity constants available in alarm.hpp.

The Problem ID

The problem ID is an integer which is assigned by the xAPP. The framework makes no attempt to verify that it has been se, nor does it attempt to validate the value. If the xAPP does not set the value, -1 is used.

Information Strings

The two information strings are also xAPP defined and provide the information that the xAPP deems necessary and related to the alarm. What the collector expects, and how these strings are used, is beyond the scope of the framework to describe or validate. If not supplied, empty strings are sent in the alarm message.

Clearing An Alarm

The Clear() function of an alarm may be used to send a clear message. In a manner similar to the Raise() functions, the Clear() functions allow the existing alarm data to be sent without change, or for the xAPP to modify the data before the message is sent to the collector. The following are the prototype for these functions.

bool Clear( );
bool Clear( int severity, int problem, std::string info );
bool Clear( int severity, int problem, std::string info, std::string addional_info );
bool Clear_all( );

Figure 15: Clear function prototypes.

Each of the clear functions returns true on success and false if the alarm message could not be sent.

The Clear_all() function sends a special action code to the collector which is assumed to clear all alarms. However, it is unknown whether that implies all alarms, or all alarms matching the problem_id, or some other interpretation. Please consult the alarm collector documentation for these specifics.

Adjusting Alarm Contents

It might be necessary for the xAPP to adjust the alarm contents outside of the scope of the Raise() function, or to adjust data that cannot be manipulated by Raise(). The following are the (self explanatory) prototypes for the setter functions which are available to the xAPP.

void Set_additional( std::string new_info );
void Set_appid( std::string new_id );
void Set_info( std::string new_info );
void Set_meid( std::string new_meid );
void Set_problem( int new_id );
void Set_severity( int new_sev );

Figure 16: Alarm Setters

METRICS SUPPORT

The C++ xAPP framework provides a lightweight interface to the metrics gateway allowing the xAPP to create and send metrics updates without needing to understand the underlying message format. From the xAPP’s perspective, the metrics object is created with one or more key/value measurement pairs and then is sent to the process responsible for forwarding them to the various collection processes. The following sections describe the Metrics object and the API associated with it.

Creating The Metrics Object

The xapp object can be created directly, or via the xapp framework. When creating directly the xAPP must supply an RMR message for the object to use; when the framework is used to create the object, the message is created as as part of the process. The framework provides three constructors for the metrics instance allowing the xAPP to supply the measurement source, the source and reporter, or to default to using the xAPP name as both the source and reporter (see section Source and Reporter for a more detailed description of these). The framework constructors are illustrated in figure 17.

std::unique_ptr<xapp::Metrics> Alloc_metrics( );
std::unique_ptr<xapp::Metrics> Alloc_metrics( std::string source );
std::unique_ptr<xapp::Metrics> Alloc_metrics( std::string reporter, std::string source );

Figure 17: The framework constructors for creating an instance of the metrics object.

#include <ricxfcpp/Metrics>

char* port = (char *) "4560";

auto x = std::unique_ptr<Xapp>( new Xapp( port ) );
auto reading = std::shared_ptr<xapp::Metrics>( x->Alloc_metric( ) );

Figure 18: Metrics instance creation using the framework.

Figures 18 illustrates how the framework constructor can be used to create a metrics instance. While it is unlikely that an xAPP will create a metrics instance directly, there are three similar constructors available. These are prototypes are shown in figure 19 and their use is illustrated in figure 20.

Metrics( std::shared_ptr<xapp::Message> msg );
Metrics( std::shared_ptr<xapp::Message> msg, std::string msource );
Metrics( std::shared_ptr<xapp::Message> msg, std::string reporter, std::string msource );

Figure 19: Metrics object constructors.

#include <ricxfcpp/Metrics>

char* port = (char *) "4560";

auto x = std::unique_ptr<Xapp>( new Xapp( port ) );
auto msg = std::shared_ptr<xapp::Message>( x->Alloc_msg( 4096 ) );
auto reading = std::shared_ptr<xapp::Metrics>( new Metrics( msg ) );

Figure 20: Direct creation of a metrics instance.

Adding Values

Once an instance of the metrics object is created, the xAPP may push values in preparation to sending the measurement(s) to the collector. The Push_data() function is used to push each key/value pair and is illustrated in figure 21.

reading->Push_data( "normal_count", (double) norm_count );
reading->Push_data( "high_count", (double) hi_count );
reading->Push_data( "excessive_count", (double) ex_count );

Figure 21: Pushing key/value pairs into a metrics instance.

Sending A Measurement Set

After all of the measurement key/value pairs have been added to the instance, the Send() function can be invoked to create the necessary RMR message and send that to the collection application. Following the send, the key/value pairs are cleared from the instance and the xAPP is free to start pushing values into the instance again. The send function has the following prototype and returns true on success and false if the measurements could not be sent.

Source and Reporter

The alarm collector has the understanding that a measurement might be sourced from one piece of equipment, or software component, but reported by another. For auditing purposes it makes sense to distinguish these, and as such the metrics object allows the xAPP to identify the case when the source and reporter are something other than the xAPP which is generating the metrics message(s).

The source is the component to which the measurement applies. This could be a network interface card counting packets, a temperature sensor, or the xAPP itself reporting xAPP related metrics. The reporter is the application that is reporting the measurement(s) to the collector.

By default, both reporter and source are assumed to be the xAPP, and the name is automatically determined using the run-time supplied programme name. Should the xAPP need to report measurements for more than one source it has the option to create an instance for every reporter source combination, or to set the reporter and/or source with the generation of each measurement set. To facilitate the ability to change the source and/or the reporter without the need to create a new metrics instance, two setter functions are provided. The prototypes for these are shown in figure 22.

void Set_source( std::string new_source );
void Set_reporter( std::string new_reporter );

Figure 22: Setter functions allowing the reporter and/or source to be set after construction.

CONFIGURATION SUPPORT

The C++ xAPP framework provides the xAPP with an interface to load, parse and receive update notifications on the configuration. The configuration, also known as the xAPP descriptor, is assumed to be a file containing json with a well known structure, with some fields or objects used by an xAPP for configuration purposes. The following paragraphs describe the support that the framework provides to the xAPP with respect to the configuration aspects of the descriptor.

The Config Object

The xAPP must create an instance of the config object in order to take advantage of the support. This is accomplished by using one of two constructors illustrated with code samples in figure 23.

#include <ricxfcpp/config.hpp>

auto cfg = new xapp::Config( );
auto cfg = new xapp::Config( "/var/myapp/config.json"  );

Figure 23: Creating a configuration instance.

The creation of the object causes the file to be found, loaded, after which the xAPP can use the instance functions to access the information it needs.

Available Functions

Once a configuration has been created the following capabilities are available:

  • Get a control value (numeric, string, or boolean)

  • Get the RMR port for the container with the supplied name

  • Set a notification callback function

  • Get the raw contents of the file

Control Values

The controls section of the xAPP descriptor is generally used to supply a flat namespace of key/value pairs which the xAPP can use for configuration. These pairs are supplied by the xAPP author as a part of development, and thus are specific to each xAPP. The framework provides a general set of functions which allows a key to be searched for in this section and returned to the caller. Data is assumed to be one of three types: numeric (double), string, or boolean.

Two methods for each return type are supported with the more specific form allowing the xAPP to supply a default value to be used should the file not contain the requested field. The function prototypes for these are provided in figure 24.

bool Get_control_bool( std::string name, bool defval );
bool Get_control_bool( std::string name );

std::string Get_control_str( std::string name, std::string defval );
std::string Get_control_str( std::string name );

double Get_control_value( std::string name, double defval );
double Get_control_value( std::string name );

Figure 24: The various controls section get functions.

If the more generic form of these functions is used, without a default value, the return values are false, “”, and 0.0 in the respective order of the prototypes in figure 24.

The RMR Port

The messaging section of the xAPP descriptor provides the ability to define one or more RMR listen ports that apply to the xAPP(s) started in a given container. The xAPP may read a port value (as a string) using the defined port name via the Get_port function whose prototype is illustrated in figure 25 below.

std::string Get_port( std::string name );

Figure 25: The get port prototype.

Raw File Contents

While it is not anticipated to be necessary, the xAPP might need direct access to the raw contents of the configuration file. As a convenience the framework provides the Get_contents() function which reads the entire file into a standard library string and returns that to the calling function. Parsing and interpreting the raw contents is then up to the xAPP.

Notification Of Changes

When desired, the xAPP may register a notification callback function with the framework. This callback will be driven any time a change to the descriptor is detected. When a change is detected, the revised descriptor is read into the existing object (overlaying any previous information), before invoking the callback. The callback may then retrieve the updated values and make any adjustments which are necessary. The prototype for the xAPP callback function is described in figure 26.

void cb_name( xapp::Config& c, void* data )

Figure 26: The prototype which the xAPP configuration notify callback must use.

Enabling The Notifications

Notifications are enabled by invoking the Set_callback() function. Once enabled, the framework will monitor the configuration source and invoke the callback upon change. This occurs in a separate thread than the main xAPP thread; it is up to the xAPP to guard against any potential data collisions when evaluating configuration changes. If the xAPP does not register a notification function the framework will not monitor the configuration for changes and the object will have static data. Figure 27 illustrates how the xAPP can define and register a notification callback.

//  notification callback; allows verbose level to change on the fly
void config_chg( xapp::Config& c, void* vdata ) {
  app_ctx* ctx;      // application context

 ctx = (app_ctx *) vdata;
 ctx->vlevel = c->Get_value( "verbose_level", ctx->vlevel );
}

Figure 27: Small notification callback function allowing on the fly verbose level change.

The xAPP would register the config_chg() function as the notification callback using the call illustrated in figure 28.

auto conf = new xapp::Config();
conf->Set_callback( config_chg );

Figure 28: Setting the notification callback and and activating notifications.

xAPP Descriptor Notes

While it is beyond the scope of this document to describe the complete contents of an xAPP descriptor file, it is prudent to mention several items which are related to the information used from the descriptor file. The following paragraphs discuss things which the xAPP developer should be aware of, and keep in mind when using the configuration class.

The RMR Section

There is a deprecated section within the xAPP descriptor which has the title rmr. The messaging section provides more flexibility, and additional information and has been a replacement for the rmr section for all applications. The information in the rmr section should be kept consistent with the duplicated information in the messaging section as long as there are container management and/or platform applications (e.g. Route Manager) which are back level and do not recognise the messaging section. The configuration parsing and support provided by the framework will ignore the rmr section.

EXAMPLE PROGRAMMES

The following sections contain several example programmes which are written on top of the C++ framework. All of these examples are available in the code repository RIC xAPP C++ framework available via the following URL:

class center
``https://gerrit.o-ran-sc.org/r/admin/repos/ric-plt/xapp-frame-cpp``

RMR Dump xAPP

The RMR dump application is an example built on top of the C++ xApp framework to both illustrate the use of the framework, and to provide a useful diagnostic tool when testing and troubleshooting xApps.

The RMR dump xApp isn’t a traditional xApp inasmuch as its goal is to listen for message types and to dump information about the messages received to the TTY much as tcpdump does for raw packet traffic. The full source code, and Makefile, are in the examples directory of the C++ framework repo.

When invoked, the RMR dump program is given one or more message types to listen for. A callback function is registered for each, and the framework Run() function is invoked to drive the process. For each recognised message, and depending on the verbosity level supplied at program start, information about the received message(s) is written to the TTY. If the forwarding option, -f, is given on the command line, and an appropriate route table is provided, each received message is forwarded without change. This allows for the insertion of the RMR dump program into a flow, however if the ultimate receiver of a message needs to reply to that message, the reply will not reach the original sender, so RMR dump is not a complete “middle box” application.

The following is the code for this xAPP. Several functions, which provide logic unrelated to the framework, have been omitted. The full code is in the framework repository.

#include <stdio.h>
#include <unistd.h>
#include <atomic>

#include "ricxfcpp/xapp.hpp"

/*
    Information that the callback needs outside
    of what is given to it via parms on a call
    by the framework.
*/
typedef struct {
    int        vlevel;             // verbosity level
    bool    forward;            // if true, message is forwarded
    int        stats_freq;         // header/stats after n messages
    std::atomic<long>    pcount; // messages processed
    std::atomic<long>    icount; // messages ignored
    std::atomic<int>    hdr;    // number of messages before next header
} cb_info_t;

// ----------------------------------------------------------------------

/*
    Dump bytes to tty.
*/
void dump( unsigned const char* buf, int len ) {
    int        i;
    int        j;
    char    cheater[17];

    fprintf( stdout, "<RD> 0000 | " );
    j = 0;
    for( i = 0; i < len; i++ ) {
        cheater[j++] =  isprint( buf[i] ) ? buf[i] : '.';
        fprintf( stdout, "%02x ", buf[i] );

        if( j == 16 ) {
            cheater[j] = 0;
            fprintf( stdout, " | %s\\n<RD> %04x | ", cheater, i+1 );
            j = 0;
        }
    }

    if( j ) {
        i = 16 - (i % 16);
        for( ; i > 0; i-- ) {
            fprintf( stdout, "   " );
        }
        cheater[j] = 0;
        fprintf( stdout, " | %s\\n", cheater );
    }
}

/*
    generate stats when the hdr count reaches 0. Only one active
    thread will ever see it be exactly 0, so this is thread safe.
*/
void stats( cb_info_t& cbi ) {
    int curv;                    // current stat trigger value

    curv = cbi.hdr--;

    if( curv == 0 ) {                    // stats when we reach 0
        fprintf( stdout, "ignored: %ld  processed: %ld\\n",
            cbi.icount.load(), cbi.pcount.load() );
        if( cbi.vlevel > 0 ) {
            fprintf( stdout, "\\n     %5s %5s %2s %5s\\n",
                "MTYPE", "SUBID", "ST", "PLLEN" );
        }

        cbi.hdr = cbi.stats_freq;        // reset must be last
    }
}

void cb1( xapp::Message& mbuf, int mtype, int subid, int len,
                xapp::Msg_component payload,  void* data ) {
    cb_info_t*    cbi;
    long total_count;

    if( (cbi = (cb_info_t *) data) == NULL ) {
        return;
    }

    cbi->pcount++;
    stats( *cbi );            // gen stats & header if needed

    if( cbi->vlevel > 0 ) {
        fprintf( stdout, "<RD> %-5d %-5d %02d %-5d \\n",
                mtype, subid, mbuf.Get_state(), len );

        if( cbi->vlevel > 1 ) {
            dump(  payload.get(), len > 64 ? 64 : len );
        }
    }

    if( cbi->forward ) {
        // forward with no change to len or payload
        mbuf.Send_msg( xapp::Message::NO_CHANGE, NULL );
    }
}

/*
    registered as the default callback; it counts the
    messages that we aren't giving details about.
*/
void cbd( xapp::Message& mbuf, int mtype, int subid, int len,
                xapp::Msg_component payload,  void* data ) {
    cb_info_t*    cbi;

    if( (cbi = (cb_info_t *) data) == NULL ) {
        return;
    }

    cbi->icount++;
    stats( *cbi );

    if( cbi->forward ) {
        // forward with no change to len or payload
        mbuf.Send_msg( xapp::Message::NO_CHANGE, NULL );
    }
}

int main( int argc, char** argv ) {
    std::unique_ptr<Xapp> x;
    char*    port = (char *) "4560";
    int ai = 1;                    // arg processing index
    cb_info_t*    cbi;
    int        ncb = 0;            // number of callbacks registered
    int        mtype;
    int        nthreads = 1;

    cbi = (cb_info_t *) malloc( sizeof( *cbi ) );
    cbi->pcount = 0;
    cbi->icount = 0;
    cbi->stats_freq = 10;

    ai = 1;
    // very simple flag parsing (no error/bounds checking)
    while( ai < argc ) {
        if( argv[ai][0] != '-' )  {        // break on first non-flag
            break;
        }

        // very simple arg parsing; each must be separate -x -y not -xy.
        switch( argv[ai][1] ) {
            case 'f':                    // enable packet forwarding
                cbi->forward = true;
                break;

            case 'p':                    // define port
                port = argv[ai+1];
                ai++;
                break;

            case 's':                        // stats frequency
                cbi->stats_freq = atoi( argv[ai+1] );
                if( cbi->stats_freq < 5 ) {    // enforce sanity
                    cbi->stats_freq = 5;
                }
                ai++;
                break;

            case 't':                        // thread count
                nthreads = atoi( argv[ai+1] );
                if( nthreads < 1 ) {
                    nthreads = 1;
                }
                ai++;
                break;

            case 'v':            // simple verbose bump
                cbi->vlevel++;
                break;

            case 'V':            // explicit verbose level
                cbi->vlevel = atoi( argv[ai+1] );
                ai++;
                break;

            default:
                fprintf( stderr, "unrecognised option: %s\\n", argv[ai] );
                fprintf( stderr, "usage: %s [-f] [-p port] "
                                "[-s stats-freq]  [-t thread-count] "
                                "[-v | -V n] msg-type1 ... msg-typen\\n",
                                argv[0] );
                fprintf( stderr, "\\tstats frequency is based on # of messages received\\n" );
                fprintf( stderr, "\\tverbose levels (-V) 0 counts only, "
                                "1 message info 2 payload dump\\n" );
                exit( 1 );
        }

        ai++;
    }

    cbi->hdr = cbi->stats_freq;
    fprintf( stderr, "<RD> listening on port: %s\\n", port );

    // create xapp, wait for route table if forwarding
    x = std::unique_ptr<Xapp>( new Xapp( port, cbi->forward ) );

    // register callback for each type on the command line
    while( ai < argc ) {
        mtype = atoi( argv[ai] );
        ai++;
        fprintf( stderr, "<RD> capturing messages for type %d\\n", mtype );
        x->Add_msg_cb( mtype, cb1, cbi );
        ncb++;
    }

    if( ncb < 1 ) {
        fprintf( stderr, "<RD> no message types specified on the command line\\n" );
        exit( 1 );
    }

    x->Add_msg_cb( x->DEFAULT_CALLBACK, cbd, cbi );        // register default cb

    fprintf( stderr, "<RD> starting driver\\n" );
    x->Run( nthreads );

    // return from run() is not expected, but some compilers might
    // compilain if there isn't a return value here.
    return 0;
}

Figure 29: Simple callback application.

Callback Receiver

This sample programme implements a simple message listener which registers three callback functions to process two specific message types and a default callback to handle unrecognised messages.

When a message of type 1 is received, it will send two response messages back to the sender. Two messages are sent in order to illustrate that it is possible to send multiple responses using the same received message.

The programme illustrates how multiple listening threads can be used, but the programme is not thread safe; to keep this example as simple as possible, the counters are not locked when incremented.

Metrics Generation

The example also illustrates how a metrics object instance can be created and used to send appliction metrics to the collector. In this example the primary callback function will genereate metrics with the receipt of each 1000th message.

#include <stdio.h>

#include "ricxfcpp/message.hpp"
#include "ricxfcpp/msg_component.hpp"
#include <ricxfcpp/metrics.hpp>
#include "ricxfcpp/xapp.hpp"

// counts; not thread safe
long cb1_count = 0;
long cb2_count = 0;
long cbd_count = 0;

long cb1_lastts = 0;
long cb1_lastc = 0;

/*
    Respond with 2 messages for each type 1 received
    Send metrics every 1000 messages.
*/
void cb1( xapp::Message& mbuf, int mtype, int subid, int len,
            xapp::Msg_component payload,  void* data ) {
    long now;
    long total_count;

    // illustrate that we can use the same buffer for 2 rts calls
    mbuf.Send_response( 101, -1, 5, (unsigned char *) "OK1\\n" );
    mbuf.Send_response( 101, -1, 5, (unsigned char *) "OK2\\n" );

    cb1_count++;

    if( cb1_count % 1000 == 0 && data != NULL ) {   // send metrics every 1000 messages
        auto x = (Xapp *) data;
        auto msgm = std::shared_ptr<xapp::Message>( x->Alloc_msg( 4096 ) );

        auto m = std::unique_ptr<xapp::Metrics>( new xapp::Metrics( msgm ) );
        m->Push_data( "tst_cb1", (double) cb1_count );
        m->Push_data( "tst_cb2", (double) cb2_count );
        m->Send();
    }
}

// just count messages
void cb2( xapp::Message& mbuf, int mtype, int subid, int len,
            xapp::Msg_component payload,  void* data ) {
    cb2_count++;
}

// default to count all unrecognised messages
void cbd( xapp::Message& mbuf, int mtype, int subid, int len,
            xapp::Msg_component payload,  void* data ) {
    cbd_count++;
}

int main( int argc, char** argv ) {
    Xapp* x;
    char*    port = (char *) "4560";
    int ai = 1;                            // arg processing index
    int nthreads = 1;

    // very simple flag processing (no bounds/error checking)
    while( ai < argc ) {
        if( argv[ai][0] != '-' )  {
            break;
        }

        switch( argv[ai][1] ) {            // we only support -x so -xy must be -x -y
            case 'p':
                port = argv[ai+1];
                ai++;
                break;

            case 't':
                nthreads = atoi( argv[ai+1] );
                ai++;
                break;
        }

        ai++;
    }

    fprintf( stderr, "<XAPP> listening on port: %s\\n", port );
    fprintf( stderr, "<XAPP> starting %d threads\\n", nthreads );

    x = new Xapp( port, true );
    x->Add_msg_cb( 1, cb1, x );        // register callbacks
    x->Add_msg_cb( 2, cb2, NULL );
    x->Add_msg_cb( x->DEFAULT_CALLBACK, cbd, NULL );

    x->Run( nthreads );                // let framework drive
    // control should not return
}

Figure 30: Simple callback application.

Looping Sender

This is another very simple application which demonstrates how an application can control its own listen loop while sending messages. As with the other examples, some error checking is skipped, and short cuts have been made in order to keep the example small and to the point.

#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <iostream>
#include <memory>

#include "ricxfcpp/xapp.hpp"

extern int main( int argc, char** argv ) {
    std::unique_ptr<Xapp> xfw;
    std::unique_ptr<xapp::Message> msg;
    xapp::Msg_component payload;                // special type of unique pointer to the payload

    int    sz;
    int len;
    int i;
    int ai;
    int response_to = 0;                // max timeout wating for a response
    char*    port = (char *) "4555";
    int    mtype = 0;
    int rmtype;                            // received message type
    int delay = 1000000;                // mu-sec delay; default 1s


    // very simple flag processing (no bounds/error checking)
    while( ai < argc ) {
        if( argv[ai][0] != '-' )  {
            break;
        }

        // we only support -x so -xy must be -x -y
        switch( argv[ai][1] ) {
            // delay between messages (mu-sec)
            case 'd':
                delay = atoi( argv[ai+1] );
                ai++;
                break;

            case 'p':
                port = argv[ai+1];
                ai++;
                break;

            // timeout in seconds; we need to convert to ms for rmr calls
            case 't':
                response_to = atoi( argv[ai+1] ) * 1000;
                ai++;
                break;
        }
        ai++;
    }

    fprintf( stderr, "<XAPP> response timeout set to: %d\\n", response_to );
    fprintf( stderr, "<XAPP> listening on port: %s\\n", port );

    // get an instance and wait for a route table to be loaded
    xfw = std::unique_ptr<Xapp>( new Xapp( port, true ) );
    msg = xfw->Alloc_msg( 2048 );

    for( i = 0; i < 100; i++ ) {
        mtype++;
        if( mtype > 10 ) {
            mtype = 0;
        }

        // we'll reuse a received message; get max size
        sz = msg->Get_available_size();

        // direct access to payload; add something silly
        payload = msg->Get_payload();
        len = snprintf( (char *) payload.get(), sz, "This is message %d\\n", i );

        // payload updated in place, prevent copy by passing nil
        if ( ! msg->Send_msg( mtype, xapp::Message::NO_SUBID,  len, NULL )) {
            fprintf( stderr, "<SNDR> send failed: %d\\n", i );
        }

        // receive anything that might come back
        msg = xfw->Receive( response_to );
        if( msg != NULL ) {
            rmtype = msg->Get_mtype();
            payload = msg->Get_payload();
            fprintf( stderr, "got: mtype=%d payload=(%s)\\n",
                rmtype, (char *) payload.get() );
        } else {
            msg = xfw->Alloc_msg( 2048 );
        }

        if( delay > 0 ) {
            usleep( delay );
        }
    }
}

Figure 31: Simple looping sender application.

Alarm Generation

This is an extension of a previous example which sends an alarm during initialisation and clears the alarm as soon as messages are being received. It is unknown if this is the type of alarm that is expected at the collector, but illustrates how an alarm is allocated, raised and cleared.

#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <iostream>
#include <memory>

#include "ricxfcpp/xapp.hpp"
#include "ricxfcpp/alarm.hpp"

extern int main( int argc, char** argv ) {
    std::unique_ptr<Xapp> xfw;
    std::unique_ptr<xapp::Message> msg;
    xapp::Msg_component payload;                // special type of unique pointer to the payload
    std::unique_ptr<xapp::Alarm>    alarm;

    bool received = false;                // false until we've received a message
    int    sz;
    int len;
    int i;
    int ai = 1;
    int response_to = 0;                // max timeout wating for a response
    char*    port = (char *) "4555";
    int    mtype = 0;
    int rmtype;                            // received message type
    int delay = 1000000;                // mu-sec delay; default 1s


    // very simple flag processing (no bounds/error checking)
    while( ai < argc ) {
        if( argv[ai][0] != '-' )  {
            break;
        }

        // we only support -x so -xy must be -x -y
        switch( argv[ai][1] ) {
            // delay between messages (mu-sec)
            case 'd':
                delay = atoi( argv[ai+1] );
                ai++;
                break;

            case 'p':
                port = argv[ai+1];
                ai++;
                break;

            // timeout in seconds; we need to convert to ms for rmr calls
            case 't':
                response_to = atoi( argv[ai+1] ) * 1000;
                ai++;
                break;
        }
        ai++;
    }

    fprintf( stderr, "<XAPP> response timeout set to: %d\\n", response_to );
    fprintf( stderr, "<XAPP> listening on port: %s\\n", port );

    // get an instance and wait for a route table to be loaded
    xfw = std::unique_ptr<Xapp>( new Xapp( port, true ) );
    msg = xfw->Alloc_msg( 2048 );


    // raise an unavilable alarm which we'll clear on the first recevied message
    alarm =  xfw->Alloc_alarm( "meid-1234"  );
    alarm->Raise( xapp::Alarm::SEV_MINOR, 13, "unavailable", "no data recevied" );

    for( i = 0; i < 100; i++ ) {
        mtype++;
        if( mtype > 10 ) {
            mtype = 0;
        }

        // we'll reuse a received message; get max size
        sz = msg->Get_available_size();

        // direct access to payload; add something silly
        payload = msg->Get_payload();
        len = snprintf( (char *) payload.get(), sz, "This is message %d\\n", i );

        // payload updated in place, prevent copy by passing nil
        if ( ! msg->Send_msg( mtype, xapp::Message::NO_SUBID,  len, NULL )) {
            fprintf( stderr, "<SNDR> send failed: %d\\n", i );
        }

        // receive anything that might come back
        msg = xfw->Receive( response_to );
        if( msg != NULL ) {
            if( ! received ) {
                // clear the alarm on first received message
                alarm->Clear( xapp::Alarm::SEV_MINOR, 13, "messages flowing", "" );
                received = true;
            }

            rmtype = msg->Get_mtype();
            payload = msg->Get_payload();
            fprintf( stderr, "got: mtype=%d payload=(%s)\\n",
                rmtype, (char *) payload.get() );
        } else {
            msg = xfw->Alloc_msg( 2048 );
        }

        if( delay > 0 ) {
            usleep( delay );
        }
    }
}

Figure 32: Simple looping sender application with alarm generation.

Configuration Interface

This example is a simple illustration of how the configuration file support (xAPP descriptor) can be used to suss out configuration parameters before creating the Xapp object. The example also illustrates how a notification callback can be used to react to changes in the configuration.

#include <stdio.h>

#include "ricxfcpp/config.hpp"
#include "ricxfcpp/message.hpp"
#include "ricxfcpp/msg_component.hpp"
#include <ricxfcpp/metrics.hpp>
#include "ricxfcpp/xapp.hpp"

int vlevel = 0;                    // verbose mode set via config

/*
    Just print something to the tty when we receive a message
    and are in verbose mode.
*/
void cb1( xapp::Message& mbuf, int mtype, int subid, int len,
            xapp::Msg_component payload,  void* data ) {
    if( vlevel > 0 ) {
        fprintf( stdout, "message received is %d bytes long\\n", len );
    }
}

/*
    Driven when the configuration changes. We snarf the verbose
    level from the new config and update it. If it changed to
    >0, incoming messages should be recorded with a tty message.
    If set to 0, then tty output will be disabled.
*/
void config_cb( xapp::Config& c, void* data ) {
    int* vp;

    if( (vp = (int *) data) != NULL ) {
        *vp = c.Get_control_value( "verbose_level", *vp );
    }
}

int main( int argc, char** argv ) {
    Xapp*    x;
    int        nthreads = 1;
    std::unique_ptr<xapp::Config> cfg;

    // only parameter recognised is the config file name
    if( argc > 1 ) {
        cfg = std::unique_ptr<xapp::Config>( new xapp::Config( std::string( argv[1] ) ) );
    } else {
        cfg = std::unique_ptr<xapp::Config>( new xapp::Config( ) );
    }

    // must get a port from the config; no default
    auto port = cfg->Get_port( "rmr-data" );
    if( port.empty() ) {
        fprintf( stderr, "<FAIL> no port in config file\\n" );
        exit( 1 );
    }

    // dig other data from the config
    vlevel = cfg->Get_control_value( "verbose_level", 0 );
    nthreads = cfg->Get_control_value( "thread_count", 1 );
    // very simple flag processing (no bounds/error checking)

    if( vlevel > 0 ) {
        fprintf( stderr, "<XAPP> listening on port: %s\\n", port.c_str() );
        fprintf( stderr, "<XAPP> starting %d threads\\n", nthreads );
    }

    // register the config change notification callback
    cfg->Set_callback( config_cb, (void *) &vlevel );

    x = new Xapp( port.c_str(), true );
    x->Add_msg_cb( 1, cb1, x );        // register message callback

    x->Run( nthreads );                // let framework drive
    // control should not return
}

Figure 33: Simple application making use of the configuration object.