blob: d9137a3a273da24817d6bd83c6c72b2ae98dc52b [file] [log] [blame]
============
USER'S GUIDE
============
.. This work is licensed under a Creative Commons Attribution 4.0 International License.
.. SPDX-License-Identifier: CC-BY-4.0
..
.. CAUTION: this document is generated from source in doc/src/*
.. To make changes edit the source and recompile the document.
.. Do NOT make changes directly to .rst or .md files.
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:
.. list-table::
:widths: auto
:header-rows: 0
:class: borderless
* - **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:
.. list-table::
:widths: auto
:header-rows: 0
:class: borderless
* - **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:
.. list-table::
:widths: 15,80
:header-rows: 0
:class: borderless
* - **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:
.. list-table::
:widths: auto
:header-rows: 0
:class: borderless
* - **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.