In both C and C++, two things must be done to define event handlers:
In C++, one or more new classes must be derived from the Asn1NamedEventHandler and/or the Asn1ErrorHandler base classes. All pure virtual methods must be implemented.
In C, a function with an appropriate signature must be created for each function pointer in the struct; the behavior of null function pointers is undefined. The error handler function, if one is desired, must also be defined.
Objects of these classes (or in C, an instance of the Asn1NamedCEventHandler struct) must be created and registered prior to calling the generated decode method or function.
The best way to illustrate this procedure is through examples. We will first show a C++ and then a C version of a simple event handler application to provide a customized formatted printout of the fields in a PER message. Then we will show a simple error handler that will ignore unrecognized fields in a BER message.
Example 1: A Formatted Print Handler (C++)
The ASN1C evaluation and distribution kits include a sample program for doing a formatted print of parsed data. This code can be found in the cpp/sample_per/eventHandler directory. Parts of the code will be reproduced here for reference, but refer to this directory to see the full implementation.
The format for the printout will be simple. Each element name will be printed followed by an equal sign (=) and an open brace ({) and newline. The value will then be printed followed by another newline. Finally, a closing brace (}) followed by another newline will terminate the printing of the element. An indentation count will be maintained to allow for a properly indented printout.
A header file must first be created to hold our print handler class definition (or the definition could be added to an existing header file). This file will contain a class derived from the Asn1NamedEventHandler base class as follows:
class PrintHandler : public Asn1NamedEventHandler { protected: const char* mVarName; int mIndentSpaces; public: PrintHandler (const char* varName); ~PrintHandler (); void indent (); virtual void startElement (const char* name, int index = -1); virtual void endElement (const char* name, int index = -1); virtual void boolValue (OSBOOL value); ... other virtual contents method declarations }
In this definition, we chose to add the mVarName and mIndentSpaces member variables to keep track of these items. The user is free to add any type of member variables he or she wants. The only firm requirement in defining this derived class is the implementation of the virtual methods.
We implement these virtual methods as follows:
In startElement, we print the name, equal sign, and opening brace:
void PrintHandler::startElement (const char* name, int index) { indent(); printf (“%s = {\n”, name); mIndentLevel++; }
In this simplified implementation, we simply indent (this is another private method within the class) and print out the name, equal sign, and opening brace. We then increment the indent level. Note that this is a highly simplified form. We don’t even bother to check if the index argument is greater than or equal to zero. This would determine if a ‘[x]’ should be appended to the element name. In the sample program that is included with the compiler distribution, the implementation is complete.
In endElement, we simply terminate our brace block as follows:
void PrintHandler::endElement (const char* name, int index) { mIndentLevel--; indent(); printf (“}\n”); }
Next, we need to create an object of our derived class and register it prior to invoking the decode method. In the reader.cpp program, the following lines do this:
// Create and register an event handler object PrintHandler* pHandler = new PrintHandler ("employee"); decodeBuffer.addEventHandler (pHandler);
The addEventHandler method defined in the Asn1MessageBuffer base class is the mechanism used to do this. Note that event handler objects can be stacked. Several can be registered before invoking the decode function. When this is done, the entire list of event handler objects is iterated through and the appropriate event handling callback function invoked whenever a defined event is encountered.
The implementation is now complete. The program can now be compiled and run. When this is done, the resulting output is as follows:
employee = { name = { givenName = { "John" } initial = { "P" } familyName = { "Smith" } } ...
This can certainly be improved. For one thing it can be changed to print primitive values out in a “name = value” format (i.e., without the braces). But this should provide the general idea of how it is done.
Example 2: A Formatted Print Handler (C)
As with the C++ version, a C version of the sample is available in the c/sample_per/eventHandler directory.
A header file containing all function declaratios must be created. In this example, an initializePrintHandler(Asn1NamedCEventHandler *printHandler, const char* varname) function is also declared, which will be used to populate the Asn1NamedCEventHandler struct:
Asn1NamedCEventHandler printHandler; void initializePrintHandler (Asn1NamedCEventHandler *printHandler, const char* varname); void finishPrint(); void indent (); void printStartElement (const char* name, int index ); void printEndElement (const char* name, int index ); void printBoolValue (OSBOOL value); void printIntValue (OSINT32 value); ...
A corresponding *.c file (printHandler.c, in this case) contains the definitions of these functions:
static int gs_IndentSpaces; void initializePrintHandler (Asn1NamedCEventHandler *printHandler, const char* varname) { printHandler->startElement = &printStartElement; printHandler->endElement = &printEndElement; printHandler->boolValue = &printBoolValue; printHandler->intValue = &printIntValue; ... } ... void printStartElement (const char* name, int index) { indent (); printf (name); if (index >= 0) printf ("[%d]", index); printf (" = {\n"); gs_IndentSpaces += 3; } void printEndElement (const char* name, int index) { gs_IndentSpaces -= 3; indent (); printf ("}\n"); }
As in Example 1, a variable gs_IndentSpaces is used to keep track of indentation.
Next, the reader program will need to create an Asn1NamedCEventHandler variable, populate it (via initializePrintHandler), and add it to the decode context:
int main (int argc, char** argv) { PersonnelRecord employee; OSCTXT ctxt; ... int i, len, stat; Asn1NamedCEventHandler printHandler; ASN1TAG tag; ... /* initialize print handler */ initializePrintHandler(&printHandler, "employee"); ... /* Add event handler to list */ rtAddEventHandler (&ctxt, &printHandler); /* Call compiler generated decode function */ stat = asn1D_PersonnelRecord (&ctxt, &employee, ASN1EXPL, 0); ... }
The rtAddEventHandler function used to push the event handler into the decode context is defined in asn1CEvtHndlr.h. This can be done multiple times, as with C++, and every event will trigger the appropriate callback function of each event handler.
Example 3: An Error Handler
Despite the addition of things like extensibility and version brackets, ASN.1 implementations get out-of-sync. For situations such as this, the user needs some way to intervene in the parsing process to set things straight. This is faulttolerance – the ability to recover from certain types of errors.
The error handler interface is provided for this purpose. The concept is simple. Instead of throwing an exception and immediately terminating the parsing process, a user defined callback function is first invoked to allow the user to check the error. If the user can fix the error, all he or she needs to do is apply the appropriate patch and return a status of 0. The parser will be none the wiser. It will continue on thinking everything is fine.
This interface is probably best suited for recovering from errors in BER or DER instead of PER. The reason is the TLV format of BER makes it relatively easy to skip an element and continue on. It is much more difficult to find these boundaries in PER.
Our example can be found in the cpp/sample_ber/errorHandler subdirectory. In this example, we have purposely added a bogus element to one of the constructs within an encoded employee record. The error handler will be invoked when this element is encountered. Our recovery action will simply be to print out a warning message, skip the element, and continue.
As before, the first step is to create a class derived from the Asn1ErrorHandler base class. This class is as follows:
class MyErrorHandler : public Asn1ErrorHandler { public: // The error handler callback method. This is the method // that the user must override to provide customized // error handling.. virtual int error (OSCTXT* pCtxt, ASN1CCB* pCCB, int stat); } ;
Simple enough. All we are doing is providing an implementation of the error method.
Implementing the error method requires some knowledge of the run-time internals. In most cases, it will be necessary to somehow alter the decoding buffer pointer so that the same field isn’t looked at again. If this isn’t done, an infinite loop can occur as the parser encounters the same error condition over and over again. The run-time functions xd_NextElement or xd_OpenType might be useful in the endeavor as they provide a way to skip the current element and move on to the next item.
Our sample handler corrects the error in which an unknown element is encountered within a SET construct. This will cause the error status ASN_E_NOTINSET to be generated. When the error handler sees this status, it prints information on the error that was encountered to the console, skips to the next element, and then returns an 0 status that allows the decoder to continue. If some other error occurred (i.e., status was not equal to ASN_E_NOTINSET), then the original status is passed out which forces the termination of the decoding process.
The full text of the handler is as follows:
int MyErrorHandler::error (OSCTXT* pCtxt, ASN1CCB* pCCB, int stat) { // This handler is set up to look explicitly for ASN_E_NOTINSET // errors because we know the SET might contain some bogus elements.. if (stat == ASN_E_NOTINSET) { // Print information on the error that was encountered printf ("decode error detected:\n"); rtErrPrint (pCtxt); printf ("\n"); // Skip element xd_NextElement (pCtxt); // Return an OK status to indicate parsing can continue return 0; } else return stat; // pass existing status back out }
Now we need to register the handler. Unlike event handlers, only a single error handler can be registered. The method to do this in the message buffer class is setErrorHandler. The following two lines of code in the reader program register the handler:
MyErrorHandler errorHandler; decodeBuffer.setErrorHandler (&errorHandler);
The error handlers can be as complicated as you need them to be. You can use them in conjunction with event handlers in order to figure out where you are within a message in order to look for a specific error at a specific place. Or you can be very generic and try to continue no matter what.
It should be noted that implementing an error handler in C does not involve a struct at all. It is only necessary to implement a function with the appropriate signature (specified above) and to pass a pointer to it to the decode buffer. So, with a error handler function:
static int myErrorHandler (OSCTXT *pctxt, ASN1CCB *pCCB, int stat) { // Error-handling code goes here... }
the function is set in the decode context by calling:
rtSetErrorHandler(&ctxt, &myErrorHandler);
where ctxt is an OSCTXT.
Also, just as in C++, there can be only one error handler set at a time.
Example 4: A Pure Parser to Convert PER-encoded Data to XML
A pure-parser is created by using the -notypes option along with the -events option. In this case, no backing data types to hold decoded data are generated. Instead, parsing functions are generated that store the data internally within local variables inside the parsing functions. This data is dispatched to the callback functions and immeditely disposed of upon return from the function. It is up to the user to decide inside the callback handler what they want to keep and they must make copies at that time. The result is a very fast and low-memory consuming parser that is ideal for parsing messages in which only select parts of the messages are of interest.
Another use case for pure-parser functions is validation. These functions can be used to determine if a PER message is valid without going through the high overhead operation of decoding. They can be used on the front-end of an application to reject invalid messages before processing of the messages is done. In some cases, this can result in significantly increased performance.
An example of a pure-parser can be found in the cpp/sample_per/per2xmlEH directory. This program uses a pure-parser to convert PER-encoded data into XML. The steps in creating an event handler are the same as in Example 1 above. An implementation of the Asn1NamedEventHandler interface must be created. This is done in the xmlHandler.h and xmlHandler.cpp files. A detailed discussion of this code will not be provided here. What it does in a nutshell is adds the angle brackets (< >) around the element names in the startElement and endElement callbacks. The data callbacks simply output a textual representation of the data as they do in the print handler case.
The only difference in reader.cpp from the other examples is that:
There is no declaration of an employee variable to hold decoded data because no type for this variable was generated, and
The Parse method is invoked instead of the Decode method. This is the generated method definition for a pureparser. If one examines the generated class definitions, they will see that no Encode or Decode methods were generated.
Compiling and running this program will show the encoded PER message written to stdout as an XML message. The resulting message is also saved in the message.xml file.