Core structure of the implementation code

The core work of implementing the module is done in the C code file. As indicated earlier, much of the detail of this will be dependent on the particular module being implemented, and this can only be described by the individual programmer concerned.
However, there is a fairly clearly defined framework that the implementation will need to follow, though this varies slightly depending on the style of the module being implemented (in particular whether it forms a table or a series of individual values). The differences will be covered in the following pages, but we first need to consider the overall shape of the framework, and the elements that are common to all styles. These are essentially the compulsory routines, the common header definitions, and assorted initialisation code.
As with the header file, most of this will be generated automatically by mib2c.

Standard includes

Certain header files are either compulsory, or required so frequently that they should be included as a matter of course. These are as follows:
  #include <config.h>			// local SNMP configuration details
  #include "mib_module_config.h"	// list of which modules are supported 
  #if HAVE_STDLIB_H
  #include <stdlib.h>
  #endif
  #if HAVE_STRING_H
  #include <string.h>
  #else
  #include <strings.h>
  #endif

  #include <sys/types.h>
All of these will usually be the first files to be included.
  #include "mibincl.h"			// Standard set of SNMP includes
  #include "util_funcs.h"		// utility function declarations
  #include "read_config.h"		// if the module uses run-time
					//	configuration controls
  #include "auto_nlist.h"		// structures for a BSD-based
					//	kernel using nlist
  #include "system.h"

  #include "name.h"			// the module-specific header
These conventionally come at the end of the list of includes. In between will come all the standard system-provided header files required for the library functions used in the file.

Module definition

Much of the code defining the contents of the MIB has traditionally been held in the header file. However, much of this has slowly migrated to the code file, and this is now the recommended location for it (as typified by the output of mib2cstruct variableN (where N is the length of the longest suffix in the table). Thus
		struct variable2 example_variables[] = {
			< individual entries go here >
		};
Each entry corresponds to one object in the MIB tree (or one column in the case of table entries), and these should be listed in increasing OID order. A single entry consists of six fields: Thus a typical variable entry would look like:
	{ EXAMPLESTRING, ASN_OCTET_STR, RONLY, var_example, 1, {1}}
If the magic numbers have not been defined in the header file, then they should be defined here, usually comming immediately before the corresponding variable entry. This is the technique used by mib2c.
Note that in practise, only certain sizes of the structure variableN are defined (listed in <agent/var_struct.h>), being sufficient to meet the common requirements. If your particular module needs a non-supported value, the easiest thing is simply to use the next largest value that is supported.

The module also needs to declare the location within the MIB tree where it should be registered. This is done using a declaration of the form

	oid example_variables_oid[] = { 1,3,6,1,4,1,2021,254 }
where the contents of the array give the object identifier of the root of the module.

Module initialisation

Many modules require some form of initialisation before they can start providing the necessary information. This is done by providing a routine called init_{name} (where {name} is the name of the module).
This routine is theoretically optional, but in practise is required to register this module with the main agent at the very least. This specifies the list of variables being implemented (from the variableN structure) and declare where these fit into the overall MIB tree.

This is done by using the REGISTER_MIB macro, as follows:

	REGISTER_MIB( "example",  example_variables, variable2,
			example_variables_oid );
where "example" is used for identification purposed (and is usually the name being used for the module), example_variables is the structure defining the variables being implemented, variable2 is the type used for this structure, and example_variables_oid is the location of the root.

In fact, this macro is simply a wrapper round the routine register_mib(), but the details of this can safely be ignored, unless more control over the registration is required.

One common requirement, particularly on older operating systems or for the more obscure areas of the system, is to be able to read data directly from kernel memory. The preparation for this is typically done here by one or more statements of the form

	#ifdef {NAME}_SYMBOL
	auto_nlist( {NAME}_SYMBOL, 0, 0);
	#endif
where {NAME}_SYMBOL is defined as part of the system-specific configuration, to be the name of the appropriate kernel variable or data structure. (The two 0 values are because the kernel information is simply being primed at this point - this call will be reused later when the actual values are required). Note that this is probably the first thing described so far which isn't provided by mib2c!

Other possibilities for initialisation may include registering config file directive handlers (which are documented in the read_config(5) man page), and registering the MIB module (either in whole or in part) in the sysOR table. The first of these is covered in the example module, and the second in many of the other modules within the main UCD distribution.

Variable handling

The other obligatory routine is that which actually handles a request for a particular variable instance. This is the routine that appeared in the variableN structure, so while the name is not fixed, it should be the same as was used there.
This routine has six parameters, which will be described in turn.

Four of these parameters are used for passing in information about the request, these being:

	struct variable *vp;
		// The entry in the variableN array from the
		//   header file, for the object under consideration.
		// Note that the name field of this structure has been
		//   completed into a fully qualified OID, by prepending
		//   the prefix common to the whole array.
	oid *name;	// The OID from the request
	int *length;	// The length of this OID
	int exact;	// A flag to indicate whether this is an exact
			// request (GET/SET) or an 'inexact' one (GETNEXT)
Four of the parameters are used to return information about the answer. The function also returns a pointer to the actual data for the variable requested (or NULL if this data is not available for any reason). The other result parameters are:
	oid *name;	// The OID being returned
	int *length;	// The length of this OID
	int *var_len;	// The length of the answer being returned
	WriteMethod **write_method;
			// A pointer to the SET function for this variable
Note that two of the parameters (name and length) serve a dual purpose, being used for both input and output.

The first thing that this routine needs to do is to validate the request, to ensure that it does indeed lie in the range implemented by this particular module. This is done in slightly different ways, depending on the style of the module, so this will be discussed in more detail later.
At the same time, it is common to retrieve some of the information needed for answering the query.

Then the routine uses the Magic Number field from the vp parameter to determine which of the possible variables being implemented is being requested. This is done using a switch statement, which should have as many cases as there are entries in the variableN array (or more precisely, as many as specify this routine as their handler), plus an additional default case to handle an erroneous call.
Each branch of the switch statement needs to ensure that the return parameters are filled in correctly, set up a (static) return variable with the correct data, and then return a pointer to this value. These can be done separately for each branch, or once at the start, being overridden in particular branches if necessary.

In fact, the default validation routines make the assumption that the variable is both read-only, and of integer type (which includes the COUNTER and GAUGE types among others), and set the return paramaters write_method and var_len appropriately. These settings can then be corrected for those cases when either or both of these assumptions are wrong. Examples of this can be seen in the example module. EXAMPLEINTEGER is writeable, so this branch sets the write_method parameter, and EXAMPLEOBJECTID is not an integer, so this branch sets the var_len parameter. In the case of EXAMPLESTRING, both assumptions are wrong, so this branch needs to set both these parameters explicitly.

Note that because the routine returns a pointer to a static result, a suitable variable must be declared somewhere for this. Two global variables are provided for this purpose - long_return (for integer results) and return_buf (for other types). This latter is a generic array (of type u_char) that can contain up to 256 bytes of data. Alternatively, static variables can be declared, either within the code file, or local to this particular variable routine. This last is the approach adopted by mib2c, which defines four such local variables, (long_ret, string, objid and c64).

Mib2c requirements

Most of the code described here is generated by mib2c. The main exceptions (which therefore need to be provided by the programmer) are Everything else should be useable as generated.


This concludes the preliminary walk-through of the general structure of the C implementation. To fill in the details, we will need to consider the various styles of module separately. The next part will look at scalar (i.e. non-table based) modules.


Copyright 1999 - D.T.Shield. Not to be distributed without the explicit permission of the author.