X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=blobdiff_plain;f=docs%2Fuser-guide.rst;h=d9137a3a273da24817d6bd83c6c72b2ae98dc52b;hb=d486a17c04f3d6d865f787168d446f4cfea3be25;hp=a4ab5e9a0bd1c7a2310cf4015627585abfb1acb8;hpb=f28cd746c57d9b23f2922ec2b980bdb6c5765a2f;p=ric-plt%2Fxapp-frame-cpp.git diff --git a/docs/user-guide.rst b/docs/user-guide.rst index a4ab5e9..d9137a3 100644 --- a/docs/user-guide.rst +++ b/docs/user-guide.rst @@ -1,831 +1,2139 @@ - - -.. 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. - - -============================================================================================ -RIC xAPP C++ Framework -============================================================================================ --------------------------------------------------------------------------------------------- -User's Guide --------------------------------------------------------------------------------------------- - -Introduction -============================================================================================ - -The C++ framework allows the programmer to create an xApp -object instance, and to use that instance as the logic base. -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. - -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. - -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 - #include - int main( ) { - std::unique_ptr xapp; - char* listen_port = (char *) "4560"; //RMR listen port - bool wait4table = true; // wait for a route table - xapp = std::unique_ptr( - 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( Message& m, int mtype, int subid, - int payload_len, Msg_component payload, - void* usr_data ); - - - Figure 2: Callback function signature - - The parameters passed to the callback function are as - follows: &multi_space - - - 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 - #include - long m1000_count = 0; // message counters, one for each type - long m1001_count = 0; - // callback function that will increase the appropriate counter - void cbf( Message& mbuf, int mtype, int subid, int len, - Msg_component payload, void* data ) { - long* counter; - if( (counter = (long *) data) != NULL ) { - (*counter)++; - } - } - int main( ) { - std::unique_ptr xapp; - char* listen_port = (char *) "4560"; - bool wait4table = false; - xapp = std::unique_ptr( - 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: - - - - $1 Replying to the sender of a received message - - $1 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 response ); - bool Send_response( int response_len, std::shared_ptr 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( Message::NO_CHANGE, 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 payload ); - bool Send_msg( int mtype, int subid, int payload_len, - unsigned char* payload ); - bool Send_msg( int payload_len, - std::shared_ptr 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. - -Example Programmes -============================================================================================ - - The following sections contain several example programmes - which are written on top of the C++ framework. - -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 - #include - #include - #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 pcount; // messages processed - std::atomic icount; // messages ignored - std::atomic 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, " 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 %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( Message& mbuf, int mtype, int subid, int len, - 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, " %-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( Message::NO_CHANGE, NULL ); - } - } - /* - registered as the default callback; it counts the - messages that we aren't giving details about. - */ - void cbd( Message& mbuf, int mtype, int subid, int len, - 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( Message::NO_CHANGE, NULL ); - } - } - int main( int argc, char** argv ) { - std::unique_ptr 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, " listening on port: %s\\n", port ); - // create xapp, wait for route table if forwarding - x = std::unique_ptr( new Xapp( port, cbi->forward ) ); - // register callback for each type on the command line - while( ai < argc ) { - mtype = atoi( argv[ai] ); - ai++; - fprintf( stderr, " capturing messages for type %d\\n", mtype ); - x->Add_msg_cb( mtype, cb1, cbi ); - ncb++; - } - if( ncb < 1 ) { - fprintf( stderr, " 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, " 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 8: 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. - - - :: - - #include - #include "ricxfcpp/message.hpp" - #include "ricxfcpp/msg_component.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 - void cb1( Message& mbuf, int mtype, int subid, int len, - 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++; - } - // just count messages - void cb2( Message& mbuf, int mtype, int subid, int len, - Msg_component payload, void* data ) { - cb2_count++; - } - // default to count all unrecognised messages - void cbd( Message& mbuf, int mtype, int subid, int len, - 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, " listening on port: %s\\n", port ); - fprintf( stderr, " starting %d threads\\n", nthreads ); - x = new Xapp( port, true ); - x->Add_msg_cb( 1, cb1, NULL ); // 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 9: 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 - #include - #include - #include - #include - #include "ricxfcpp/xapp.hpp" - extern int main( int argc, char** argv ) { - std::unique_ptr xfw; - std::unique_ptr msg; - 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, " response timeout set to: %d\\n", response_to ); - fprintf( stderr, " listening on port: %s\\n", port ); - // get an instance and wait for a route table to be loaded - xfw = std::unique_ptr( 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, Message::NO_SUBID, len, NULL )) { - fprintf( stderr, " 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 10: Simple looping sender application. - +============ +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 + #include + int main( ) { + std::unique_ptr xapp; + char* listen_port = (char *) "4560"; //RMR listen port + bool wait4table = true; // wait for a route table + + xapp = std::unique_ptr( + 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 + #include + 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; + char* listen_port = (char *) "4560"; + bool wait4table = false; + + xapp = std::unique_ptr( + 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 response ); + + bool Send_response( int response_len, std::shared_ptr 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 payload ); + + bool Send_msg( int mtype, int subid, int payload_len, + unsigned char* payload ); + + bool Send_msg( int payload_len, + std::shared_ptr 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 + + 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 Alloc_alarm( ); + std::unique_ptr Alloc_alarm( std::string meid ); + std::unique_ptr 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 Alloc_metrics( ); + std::unique_ptr Alloc_metrics( std::string source ); + std::unique_ptr Alloc_metrics( std::string reporter, std::string source ); + +Figure 17: The framework constructors for creating an +instance of the metrics object. + + +:: + + + #include + + char* port = (char *) "4560"; + + auto x = std::unique_ptr( new Xapp( port ) ); + auto reading = std::shared_ptr( 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 msg ); + Metrics( std::shared_ptr msg, std::string msource ); + Metrics( std::shared_ptr msg, std::string reporter, std::string msource ); + +Figure 19: Metrics object constructors. + + +:: + + #include + + char* port = (char *) "4560"; + + auto x = std::unique_ptr( new Xapp( port ) ); + auto msg = std::shared_ptr( x->Alloc_msg( 4096 ) ); + auto reading = std::shared_ptr( 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 + + 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 + #include + #include + + #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 pcount; // messages processed + std::atomic icount; // messages ignored + std::atomic 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, " 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 %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, " %-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 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, " listening on port: %s\\n", port ); + + // create xapp, wait for route table if forwarding + x = std::unique_ptr( new Xapp( port, cbi->forward ) ); + + // register callback for each type on the command line + while( ai < argc ) { + mtype = atoi( argv[ai] ); + ai++; + fprintf( stderr, " capturing messages for type %d\\n", mtype ); + x->Add_msg_cb( mtype, cb1, cbi ); + ncb++; + } + + if( ncb < 1 ) { + fprintf( stderr, " 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, " 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 + + #include "ricxfcpp/message.hpp" + #include "ricxfcpp/msg_component.hpp" + #include + #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( x->Alloc_msg( 4096 ) ); + + auto m = std::unique_ptr( 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, " listening on port: %s\\n", port ); + fprintf( stderr, " 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 + #include + #include + + #include + #include + + #include "ricxfcpp/xapp.hpp" + + extern int main( int argc, char** argv ) { + std::unique_ptr xfw; + std::unique_ptr 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, " response timeout set to: %d\\n", response_to ); + fprintf( stderr, " listening on port: %s\\n", port ); + + // get an instance and wait for a route table to be loaded + xfw = std::unique_ptr( 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, " 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 + #include + #include + + #include + #include + + #include "ricxfcpp/xapp.hpp" + #include "ricxfcpp/alarm.hpp" + + extern int main( int argc, char** argv ) { + std::unique_ptr xfw; + std::unique_ptr msg; + xapp::Msg_component payload; // special type of unique pointer to the payload + std::unique_ptr 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, " response timeout set to: %d\\n", response_to ); + fprintf( stderr, " listening on port: %s\\n", port ); + + // get an instance and wait for a route table to be loaded + xfw = std::unique_ptr( 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, " 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 + + #include "ricxfcpp/config.hpp" + #include "ricxfcpp/message.hpp" + #include "ricxfcpp/msg_component.hpp" + #include + #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 cfg; + + // only parameter recognised is the config file name + if( argc > 1 ) { + cfg = std::unique_ptr( new xapp::Config( std::string( argv[1] ) ) ); + } else { + cfg = std::unique_ptr( new xapp::Config( ) ); + } + + // must get a port from the config; no default + auto port = cfg->Get_port( "rmr-data" ); + if( port.empty() ) { + fprintf( stderr, " 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, " listening on port: %s\\n", port.c_str() ); + fprintf( stderr, " 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. +