NPR Tutorial >> Writing A Plugin | TOC |
The source code for all of the predefined plugins are in subdirectories of ~onl/npr/plugins/ in the ONL filesystem. For example, ~onl/npr/plugins/mycount/ is the directory for the mycount plugin, and ~onl/npr/plugins/mycount/mycount.c is the source file for the plugin. These files are important to the plugin developer because most developers start from the source code for a plugin that serves as the starting point for the code. All of the source code have the same base, and the null plugin is the required plugin code stripped of all functionality other than packet forwarding. Every plugin directory has a README file that describes the plugin and the degree to which the plugin has been tested. Most of these plugins are summarized in the table in the NPR Tutorial page " NPR Tutorial => Router Plugins => Predefined Plugins".
As discussed later, the files in ~onl/npr/ may also be worthwhile: the .h files in the subdirectory pluginFramework/ contain the functions that make up the plugin API. Also, reading the code may give you a better understanding of how plugins work. And sometimes, you can get copy the code and modify it for your own use.
The source code of most predefined plugins is organized into the following segments:
Typically, these lines come from copying plugin code that you plan to use as a base. They include the functions from the plugin API. Sometimes developers augment the functions with their own functions. In the examples, these functions are usually collected together in a file called plugin_helpers.h.
These declarations define variables whose values are often used by support functions and are set as a side-effect of calling functions that send and receive messages from various communication rings in the NPR. They should NOT be deleted.
When writing a new simple plugin, handle_pkt_user() and handle_msg() are the two most likely functions will change. Usually, only a few changes are needed in plugin_init_user(). And callback_user doesn't need to be changed at all unless you need to execute code periodically. This is the case when developing the mycount plugin by modifying either the count or debug plugin code.
You should never have to modify this part of the code when writing a simple plugin. However, when we developed the delay plugin by starting with the mycount, we did have to modify parts of this code. But the delay plugin is not a simple plugin.
We installed the mycount plugin at NPR 1 (left NPR) and directed all packets coming into port 1.1 to go to the plugin. The plugin counts the packet by incrementing two counters and then forwards the packet onto the Queue Manager. It also displays a debug message whenever it gets a packet and responds to control messages from the RLI. It counts packets in two ways: 1) by incrementing PluginCounter 0 which can be easily displayed in a chart; and 2) by incrementing a normal variable whose value can be displayed by sending the plugin a control message requesting the value of the counter.
The figure (above) shows the effect of sending 10 ping packets from n1p1 to n2p1. The chart shows PluginCounter 0 increasing from 0 to 10 and was obtained through the NPR 1 => Monitoring => Plugin Counter menu item. The Plugin Command Log window shows the value of the variable pkt_count which was displayed when we sent a get command to the plugin. The xterm window shows the debug messages that were sent from the plugin a meta-packet was handled by the handle_pkt() routine. These messages were stored in the file debug.log after configuring the debugging by selecting NPR 1 => Plugin Debugging. Note that the packet count is displayed as a hexadecimal number (e.g., 'pkt xa' is referring to packet 10 decimal).
At the beginning of the mycount plugin code (past the initial comment lines), we see the following two lines:
#include "plugin_api.h" #include "./plugin_helpers.h"
The file ./plugin_helpers.h contain three helper functions:
The next group of lines we see are the thread-specific global variable declarations and should not be modified:
__declspec(gp_reg) int dlNextBlock; // where to send packets to next __declspec(gp_reg) int dlFromBlock; // where to get packets from __declspec(gp_reg) int msgNextBlock; // where to send control messages to next __declspec(gp_reg) int msgFromBlock; // where to get control messages from // see ring_formats.h for struct definitions volatile __declspec(gp_reg) plc_plugin_data ring_in; // ring data from PLC volatile __declspec(gp_reg) plugin_out_data ring_out; // ring data to next block __declspec(local_mem) unsigned int timeout; // time (cycles) between callbacks __declspec(gp_reg) unsigned int pluginId; // plugin id (0...7)
Note that the above variables appear to be global (outside the scope of any of the functions) and they are. But also, they will actually be replicated for each thread when the plugin is loaded ... unless they have the shared attribute (which they don't here). The variables are used in the ways summarized in the following table:
Variable | Usage |
---|---|
dlNextBlock | Where to send the next meta-packet |
dlFromBlock | Where to get the next meta-packet |
msgNextBlock | Where to get the next control message |
msgFromBlock | Where to send the next control message |
ring_in | Incoming data from the PLC block |
ring_out | Data to be sent to the next block (QM) |
timeout | Callback timeout (not used here) |
pluginId | The plugin ME number, a number between 0 and 4 |
For example, dlNextBlock is usually set to the value of QM. QM is a constant that is defined in ~onl/npr/pluginFramework/plugin_dl.h to be 0 and stands for the Queue Manager block.
Note that every thread is loaded with and executes a main() function. The main function does initialization and then executes a code segment based on the hardware context returned by the ctx() call.
void main() { 1 int c; 2 plugin_init(); 3 dl_sink_init(); 4 dl_source_init(); 5 c = ctx(); // get the thread's hardware context# (0-7) 6 if(c >= FIRST_PACKET_THREAD && c <= LAST_PACKET_THREAD) 7 { 8 while(1) { handle_pkt(); } 9 } 10 #ifdef MESSAGE_THREAD 11 else if(c == MESSAGE_THREAD) 12 { 13 while(1) { handle_msg(); } 14 } 15 #endif 16 #ifdef CALLBACK_THREAD 17 else if(c == CALLBACK_THREAD) 18 { 19 while(1) { callback(); } 20 } 21 #endif }
The function plugin_init() contains the plugin initialization code that is common to all plugins. It initializes global variables (e.g., pluginId) and then calls plugin_init_user() to allow users to do their own initialization processing.
void plugin_init() { 1 timeout = 10000; 2 dlNextBlock = QM; 3 switch(__ME()) 4 { 5 case 0x7: 6 pluginId = 0; 7 dlFromBlock = PACKET_IN_RING_0; 8 msgFromBlock = MESSAGE_IN_RING_0; 9 msgNextBlock = MESSAGE_OUT_RING_0; 10 break; 11 case 0x10: 12 pluginId = 1; 13 dlFromBlock = PACKET_IN_RING_1; 14 msgFromBlock = MESSAGE_IN_RING_1; 15 msgNextBlock = MESSAGE_OUT_RING_1; 16 break; 17 ... Cases 0x11, 0x12, 0x13, default ... 18 } 19 plugin_init_user(); // user hook }
The function plugin_init_user() contains the plugin initialization code that is specific to the plugin.
void plugin_init_user() { 1 if(ctx() == 0) 2 { 3 pkt_count = 0; 4 debug_on = 1; 5 sleep(timeout); 6 helper_plugin_cntr_zero( 0 ); 7 } }
The function handle_pkt() is called from main() if the thread context is one of the packet handling threads.
void handle_pkt() { 1 dl_source_packet(dlFromBlock); 2 default_format_out_data(dlNextBlock); 3 handle_pkt_user(); 4 dl_sink_packet(dlNextBlock); }
The function handle_pkt_user() does the actual packet processing.
void handle_pkt_user() { 1 __declspec(local_mem) char dbgmsg[28] = "got pkt x"; 2 __declspec(local_mem) char pkt_count_str[9]; 3 ++pkt_count; 4 if( debug_on ) { 5 onl_api_int2str( pkt_count, pkt_count_str ); 6 strcat_lmem(dbgmsg, pkt_count_str); 7 onl_api_debug_message(msgNextBlock, dbgmsg); 8 } 9 onl_api_plugin_cntr_inc(pluginId, 0); }
The function handle_msg() is called from main() if the thread context is the message handling thread. Some plugins call handle_msg_user() to do the plugin-specific processing of control messages, but mycount does not.
void handle_msg() { 1 __declspec(gp_reg) unsigned int message[8]; 2 __declspec(local_mem) char in_msgstr[28]; 3-7 ... other declarations ... 8 9 for( i=0; i<8; ++i ) { message[i] = 0; } 10 dl_source_message( msgFromBlock, message ); 11 12-14 ... make a copy of the request message header ... 15 16 onl_api_intarr2str( &message[1], in_msgstr ); 17-31 ... process the request which should be "z", "d" or "g" ... 32 33-37 ... form the control message response header ... 38 dl_sink_message(msgNextBlock, message); }
We see the general structure of handle_msg(). Line 10 inputs the message in raw form, and line 38 outputs the response message.
1 __declspec(gp_reg) unsigned int message[8]; 2 __declspec(local_mem) char in_msgstr[28]; 3 __declspec(local_mem) char pkt_count_str[28]; 4 __declspec(gp_reg) onl_api_ctrl_msg_hdr hdr; 5 __declspec(gp_reg) int i; 6 __declspec(local_mem) char out_msgstr[28]; 7 __declspec(local_mem) char BAD_op_err[8] = "BAD OP";
However, note that the __declspec() portions of the declarations indicate the variables are to be either in general-purpose registers or local memory. Also, note that the type onl_api_ctrl_msg_hdr is defined as a union between an int and several bit fields. The definition of onl_api_ctrl_msg_hdr is in ~onl/npr/pluginFramework/plugin_dl.h.
Now we discuss the processing that precedes the command processing code. In most cases, these lines should never be changed.
9 for( i=0; i<8; ++i ) { message[i] = 0; } 10 dl_source_message( msgFromBlock, message ); 11 12 hdr.value = message[0]; 13 if( hdr.type != CM_CONTROLMSG ) { return; } 14 if( hdr.response_requested != 1 ) { return; }
Now we discuss the command processing part. If we look only at the control structure of the code, it is obvious that the code is looking to see if the first character in the message payload is one of the three characters 'z', 'd' or 'g' These characters represent the commands zero counters, toggle debug flag, and get counts. There is currently no standard for the format of control messages other than that they are a sequence of strings. The mycount plugin uses the simplest form of command. The delay plugin uses much longer command names (e.g., "=counts", "delay=", "=delay", "reset") and uses SRAM instead of local memory for processing command names. The one difficulty with using local memory is that there is only 4,096 words of local memory.
17 if( in_msgstr[0] == 'z' ) { 18 pkt_count = 0; 19 helper_plugin_cntr_zero( 0 ); 20 } else if( in_msgstr[0] == 'd' ) { 21 debug_on = (debug_on+1) % 2; 22 } else if( in_msgstr[0] == 'g' ) { 23 helper_ultodec_lmem( pkt_count, pkt_count_str ); 24 strcpy_lmem( out_msgstr, pkt_count_str ); 25 if( onl_api_str2intarr(out_msgstr, &message[1]) < 0 ) 26 { return; } 27 } else { 28 strcpy_lmem( out_msgstr, BAD_op_err ); 29 if( onl_api_str2intarr(out_msgstr, &message[1]) < 0 ) 30 { return; } 31 }
The code that sets up the header of the reply message is straightforward, and you should not have to modify these lines.
33 hdr.type = CM_CONTROLMSGRSP; 34 hdr.response_requested = 0; 35 hdr.num_words = 7; 36 message[0] = hdr.value;
You should now have a better appreciation of how to write a simple plugin in which the packet handling involves getting a meta-packet, doing some simple counting, and then sending the meta-packet onto the Queue Manager.
There are a number of issues that we have glossed over or just ommitted. We explain the meaning of the lines in the file Makefile shown below to clarify one such issue.
# set spill based on your spilling needs # 8 = spill to Local Memory then SRAM # 7 = no spill # 6 = spill to SRAM only # 5 = spill to Local Memory then Next Neighbor regs # 4 = spill to Next Neighbor regs then Local Memory # 3 = spill to Local Memory only # 2 = spill to Next Neighbor regs only # 1 = spill to Next Neighbor regs then Local Memory then SRAM # 0 = spill to Local Memory then Next Neighbor regs then SRAM SPILL=3
All but the last line are comments. The mycount Makefile has chosen SPILL=3 which means that when the compiler runs out of registers when attempting to allocate registers to variables, it will try to use local memory. And if local memory runs out of space, the compiler will output an error message and quit. This choice can be significant if you were trying to write a larger plugin because the local memory in each microengine contains only 640 32-bit words. Luckily, this is not a problem with the mycount plugin.
One reason for choosing the SPILL=3 option is that the plugin will not use additional SRAM. Later, we will see that the delay plugin does use SRAM, and we will have to change the Makefile to avoid SRAM reference conflicts if we plan to load the plugin onto more than one ME. Because the mycount plugin doesn't use additional SRAM, we could load the mycount plugin into any or all of the five plugin MEs without worrying about SRAM usage by the compiler.
We now need to cover three topics to give a more complete view of NPR plugins:
For example, if we use an auxilliary filter to send a copy of a meta-packet to a plugin for accounting purposes, we will want to drop the packet after we have done updating counters. These processing functions include dropping a packet, reading various protocol headers, reading the packet payload, modifying a packet, or copying a packet.
For example, if we want to implement a different packet scheduling protocol without changing the Queue Manager, we may need a plugin that stores packets into its own queues until it is time for the packet to be transmitted.
We showed how to output debug messages. But this approach is the most basic approach to testing and is limited to low rate traffic (e.g., 1-3 packets per second) since the debug processing represents a significant amount of overhead.
Revised: Thu, Nov 13, 2008
NPR Tutorial >> Writing A Plugin | TOC |