Chapter 9 - Advanced Module Writing

This chapter discusses some advanced aspects of module writing. It covers some of the more esoteric functions of the IRIS Explorer product, which you may be interested in if you are writing complex and powerful modules that push the capabilities of IRIS Explorer beyond everyday levels. Some of the topics describe advanced techniques, such as writing Hook Functions, and some are more explanatory, such as the section on Understanding Reference Counting. An in-depth knowledge of how IRIS Explorer operates allows you to make the most of the benefits that IRIS Explorer offers.

You need not read this chapter unless you are interested in learning more about IRIS Explorer or in implementing these techniques.

Controlling Module Functions

IRIS Explorer provides the module writer with many opportunities to make a choice between letting IRIS Explorer control a particular process in the module, or taking charge and running that process directly from the user function. As a result, you can write modules that operate at varying levels of independence from IRIS Explorer. Where the module writer settles on the continuum from total control to very little depends on current needs and programming expertise.

The major divide is between modules with a Module Data Wrapper (MDW) and those without. If you build a module without an MDW, you must make sure that your user function performs all the actions that the MDW would otherwise have done.

Figure 9-1 shows the range of module complexity. The modules in division 1 contain code that works anywhere, and is reusable in other programs and systems. Division 2 modules contain code, possibly written for other purposes, that can be made to work exclusively in IRIS Explorer without a lot of hard work, by adding cx* API routines and the IRIS Explorer data type structures to the basic function or subroutine. Modules in division 3 are written specifically for IRIS Explorer, designed to take advantage of all the flexibility and potential in the system, and the code is not reusable at all in other systems.



Figure 9-1 Spectrum of Module Complexity

The Module Wrappers

IRIS Explorer uses three controlling interfaces or "wrappers" to mediate between the module's user function and the rest of the system. They are the Module Control Wrapper (MCW), the Module Data Wrapper (MDW), and the Generic Wrapper. The relationship between wrappers and user or computational function is shown in Figure 9-2.



Figure 9-2 The Module Structure

Module Control Wrapper

Every module has a Module Control Wrapper (MCW), which is the module data manager. The MCW contains the firing algorithm and provides interfaces to input and output functions, including port and widget settings. The MCW also manages all communication with other modules.

The firing algorithm is described in Appendix B - The Firing Algorithm. Information about the MCW's interface capacity is contained in the API routines for all the input and output functions.

The Module Builder constructs two kinds of MCWs, the default and a special form for X modules. The X Windows MCW uses the X mechanism for handling scheduling.

Module Data Wrapper

The Module Data Wrapper (MDW) performs conversions between IRIS Explorer and user-defined data types on the ports and the data format required by the user function. You can thus develop modules with customized interfaces without having to interact with the IRIS Explorer data types. Since you can create IRIS Explorer modules from existing code, it is easy to fold a function into a module without writing a line of source code yourself. Similarly, it is possible to convert a library of routines into a library of modules with minimal effort.

The connection between the data type and the subroutine is mediated by the Module Data Wrapper (MDW), which translates input data into a form the user function can process, and then translates the result into a suitable form for output. The Module Builder generates an MDW for a module by default. However, you can choose not to have an MDW, in which case you must explicitly incorporate the MDW functions in your user function. These include:

The Generic Wrapper

The Generic Wrapper is not accessible by the module writer, except indirectly through the Hook Functions menu. It contains:

Working with Module Wrappers

In general, when you build a module, the Module Builder writes code for the Module Data Wrapper and generates a linkage to a default Module Control Wrapper (MCW), or an X Window System MCW if so selected. You make these choices using the build options control panel. This is invoked by selecting Options... from the Build menu on the Module Builder window. It offers you options for expanding individual control over the interface between the user function and IRIS Explorer.

Modules Without MDW

If you stipulate no automatically generated MDW, you must construct the interface between the user function and the input and output ports yourself. To build a module that has no Module Data Wrapper, follow these steps.

  1. Create the basic module, including input and output ports, the control panel, and the documentation, in the Module Builder.

    You must define the user function name in the Function Arguments window, but you can skip the function definitions and the Connections window, since the information in these panes is used to generate the MDW.

  2. Open the Build menu from the main Module Builder window and select Options....
  3. Toggle the Write wrapper code button to No.

The user function must then create the access to input and output ports using API subroutines, such as:

For details on these subroutines, see the IRIS Explorer Reference Pages.

Using the X-MCW Option

When you build a module, you can choose whether to generate a default Module Control Wrapper or an X Windows MCW. You will require an X Windows MCW only if you have a DrawingArea widget, or otherwise need access to the X server.

The X Windows MCW provides IRIS Explorer with the ability to recognize and manipulate X Windows widgets in a module. You can use a drawing area in a module as an X input window, to hold an X Window or Motif widget, or as a general X graphics drawing area. An example of an IRIS Explorer module that contains X Windows drawing areas and uses an X Windows MCW is GenerateColormap.

The code examples in Examples Using X Widgets later in this Chapter illustrate how to incorporate some of these widgets into an IRIS Explorer module.

These are the advantages of using X Windows for customized graphics:

These are the disadvantages:

IRIS Explorer provides some API subroutines to help you handle X Window System widgets efficiently. They are described in the IRIS Explorer Reference Pages and summarized in Table 9-1.

Table 9-1 X Window API Subroutines

Subroutine Function
cxXtAreaInitialize Creates a drawing area that can later be bound to a module control panel
cxXtAreaAttach Attaches a widget hierarchy to a control panel drawing area
cxXtAreaResize Updates the size of a widget hierarchy in the drawing area of a module control panel
cxXtAreaCallbackAdd Registers a callback function for a cxXtArea
cxXtGLAreaInitialize Creates an OpenGL window inside a control panel drawing area
cxXtGLAreaRedraw

Example Using X Widgets

This example shows how an X drawing area can be used as an input mechanism. It places a Motif button in an X drawing area and reports whenever the button is pressed. The code is in $EXPLORERHOME/src/MWGcode/Advanced/C/XDrawInput.c. The module resource file may also be found in the same directory.

Note: It is not possible to write this module in Fortran.

/*
 * This example shows how an X drawing area can be used
 * as an input mechanism.
 */

#include <X11/Intrinsic.h>
#include <X11/Xm/PushB.h>

#include <cx/XtArea.h>

void xcallback(Widget w, void *client)
{
     printf("button pressed\n");
}

void xwidget(long window)
{
     static int first = 1;
     static Widget top;

    Widget button;

    /*
     * Do initialization stuff
     */
    if (first) {
	first = 0;

	/*
	 * Get a Xt form and put a button on it
	 */
	top = cxXtAreaInitialize();
	button = XtVaCreateManagedWidget("button",
	    xmPushButtonWidgetClass, top,
	    XmNleftAttachment, XmATTACH_FORM,
	    XmNrightAttachment, XmATTACH_FORM,
	    XmNtopAttachment, XmATTACH_FORM,
	    XmNbottomAttachment, XmATTACH_FORM,
	    NULL);

	/*
	 * Add a callback then attach the form to the window
	 */
	XtAddCallback(button, XmNactivateCallback,
	    (XtCallbackProc) xcallback, (XtPointer) NULL);

	cxXtAreaAttach(top, window);
    }
    cxXtAreaResize(top, window);
}

Graphics Library (OpenGL) Example

This example uses a GLXWidget to create a drawing area. The code is in $EXPLORERHOME/src/MWGcode/Advanced/C/GLXWidget.c. The module resource file may also be found in this directory.

Note that in order to create a module that calls OpenGL directly it is necessary to have a linkable version of the OpenGL libraries for your platform.

Note: There is no Fortran version of this module.

/*
 * Example module using a GLXWidget
 */

#include <stdio.h>

/* X includes */
#include <X11/Intrinsic.h>
#include <Xm/Xm.h>

/* OpenGL Drawing Area Include */
#include <X11/GLw/GLwDrawA.h>

/* GL includes */
#include <GL/gl.h>
#include <GL/glx.h>

/* Explorer includes */
#include <cx/PortAccess.h>
#include <cx/DataAccess.h>
#include <cx/UserFuncs.h>
#include <cx/XtArea.h>

Widget top; /* top level form widget */
Widget da;  /* drawing area widget */

/* 
 * Widget action routine 
 */
void xaction(void *client, XEvent *ev)
{
        float p[2];     /* temporary storage for coordinates */
        short xsize, ysize;    /* size of GL widget */

        /* get size of OpenGL window */
        XtVaGetValues(top, XmNwidth, &xsize, NULL);
        XtVaGetValues(top, XmNheight, &ysize, NULL);
        switch (ev->type)
        {
        case ButtonPress:
        /*
         * IMPORTANT NOTE: X origin is *upper* left;
         * GL origin is *lower* left.
         */
                printf("button pressed at %d, % d\n",
                 ev->xbutton.x, ysize - 1 - ev->xbutton.y);
                break;

        case Expose:
      /*
       * some sample OpenGL stuff
       */

                glViewport(0, 0, (GLsizei)xsize, (GLsizei)ysize);

                glClearColor(0.0, 0.317, 0.439, 0.627);
                glClear(GL_COLOR_BUFFER_BIT);

                glMatrixMode(GL_PROJECTION);
                glLoadIdentity();
                gluPerspective(40.0, 1.0, 0.5, 5.0);
                glTranslatef(0.0, 0.0, -4.0);
                
                glColor4b(255, 255, 255, 255);
                
                glBegin(GL_LINE_LOOP);
                p[0] = 0.0;
                p[1] = 0.0;
                glVertex2fv(p);
                p[0] = 1.0;
                p[1] = 1.0;
                glVertex2fv(p);
                p[0] = 1.0;
                p[1] = -1.0;
                glVertex2fv(p);
                p[0] = -1.0;
                p[1] = -1.0;
                glVertex2fv(p);
                p[0] = -1.0;
                p[1] = 1.0;
                glVertex2fv(p);
                glEnd();

                glFlush();
                GLwDrawingAreaSwapBuffers(da);
                
      break;
    }
}

/*
 * Module initialization routine
 *
 * This routine must be listed as the "initialization" hook function
 */
void xex2_init()
{
        long wid;                     /* window id from UI */
        XtTranslations translations;  /* X translations structure */
        int     n;
        Arg     args[12];
        XVisualInfo *vi;
        static GLXContext glx_context;

        /* X actions record */
        static XtActionsRec actions[] ={"xaction",(XtActionProc) xaction};

        /* Initialize an Xt area, get an empty form widget */
        top = cxXtAreaInitialize();
  
        /* Get the UI drawing area widget id  */
        wid = cxParamLongGet((cxParameter *) 
        cxInputDataGet(cxInputPortOpen("window")));
  
        /* Define some actions and translations for our widget */
        XtAddActions(actions, XtNumber(actions));
        translations = XtParseTranslationTable(
                "<BtnDown>: xaction()\n\
                 <Expose>: xaction()");
  
        /* Put a drawing area widget in the form */
        da = XtVaCreateManagedWidget("da",
                                glwDrawingAreaWidgetClass, top,
                                GLwNrgba, TRUE,
                                GLwNdoublebuffer, TRUE,
                                XmNtranslations, translations,
                                XmNleftAttachment, XmATTACH_FORM,
                                XmNtopAttachment, XmATTACH_FORM,
                                XmNrightAttachment, XmATTACH_FORM,
                                XmNbottomAttachment, XmATTACH_FORM,
                                NULL);

        /* Attach the form to the drawing area window  */
        cxXtAreaAttach(top, wid);

        /* initialise the OpenGL area */
        XtSetArg(args[0], GLwNvisualInfo, &vi);
        XtGetValues(da, args, 1);

        glx_context = glXCreateContext(XtDisplay(da), vi, 0, GL_FALSE);
        GLwDrawingAreaMakeCurrent (da, glx_context);    
        glDrawBuffer(GL_FRONT_AND_BACK);
}


/*
 * Module execution function
 *
 */

long xex2()
{
        int windowPort;         /* port number of window parameter */
        long wid;               /* window id from UI */

        /* Get window port number and the widget ID of the drawing area */
        windowPort = cxInputPortOpen("window");
        wid = cxParamLongGet((cxParameter *)                    
                        cxInputDataGet(cxInputPortOpen("window")));

        /* Resize the form widget to the UI drawing area size */
        cxXtAreaResize(top, wid);

        return 0;
}

Sharing Module Executables

You can save a significant amount of disk space by combining several related modules into a single executable image. This means you have only one executable file for all the related modules. If these modules were combined into one in the GUI, you could end up with a very complicated module control panel. To avoid this, IRIS Explorer allows each module sharing a single executable to have its own control panel. Each control panel is defined in a separate module resources file with the same name as the module itself.

This section describes how to create a single executable, or combination module, in the Module Builder and suggests what part of the library API to call from the combined module code.

Types of Shared Executable

A combination module determines its actions in two ways. It can:

Both AddImg and XorImg have module resource files that describe their control panels and ports. In addition, on the Build Options control panel, AddImg has Alternate Executable selected, with the name XorImg displayed in the executable name field. The XorImg module does not have Alternate Executable selected, so its name field is identical to the module name. It is clear that both AddImg and XorImg share the XorImg executable.

When the modules are built, the Module Builder causes the XorImg executable to be compiled, linked, and installed along with XorImg.mres. The rules for AddImg cause only its module resources file to be installed, thus referring to the XorImg binary. When either module is started, the same executable is run. Enough information is provided to the XorImg binary that it can determined whether it was invoked as AddImg or XorImg, and, consequently, which set of functionality to provide.

Module Builder generates a Makefile which may also be executed outside the Module Builder.

The XorImg user function includes a call to the API routine cxModuleNameGet, which returns the name of the module that was invoked: in this case, AddImg or XorImg. The user function then compares the name to a table of known operations to determine whether to add or xor two images. The module can also use this name to determine its expected port names and to access its port data.

The DataScribe modules provide another example of the use of alternate executables. Each customized DataScribe module is simply a module resource file that expects an input script and knows which set of ports to query, based on the contents of the script. Every DataScribe module refers to the generic DataScribe module for its executable. The DataScribe module uses the input script, not cxModuleNameGet, to direct its activities, which are to interpret the input script and carry out specific actions on data files or IRIS Explorer ports.

Using an Alternate Executable

An ordinary module, called MyModule for example, can use an executable name derived from its mres file, MyMod.mres. But some modules, notably the ImageVision Library(tm) modules, refer to a different executable. Since many modules can refer to a single executable, those modules need not build the executable; some other similarly named module does that. For example, XorImg is the module that builds the executable XorImg; all the other ImageVision Library modules refer to it.

The Alternate Executable item in the Build Options window of the Module Builder lets you:

In the case of a combined executable, each module has its own module resources file, but only one of the modules creates and installs an executable. The executable is then responsible for determining which operation to invoke upon firing.

Building Loop Controller Modules

There is no difference in programming between the user code of a loop controller and that of an ordinary module. The loop control work is handled in the MCW. There are certain API routines, discussed below, that simplify writing loop controller modules.

However, there are some capabilities a loop controller should have. The module must have at least one input and at least one output port. It needs to know that the loop continues by default if new data is sent into the loop, and the loop stops if no new data enters the loop. If the module is to continue or terminate a loop, it must be able to evaluate the condition for the loop. The module should also be able to break its loop, to prevent the loop from iterating indefinitely.

Note: A module that flushes multiple data sets, such as Streakline, should not be given loop controller status. Multiple data flushes from a controller module can lead to an exponential increase in loop data that will paralyze your system.

Using the Loop Controller API

There are four API subroutines you can use when writing a loop controller module: cxLoopBreak, cxLoopControlArc, cxLoopCtlr and cxLoopIteration. The three routines, cxLoopCtlr, cxLoopControlArc and cxLoopIteration, are Boolean queries that deal respectively with loop controller status of the module, control arc status of a connection, and looping, as opposed to non-looping, invocation of an iteration. You can use these calls to get information about the actions and state of a loop controller module.

The cxLoopBreak routine halts a current loop iteration, as well as marking all pending iterations for cancellation by the loop controller. If the controller puts out "end-of-loop" data, those datasets should be assigned to the output ports when cxLoopBreak is called, as the user function will not be called again for that loop iteration. In programming a loop controller module, you must use the API routine cxLoopBreak to terminate iteration of a loop. This causes the MCW to cancel all pending iterations of the loop and return the module to a quiescent state.

Data is put on the synchronisation port Loop Ended by the MCW when the iteration of a loop is terminated. You can prevent data being put on this port, as well as the Firing Done port, by calling cxOutputNoSync.

You may also terminate a loop by sending out a sync frame, which is a frame with no new data, on the looping outputs. You must take care here, as the Map Editor user might wire a loop on ports you did not expect to be used, causing the module not to terminate the loop. If the synchronisation port Firing Done is wired into the loop, as data is always put on this port when the module fires successfully, then the only way to terminate the loop is by calling cxLoopBreak. It is safest to use cxLoopBreak in all cases.

Using Generic Controller Modules

The While and Repeat loop controller modules simply examine the condition for the loop, which is a parameter value, and copy a pointer over from the input to the output port. These are "typeless" modules which have cxGeneric input and output ports, and do not interrogate their generic inputs.

You can copy the modulename.mres file of these modules from directory $EXPLORERHOME/modules and modify each one slightly to create loop controllers that pass additional data sets through, or accept particular data types. Just rename the module and augment the port list. In the Build Options control panel, set the alternate executable to be the name of the module you copied, e.g. While, (see Selecting Build Options in Chapter 2) and run the Module Builder on the new module.

The While module has the ports listed in Table 9-2:

Table 9-2 While Module Ports

Input Ports Output Ports
Condition Final Value 0
Initial Value 0 Final Value 1
... ...
LoopIn Value 0 LoopOut Value 0
LoopIn Value1 LoopOut Value1

Substitute your own name for the "Value N" part of the port name. The module must match initial/final and loop in/loop out values. It can do this by finding the keywords "Initial", "Final", "LoopIn" and "LoopOut" on the port names.

The For Module

The For loop is a specific case of the While loop, and the For module is designed to obviate the need for parameter functions. The For module has sliders for changing initial, final and current parameter values and parameter logic, and it also has a Refire button. The module evaluates a current value and waits for the return value. The loop index uses double precision floating point values. The current value is put out even when the loop terminates, so that other downstream modules can query it.

The cxGeneric Data Type

The cxGeneric data type is found on input and output ports in the generic loop controller modules, which are For, While, Repeat and Trigger. You can wire any port types into or out of a cxGeneric port.

The purpose of cxGeneric is to allow passage of all data types through a module. It is actually more of an encapsulation system than a true type, since it does not operate on the data at all.

Installing Interpreter Modules

When you install an interpreter module, you can list any extra files you want installed with the module in the Module Builder. The build options control panel has a text slot for listing them. Any module can cause extra files to be sent to $EXPLORERUSERHOME/modules when the Build and Install command is issued from the Module Builder (or a make install command is issued from outside it).

The Module Builder will check for information pertaining to interpreter modules, that is, those with an alternate executable name. Such a module may have a file $EXPLORERHOME/lib/modulename.interp which lists the interpreter module, optional file suffix to use, and widget normally containing the name of the interpreter script file. Table 9-3 lists three examples:

Table 9-3 Interpreter Module Files

Interpreter Module Module Executable Suffix Widget Name with embedded space
LatFunction.interp LatFunction .shp Program File
GenericDS.interp GenericDS .scribe Script File
MyInterp.interp MyInterp NULL My Widget

These are the rules that the Module Builder follows during installation.

Hook Functions

Hook functions allow you to gain control of a module when specific events occur in the Module Control Wrapper (MCW). The MCW passes control to the hook function whenever these events occur. A hook function can be linked to any one of the following events in an IRIS Explorer session. These events are:

Once you have control through the hook function, you can call a subroutine to carry out an activity related to the main user function. For example, the Render module has an initialization hook function that is triggered when the module is launched in the Map Editor. This function initializes the Open Inventor Library and the OpenGL drawing area, among other things. When the subroutine has completed its task, control is returned to the MCW.

A hook function table is contained in The Generic Wrapper of every module. The table is empty by default. To fill in the default table with your own hook functions, you must:

Hook functions written in C++ must have a C linkage. You can do this by declaring the function:

extern "C" my_hook_fcn( )
{ ...
}

Hook Function Calling Sequence

There are two forms of the calling sequence, one used in the initialization and removal hook functions, and one used in the connection and disconnection hook functions.

For the latter, the MCW passes the name of the affected port (portName) and an integer that identifies that particular connection (linkID). The linkID values are unique with respect to a given port and increase throughout a single IRIS Explorer session.

The calling sequence of each type of hook function is given below. You can give a hook function or subroutine any name you like; these are merely examples.

Creating a module:

C:
void CreateHook(void);
Fortran:
subroutine CreateHook

The MCW calls this function when the module is created, before connections are made and parameters are delivered. Parameter values are meaningless at this time.

Connecting input ports:

C:
void ConnectInput(char*portName, int linkID);
Fortran:
subroutine ConnectInput(portName, linkID)
character*(*) portName
integer linkID

The MCW calls this function after the user has connected the input port called portName to some output.

Writing Hook Functions

Hook functions are ordinary procedures, which you must write before the MCW can use them. The hook functions and user function must both have either C or Fortran linkage; mixing linkages is not permitted.

You can use the Module Builder to generate Hook Function Prototypes. This is most easily done as follows:

  1. Select Hook Funcs... from the Build menu in the Module Builder window to open the Hook Functions window (see Figure 9-3).
  2. Type the name you wish to give each hook function in the slot next to its description. For example, you may type myDestroyFunc as the function name in the slot next to "Remove Hook Function".
  3. When you have finished listing the hook functions for the module, click on OK. When you select Build or Build and Install from the Build menu, the Module Builder creates the hook function table in the Generic Wrapper.

    Once the hook function names have been specified, the Module Builder will automatically add prototypes for them (in the correct language) to the prototype user function file, if you ask for one to be created. After that, you need merely fill in the body of the functions.



Figure 9-3 The Hook Function Window

Here are some points to note when you are writing a hook function.

Note: If you have old modules that define cxHookTable, you must remove this definition from your source code. If you do not, you will see a multiply-defined symbol. This may make debugging more difficult and might even prevent the module from building.

Examples

The following example shows the use of hook functions to allocate storage space at module initialization, to get information about port connections, and to delete persistent lattice data in shared memory when the module is destroyed. Code and resources files may be found in the directories $EXPLORERHOME/src/MWGcode/Advanced/C and $EXPLORERHOME/src/MWGcode/Advanced/Fortran.

C Version:

#include <stdio.h>
#include <cx/DataAccess.h>

cxLattice *permanentLattice = NULL;

/***********************************************************************
 * This module shows how hook functions may be used
 *
 * Each hook function generates output, so the calling of each may be
 * followed as the module starts up, is connected to other modules,
 * is fired, and gets deleted.
 ***********************************************************************
 */
void HookFuncModule(cxLattice *lat)
{
  /* You may complete the user function by adding lines of code here
   * This output will appear every time the modules is fired
   */
  printf("User function called.\n");

  /* A private copy of the input lattice is stored by the user
   * This storage would be leaked on module destruction, unless
   * we increment the reference counter for this storage, and
   * ensure to delete it in the hook destroy function
   */
  if (permanentLattice == NULL)
    {
      permanentLattice = cxLatDup(lat,1,1);
      cxDataRefInc(permanentLattice);
      printf("increase the reference count for the private lattice\n");
    }
}
#include <stdio.h>
#include <cx/DataOps.h>

#define MAXSTR 256
typedef char strng[MAXSTR];

strng *myLinkArray = NULL;

/***********************************************************************
 * Initialisation hook function in C
 *
 * Some storage is allocated for use while the module exists
 ***********************************************************************
 */
void myInitFunc()
{
  /* You may complete the initialisation function by adding code here
   * for example to initialise the geometry library
   * This output will only appear when the module is started up
   */
  printf("Initialisation hook function called.\n");

  /* Initialise the link array */
  if (myLinkArray == NULL)
    myLinkArray = (strng *) cxDataCalloc(100, sizeof(strng));
}
#include <stdio.h>
#include <string.h>

#define MAXSTR 256
typedef char strng[MAXSTR];

/***********************************************************************
 * Connection hook function in C.
 *
 * This shows how one might save the port name associated with every
 * connection. Storage allocation of "myLinkArray" is done in the
 * initialisation hook function.
 */

void myConnectFunc(portName, linkID)
     char *portName;
     int linkID;
{
  extern strng *myLinkArray;

  /* Save the association of port and linkID. The link tag may be
   * negative, indicating a widget connection or a parameter function
   * (pfunc) connection. If so, ignore it.
   */
  printf("linkID=%d\n", linkID);
  if (linkID >= 0 && linkID < 100)
    {
      strcpy(myLinkArray[linkID], portName);
      printf("link %d is named %s\n",linkID, myLinkArray[linkID]);
    }
}
#include <stdio.h>
#include <cx/DataAccess.h>

/***********************************************************************
 * Destroy hook function in C.
 *
 * This procedure shows how one might write a "destruction" hook
 * function.  When the map editor user destroys a module, this
 *  procedure, if defined, will be called.
 *
 * Typically, this is useful if the module caches old data inputs.
 *  This provides a place to put the code that can decrement the
 * reference counts on the data.
 ***********************************************************************
 */
void myDestroyFunc(void)
{
  extern cxLattice *permanentLattice;
  int i;

  /* Here, one might adjust the reference counts on any cached data
   * In this case, there is only one lattice
   */
  cxDataRefDec(permanentLattice);
  printf("User destroy hook function called.\n");
}

Fortran Version:

      SUBROUTINE HKFUNC(LAT)
C
      INCLUDE '/usr/explorer/include/cx/DataAccess.inc'
C
C     This module shows how hook functions may be used
C
C     Each hook function generates output, so the calling of each may be
C     followed as the module starts up, is connected to other modules,
C     is fired, and gets deleted.
C
C     .. Scalar Arguments ..
      INTEGER           LAT
C     .. Scalars in Common ..
      INTEGER           PRVLAT
C     .. Local Scalars ..
      INTEGER           FIRST
C     .. External Subroutines ..
      EXTERNAL          CXDATAREFINC
C     .. Common blocks ..
      COMMON            /CACHE/PRVLAT
C     .. Data statements ..
      DATA              FIRST/1/
C     .. Executable Statements ..
C
C     You may complete the user function by adding lines of code here
C     This output will appear every time the modules is fired
C
      PRINT *, 'User function called.'
C
C     A private copy of the input lattice is stored by the user
C     This storage would be leaked on module destruction, unless
C     we increment the reference counter for this storage, and
C     ensure to delete it in the hook destroy function
C
      IF (FIRST.EQ.1) THEN
         PRVLAT = CXLATDUP(LAT,1,1)
         CALL CXDATAREFINC(PRVLAT)
         PRINT *, 'increase the reference count for the private lattice'
         FIRST = 0
      END IF
C
      RETURN
      END

      SUBROUTINE MYINIF
C
C     Initialization hook function in Fortran
C
C     .. Arrays in Common ..
      LOGICAL          MLSTAT(100)
C     .. Local Scalars ..
      INTEGER          I
C     .. Common blocks ..
      COMMON           /CONN2/MLSTAT
C     .. Executable Statements ..
C
C     You may complete the initialisation function by adding code here
C     for example to initialise the geometry library
C     This output will only appear when the module is started up
C
      PRINT *, 'Initialisation hook function called.'
C
C     Initialise the link tags to .FALSE.
C
      DO 20 I = 1, 100
         MLSTAT(I) = .FALSE.
   20 CONTINUE
C
      RETURN
      END

      SUBROUTINE MYCONF(PRTNAM,LNKTAG)
C
C     This connect hook function shows how one might save the
C     port name associated with every connection.
C     The name is saved in a common variable.
C     The initialisation hook function sets array MLSTAT to .FALSE.
C
C     .. Scalar Arguments ..
      INTEGER           LNKTAG
      CHARACTER*(*)     PRTNAM
C     .. Arrays in Common ..
      LOGICAL           MLSTAT(100)
      CHARACTER*32      MLARAY(100)
C     .. Local Scalars ..
      INTEGER           I
C     .. Common blocks ..
      COMMON            /CONN1/MLARAY
      COMMON            /CONN2/MLSTAT
C     .. Executable Statements ..
C
C     The LNKTAG may be negative, indicating that the connection is
C     either a widget or a parameter function (pfunc) in which case
C     this code ignores it.
C
      IF (LNKTAG.GE.0 .AND. LNKTAG.LT.100) THEN
         MLARAY(LNKTAG+1) = PRTNAM
         MLSTAT(LNKTAG+1) = .TRUE.
      END IF
C
      DO 20 I = 1, 100
         IF (MLSTAT(I)) THEN
            PRINT *, 'Link number', I - 1, ' is named ', MLARAY(I)
         END IF
   20 CONTINUE
C
      RETURN
      END

      SUBROUTINE MYDSTF
C
C     Destroy hook function in Fortran.
C
C     This procedure shows how one might write a "destruction" hook
C     function.  When the map editor user destroys a module, this
C     procedure, if defined, will be called.
C
C     Typically, this is useful if the module caches old data inputs.
C     This provides a place to put the code that can decrement the
C     reference counts on the data.
C
C     The name of this procedure must be entered into the hook function
C     table in the module builder.
C
C     .. Scalars in Common ..
      INTEGER          PRVLAT
C     .. External Subroutines ..
      EXTERNAL         CXDATAREFDEC
C     .. Common blocks ..
      COMMON           /CACHE/PRVLAT
C     .. Executable Statements ..
C
C     Here, one might adjust the reference counts on any cached data
C     In this case ther is only one lattice
C
      CALL CXDATAREFDEC(PRVLAT)
   20 CONTINUE
      PRINT *, 'User destroy hook function called.'
C
      RETURN
      END

Understanding Reference Counting

IRIS Explorer transfers data between modules using shared memory if the modules are on the same machine and the machine supports shared memory. The advantage of using shared memory is that large quantities of data can be transferred from one module to another with very little communication overhead. All modules access the shared memory arena simultaneously, so only the address of the data has to be transferred, not the data itself. However, the data must be managed somehow, and this is done by means of reference counting.

The reference count can be thought of as the number of places that wish to keep a persistent copy of specific data. These places are also responsible for decrementing the count when they have finished with the data.

Managing Data in Shared Memory

All modules are responsible for collectively managing the data in shared memory. One module creates data and sends it to other modules. When all the modules are finished with that data, the space it occupies must be reclaimed. IRIS Explorer uses reference counting to manage this process. With reference counting, a module does not need to know anything about which other modules use the data. It must simply perform certain bookkeeping actions on the data it references, so that the system knows when the data should be reclaimed.

Reference counting is essential when using shared memory, but the mechanism is also general enough to be used by a single process on private data. Thus, modules executing on a machine that does not support shared memory can still use the same code for managing data memory.

As long as every module does its bookkeeping correctly and consistently, there will be no memory leaks, and data will not be reclaimed while a module still refers to it.

Caution: The automatically generated Module Data Wrapper cannot completely recover if the shared memory arena becomes full. It is possible that, if there is not enough memory for the computational function or for the MDW itself, the MDW may leak memory.

How Reference Counting Works

An integer is kept with each data object, to record the number of places where the data is used. When a module receives data, it increments the integer count to show that the data is being used in an additional place. When the module is finished with the data, it decrements the reference count. When the count becomes zero, the data is no longer needed and that module deletes the data.

For example, the module's input and output ports store data for the module, so they must manipulate the reference count. When the module puts data on the output port, the output port decrements the count of the old data on the port and increments the count of the new data. The output port retains use of the data so it can be sent to modules as they are connected to the port.

When a module receives new data, the input port decrements the count on the old data on that port and increments the count on the new data. The input port retains use of the data until it is replaced, which means the data is available every time the module fires, even if the data has not changed.

The API subroutines cxDataRefInc and cxDataRefDec are used to increment and decrement the reference count.

How IRIS Explorer Implements Reference Counting

IRIS Explorer implements a less rigid version of reference counting than that defined above. Data objects are created with a reference count of 0 because nothing has claimed responsibility for decrementing the count. If the count on such an object is decremented immediately, it becomes -1. The object is deleted and no error is reported.

The default count of 0 simplifies coding of the user function cases. A data object is created and (usually) placed on the output port. The output port increments the count and assumes responsibility for maintaining the count, thus freeing the user function from that responsibility and allowing it to ignore the data. If data objects were always created with a reference count of 1, then you would have to decrement the count of each object after placing it in the output port.

A user function may want to allocate a data object for temporary storage so that the same data can be output more than once. In this case, the user function does need to increment the count, ensuring that the data will persist as long as the user function needs it. When it is no longer required, the user function should decrement the count.

A module may have more than one reference to any data, so the value of the reference count may be greater than the number of modules using it. It is easier to code a module when you can allow it to have multiple references to the data, because the user function does not have to maintain exactly one reference count.

Reference-Counting Data Types

All IRIS Explorer system and user-defined data types are reference-counted. Data objects may contain references to other reference-counted objects. For example, the cxLattice and cxPyramid data types contain references to cxData, cxCoord, cxConnection, and cxLattice objects. The reference-counting scheme handles arbitrary sharing of IRIS Explorer data objects within other objects, just as data objects are shared between modules. For example, a cxData object can be contained in more than one cxLattice. Likewise, a cxLattice object can be contained in more than one cxPyramid and referenced by more than one module.

Reference Counts for Contained Data Types

The IRIS Explorer routines that set the contents of cxLattice and cxPyramid, for example, cxLatPtrSet and cxPyrSet, correctly manage the reference counts of the contained data types. When the count of a cxLattice or cxPyramid reaches 0 or -1, the generic data deletion routine uses the data type information to decrement the reference count of each contained data type.

The automatically generated interface routines cxLatPtrSet, cxPyrSet, and cxPyrLayerSet correctly manage the reference counts for the data objects they contain. You are strongly encouraged to use them.

The cxGeometry data type contains references to the field data of the IRIS Explorer nodes within an Open Inventor scene graph. This reference counting is managed within the geometry API and the IRIS Explorer node classes and you will never need to manipulate these directly. Note that this is in addition to the Open Inventor reference counting for nodes and referencing and deleting nodes, which should be programmed as usual for an Open Inventor scene graph, when using Open Inventor directly instead of the geometry API. It is important to ensure that a scene graph is correctly deleted when a module exits, lest it leak the shared memory in its nodes.

Manipulating Reference Counts

Module writers rarely have to manipulate reference counts, but there are times when it is necessary. This happens most commonly when the module needs to retain data that the system would normally have released. For example, a module that outputs a moving average of the last three lattices delivered to it might need to keep the lattices from two previous firings. When a new lattice arrives on a wire, it decrements the reference count on the lattice that it replaces. This can result in the memory for the old lattice being deallocated. To stop this happening, the module should increment the reference count of the lattices it wants to save. This process applies to all data types.

If a module increments the reference count to save data, it is very important that it decrement the reference count to free the memory when the data is no longer needed. Not properly balancing reference count increments and decrements can result in memory leaks which degrade performance.

Shared Memory Arena

The shared memory arena is a fixed-size resource, whose upper limit is system dependent. Because it is stored in a memory-mapped file, its size is limited by the available disk space and the command line and configuration options that are specified when the user starts IRIS Explorer; therefore, it is possible for data memory allocation routines to fail, no matter how large the arena might be, because the arena size may exceed the amount of available memory.

Allocating Memory Efficiently

It is very important to keep the limitations of memory allocation in mind when you write modules. The memory requirements of the system fluctuate, so many out-of-memory situations are transient. IRIS Explorer tries to allocate memory several times over a short period before returning an error flag, at which time an error message is sent to the Map Editor, which in turn generates a pop-up dialog box.

Modules should check the data allocation flag after every IRIS Explorer routine that allocates data memory. The cxDataAllocErrorGet routine returns TRUE (non-zero) if data could not be allocated. See the entry for cxDataAllocErrorGet in the IRIS Explorer Reference Pages for a list of the routines that allocate data memory. (For reasons of simplicity such error checking has been omitted from many of the example programs.)

If a module ignores allocation errors, it may crash due to a bad pointer. Even worse, it may cause another module to crash. This kind of module failure can be very hard to diagnose. If a module crashes, it cannot decrement reference counts to data to which it has references, so the data will be leaked. This makes a bad situation even worse.

Recovering from Memory Errors

The IRIS Explorer modules provided with the system do not attempt memory allocations after an error has been detected. They simply reclaim any memory they have already allocated and return from the user function. Unfortunately, the module may have successfully allocated several data objects, which it must reclaim before returning. The recovery code can get quite complicated, especially for a module that creates many data objects or creates cxPyramid or cxGeometry objects. It helps a great deal if all data is allocated in one place, where it is easier to keep track of what should and should not be recovered. This is not always possible, but grouping code in modules to streamline data allocation helps to eliminate programming errors.

If the memory requirements are smaller when no modules are firing, the Fire Now option on the module pop-up menu will cause it to execute again. If the module still does not have enough memory, you can free some data memory by deleting modules that are consuming large portions of memory.

If a module runs out of memory, it can:

However, each failed attempt still generates the pop-up dialog box in the Map Editor.

Examples of Memory Recovery

The following examples show how to recover from allocation errors for some of the IRIS Explorer data types. They also illustrate some coding styles for recovery.

Note: These examples are in C only because they are meant to illustrate specific techniques, rather than provide reusable code. Templates for these routines may be found in directory $EXPLORERHOME/src/MWGcode/Advanced/C.

Example 1

The first example allocates a cxParameter and two cxLattices. It uses a single memory recovery routine, memCleanup, which takes advantage of the fact that cxDataRefDec accepts a NULL pointer; therefore, memCleanup can be called at any point in the program.

#include <cx/DataAccess.h>
#include <cx/DataOps.h>

cxParameter     *parm = NULL;
cxLattice       *lat1 = NULL;
cxLattice       *lat2 = NULL;

void memCleanup()
{
  cxDataRefDec( parm );
  cxDataRefDec( lat1 );
  cxDataRefDec( lat2 );
}

void userFunction()
{
  parm = cxParamNew();
  if (cxDataAllocErrorGet())
    return;                  /* no cleanup required */
  lat1 = cxLatNew(...);
  if (cxDataAllocErrorGet())
    {
      memCleanup();
      return;
    }
  lat2 = cxLatNew(...);
  if (cxDataAllocErrorGet())
    {
      memCleanup();
      return;
    }
  /*
   * Et cetera...
   */
}

Example 2

The second example shows the use of recovery labels in the reverse order of the data allocation. Execution falls through the labels, unwinding the successful allocations.

#include <cx/DataAccess.h>
#include <cx/DataOps.h>

void        userFunction()
{
  cxParameter *parm = NULL;
  cxLattice *lat1 = NULL, *lat2 = NULL;

  parm = cxParamNew();
  if ( cxDataAllocErrorGet() )
    goto parm_Failed;
  lat1 = cxLatNew(...);
  if ( cxDataAllocErrorGet() )
    goto lat1_Failed;
  lat2 = cxLatNew(...);
  if ( cxDataAllocErrorGet() )
    goto lat2_Failed;

  /* ... */

  /* normal return */
  return;

 lat2_Failed:
  cxDataRefDec( lat1 );
 lat1_Failed:
  cxDataRefDec( parm );
 parm_Failed:
  return;
}

Example 3

The third example shows the memory for lattice data and coordinates allocated separately. Recovery becomes more complicated in this case. Once cxLatPtrSet stores data and coords into lat1, lat1 has reference counts on data and coords and is responsible for decrementing their counts. Once succesfully stored in the lattice, they should not be decremented by the user function.

#include <cx/DataAccess.h>
#include <cx/DataOps.h>

void userFunction()
{
  cxLattice *lat1, *lat2;
  cxData *data;
  cxCoord *coords;

  lat1 = cxLatRootNew(...);
  if( cxDataAllocErrorGet() ) return;
  data = cxDataNew(...);
  if( cxDataAllocErrorGet() )
    {
      cxDataRefDec( lat1 );
      return;
    }
  coords = cxCoordNew(...);
  if( cxDataAllocErrorGet() )
    {
      cxDataRefDec( lat1 );
      cxDataRefDec( data );
      return;
    }
  /* cxLatPtrSet doesn't allocate memory */
  cxLatPtrSet( lat1, data, NULL, coords, NULL );

  lat2 = cxLatNew(...);
  if( cxDataAllocErrorGet() )
    {
      cxDataRefDec( lat1 ); /* NOT data or coords */
      return;
    }
  /* Etcetera .. */

  /* normal return */
  return;
}

Example 4

The fourth example shows a recovery strategy for the cxPyramid data type. It is similar to that required by cxLattice.

The base lattice and the lattice/connection pair are managed similarly to the data and coordinates for cxLattice. Once successfully stored in the pyramid, they should not be decremented by the user function.

Also, cxPyrLayerSet allocates memory if the number of pyramid layers is to be increased, so cxDataAllocErrorGet should be checked after a call to cxPyrLayerSet if there is any chance that the number of layers will increase. If cxPyrLayerSet fails, you must decrement the lattice and connection manually because they were not successfully stored in the cxPyramid.

#include <cx/DataAccess.h>
#include <cx/DataOps.h>

void userFunction()
{
  cxPyramid     *pyr;
  cxLattice    *base, *layer, *other;
  cxConnection    *conn;

  base = cxLatNew(...);
  if( cxDataAllocErrorGet() ) return;
  pyr = cxPyrNew(...);
  if( cxDataAllocErrorGet() )
    {
      cxDataRefDec( base );
      return;
    }
  cxPyrSet( pyr, base );        /* no allocation */

  layer = cxLatNew(...);
  if( cxDataAllocErrorGet() )
    {
      cxDataRefDec( pyr );       /* not base */
      return;
    }

  conn = cxConnNew(...);
  if( cxDataAllocErrorGet() )
    {
      cxDataRefDec( pyr );
      cxDataRefDec( layer );
      return;
    }

  cxPyrLayerSet( pyr, i, conn, layer );
  if( cxDataAllocErrorGet() )
    {
      cxDataRefDec( pyr );
      cxDataRefDec( layer );       /* layer and conn not in pyr */
      cxDataRefDec( conn );
      return;
    }

  other = cxLatNew(...);
  if( cxDataAllocErrorGet() )
    {
      cxDataRefDec( pyr );
      return;
    }
  /* Etcetera .. */

  /* normal return */
  return;
}

Example 5

The cxGeometry data type complicates error recovery even more, since it contains a buffer of transcribed geometry specification commands. Each geometry specification routine adds commands to this buffer, which may cause the buffer to be enlarged, so cxDataAllocErrorGet must be checked after each command. In addition, the actual buffer location is not associated with the geometry object while primitives are added, so the cxGeoBufferClose routine must be called before the geometry object can be reclaimed.

In this example, the module performs operations if the error flag is not set. It can then check the flag at key points and perform cleanup if required.

The buffer may be expanded more than once for each geometry specification command, so it may contain a partial command when the data allocation fails. The partial command may confuse downstream modules so seriously that the contents of the geometry must be thrown away.

Because the geometry interface is a delta protocol (only changes are sent), any cxGeoDelete command will be lost after the local data structures have been modified. The downstream modules will misinterpret future geometry inputs and will appear corrupted. The best way to recover is to disconnect and reconnect the geometry output that ran out of memory.

Note: cxGeoBufferClose should not be called twice on a cxGeometry object.

#include <cx/DataAccess.h>
#include <cx/DataOps.h>
#include <cx/Geometry.h>

void        userFunction()
{
  cxGeometry *geo;

  geo = cxGeoNew();
  if ( cxDataAllocErrorGet() )
    return;
  cxGeoBufferSelect( geo );

  if ( !cxDataAllocErrorGet() )
    cxGeoRoot();
  if ( !cxDataAllocErrorGet() )
    cxGeoDelete();
  if ( cxDataAllocErrorGet() )
    goto cleanup_selected;

  cxGeoXformPush();
  if ( !cxDataAllocErrorGet() )
    cxGeoLinesDefine(...);
  if ( !cxDataAllocErrorGet() )
    cxGeoColorAdd(...);
  if ( !cxDataAllocErrorGet() )
    cxGeoXformPop();
  if ( cxDataAllocErrorGet() )
    goto cleanup_selected;

  cxGeoBufferClose( geo );
  if ( cxDataAllocErrorGet() )
    goto cleanup;

  /* ... */

  /* normal return */
  return;

 cleanup_selected:
  cxGeoBufferClose( geo );
 cleanup:
  cxDataRefDec( geo );
  return;

}

Extending the Power of Modules

Several subroutines in the API allow you to increase the flexibility of your module. You can:

The use of these routines is described briefly in the next sections.

Communicating with UNIX Processes

The subroutine cxInputAdd registers an open UNIX file descriptor that the module will monitor. The file descriptor can reference any UNIX device on the system that has a file descriptor. The module can request that it be informed when the device is ready for reading or writing, or when an exception condition exists. When one of these conditions is met, the Module Control Wrapper calls the user callback routine, which passes the data collected from the UNIX device to the IRIS Explorer module. cxInputAdd returns a unique handle, which allows the module to distinguish more than one such input device.

When you use cxInputAdd, you should note that the callback routine is not called when the module is firing, only when it is quiescent.

The related subroutine, cxInputRemove, cancels the UNIX file descriptor monitoring process. You can call it from the callback routine specified in cxInputAdd.

cxInputAdd is a handy tool for monitoring, and collecting data from, processes outside the IRIS Explorer environment. For example, you can start up and communicate with a data base server from your IRIS Explorer module.

An Example Module

This module demonstrates the use of cxInputAdd. The module has a single text type-in slot on its control panel, into which the user types a UNIX command. The module runs that command and routes the output to the file descriptor fd. When the output is available on the file descriptor, the feedme routine is called. This routine simply copies this output to the module's standard output. The code is in $EXPLORERHOME/src/MWGcode/Advanced/C/InputAdd.c. The resources file may be found in the same directory. To test the module type a command, for example ls, in the text slot and observe the results.

Note: There is no equivalent Fortran version of this module.

/*
 * Sample module to demonstrate the use of cxInputAdd()
 *
 * This module takes a single text type-in, into which should be typed a UNIX
 * command.  It runs that command, and routes the command's output to its own
 * standard output.
 */

#include <stdio.h>
#include <cx/DataTypes.h>
#include <cx/ModuleCommand.h>

#ifdef __cplusplus
extern "C" {
#endif

void        selectTestFunc  (
unsigned char *command )
{
    FILE           *fd;
    void           *handle;
    void            feedme();

    if (strlen(command) == 0) {
	return;
    }

    /*
     * First, send the user's command off for invocation, and hang on to a
     * descriptor for the pipe.
     */
    if ((fd = popen(command, "r")) == NULL) {
	cxModAlert("Command failed.");
	return;
    }

    /*
     * Now, add this file descripter to the scheduler's select set. When
     * data arrives from the command, "feedme" will be called by Explorer.
     */
    handle = cxInputAdd(fileno(fd), cx_InputReadMask,
			feedme, (void *) fd);
    return;
}

/*
 * Input available callback.
 *
 * This is called when the file descriptor from the popen() call above is
 * available.  The cient data (data) argument is just the FILE * form of the
 * descriptor.
 */

void        feedme( data, fd, handle )
void       *data;
int         fd;
void       *handle;
{
    char            buf[BUFSIZ];
    int             len;

    /*
     * Read from the pipe.  If no data remains on the file descriptor
     * (EOF) then remove the file descriptor from the select set.
     * Otherwise, just write the data to the window.
     */
    len = read(fd, buf, sizeof buf);

    if (len <= 0) {
	printf("closing pipe\n");
	pclose((FILE *) data);
	cxInputRemove(handle);
    } else {
	write(1, buf, len);
    }
}

#ifdef __cplusplus
};
#endif

Timer Events

You can use cxTimerAdd to time the occurrence of certain events that relate to the module function. The module can request callback routines to be called after a delay of a specified number of milliseconds, or at specified intervals. For example, you can set the timer to operate once and then switch off, or to reset itself and continue to run.

The callback is made when the timer expires, but the user function must be quiescent at the time. If the timer expires while the user function is executing, the callback will not be made until it has completed execution, and the timing schedule will be thrown off.

You can use cxTimerRemove to cancel this routine.

The code for this example is in $EXPLORERHOME/src/MWGcode/Advanced/C/Timer.c and $EXPLORERHOME/src/MWGcode/Advanced/Fortran/Timer.f. Resources files may be found in the same directories.

C Version:

/*
 * Trigger module -- demonstrates cxTimerAdd()
 *
 * This module fires every X seconds, where X is the value on
 * the dial widget on its control panel
 */

#include <cx/DataTypes.h>
#include <cx/Timer.h>


/*
 * Callback routine for the timer.
 * All this does is causes the module to fire.
 */

void        myTrigger(void *handle)
{
    printf("Timer trigger called\n");
    cxFireASAP();
}

/*
 * Main user function.
 *
 * secs -- number of seconds to delay (from a dial)
 * newtime -- non-zero if the firing resulted from the dial changing
 */

void        trigger  ( long secs, long newtime )
{
    static void    *lastTimer = NULL;

    /*
     * If the dial changed, remove any old interval timers and register a
     * new one.
     */

    if (newtime) {
        if (lastTimer != NULL) {
            cxTimerRemove(lastTimer);
        }
        lastTimer = cxTimerAdd(secs * 1000, 1, myTrigger, &lastTimer);
    }
    printf("triggered\n");
}

Fortran Version:

      SUBROUTINE TRIGGR(SECS,NEWTIM)
C
C     User function.
C     Trigger module -- demonstrates cxTimerAdd()
C
C     This module fires every X seconds, where X is the value on
C     the dial widget on its control panel
C
C     SECS   - number of seconds to delay (from a dial)
C     NEWTIM - non-zero if the firing resulted from the dial changing
C
C      include "cx/Timer.inc"
      INCLUDE '/usr/explorer/include/cx/Timer.inc'
C
C     .. Scalar Arguments ..
      INTEGER           NEWTIM, SECS
C     .. Local Scalars ..
      INTEGER           LSTTIM
C     .. External Subroutines ..
      EXTERNAL          CXTIMERREMOVE, MYTRG
C     .. External Functions ..
      EXTERNAL          CXTIMERADD
C     .. Save statement ..
      SAVE              LSTTIM
C     .. Data statements ..
      DATA              LSTTIM/0/
C     .. Executable Statements ..
C
C     If the dial changed, remove any old interval timers
C     and register a new one.
C
      IF (NEWTIM.NE.0) THEN
         IF (LSTTIM.NE.0) THEN
            CALL CXTIMERREMOVE(LSTTIM)
         END IF
         LSTTIM = CXTIMERADD(SECS*1000,1,MYTRG,0)
      END IF
C
      PRINT *, 'TRIGGERED'
      RETURN
      END
      SUBROUTINE MYTRG
C
C     Callback routine for the timer.
C     All this does is causes the module to fire.
C
C     .. External Subroutines ..
      EXTERNAL         CXFIREASAP
C     .. Executable Statements ..
C
      PRINT *, 'TIMER TRIGGER CALLED.'
      CALL CXFIREASAP
C
      RETURN
      END

Expanding Filenames

You can use cxFilenameExpand to expand the name of a file fully to include the directory path. You can give it a string including, for example, $EXPLORERHOME or ${EXPLORERUSERHOME}, and it will expand the variable into the full name. This is particularly useful because there is no standard C library routine for doing this.

The code for the following example is in $EXPLORERHOME/src/MWGcode/Advanced/C/FilenameExpand.c and the resources file FilenameExpand.mres may be found in the same directory.

Note: There is no equivalent Fortran version for this module.

#include <stdio.h>
#include <string.h>

#include <cx/DataAccess.h>

void expand(char *filename)
{
        /* terminate early if filename is empty */
        if ( strlen(filename) == 0 )
                return;

        /*
                The string returned by cxFilenameExpand must be copied
                if it is to be saved for future use.  Subsequent calls
                to cxFilenameExpand will write over this buffer.
        */
        printf("delivered filename: %s\n",filename);
        printf("expanded filename:  %s\n",cxFilenameExpand(filename));

        /* increment counter for %n */
        cxFilenameIndexIncrement();
}

Making Subdirectories for Module Development

This section explains how to set up a user directory in which modules can be made. It assumes some knowledge of UNIX program development, specifically, the make(1) program. IRIS Explorer uses an extension to make called Imake which allows us to distribute a portable development mechanism. With Imake, you do not directly create the Makefile files used by make. Instead, Makefile files are generated automatically from a file named Imakefile (see Appendix A - Using Makefiles). IRIS Explorer takes this one step further.

Within the subdirectories where you develop modules, the Module Builder creates the Imakefile for you from the information in the module resource file, and then it creates a Makefile from that.

There are two paths to follow at this point:

  1. Try building the sample IRIS Explorer modules provided in source format within your own module tree.
  2. Set up your own build system, skipping the attempt to build the IRIS Explorer modules.

Path 1 provides a good test of the installation of IRIS Explorer on your machine and shows a working example of module compilation. Path 2 is more direct, allowing you to begin module development immediately. In either case, you must first set up a new, empty module build tree. Each method is described in detail in the following sections.

Creating the Directory

The plan is to create a directory where you have write permission (for example, beneath your home directory) and then develop modules in subdirectories of that directory. One or more modules may be developed in each subdirectory and you can build one or all of them with a single command.

First, reset the EXPLORERUSERHOME environment variable to the directory you have chosen as the IRIS Explorer installation directory; and place $EXPLORERHOME/bin in your execution path:

setenv EXPLORERUSERHOME /usr/local/explorer
set path = $EXPLORERHOME/bin $path

Note: Individual users do not always have write access to /usr/local/explorer on a multi-user system. In this case, set EXPLORERUSERHOME to a directory in which you do have write permission.

Decide where you want to do module development and create that directory. ~/explorer/modules is a good choice.

mkdir ~/explorer/modules

If ~/explorer does not already exist on your system, create it by typing:

mkdir ~/explorer

From here, you can proceed through one or the other, or both, of the following sections, depending on your purpose.

Building Sample IRIS Explorer Modules

If you decide to build the sample IRIS Explorer modules provided in source format, you can compile them in your own source directory. To do this, first copy the sources into your directory:

cp -r $EXPLORERHOME/src/MWGcode/* ~/explorer/modules

This assumes that explorer/modules is a directory in your home directory where you will build these modules. Next, make a Makefile in that directory:

cd ~/explorer/modules
cxmkmf

Next, make sure that your EXPLORERUSERHOME environment variable is set to a place where you want these modules installed, for example:setenv EXPLORERUSERHOME /usr/tmp/modules mkdir $EXPLORERUSERHOME

Now, create Makefiles for those module subdirectories:

make Makefiles

Finally, you compile, link and install these modules:

make install

At the end, all of the sample modules should be compiled, linked, and installed.

Setting Up a Personal Module Tree

If you decide to set up your own build system, you create subdirectories in which you can develop one or more modules. First copy the template Imakefile into your development directory and make it writable:

cp $EXPLORERHOME/lib/Imakefile.modules Imakefile
chmod +w Imakefile

Then you can make a subdirectory (whose name may or may not match the module name you wish to use), and add that subdirectory to the list in Imakefile which starts with the line

MODULESUBDIRS = \

and is followed by a list of subdirectories containing modules. Any number of directory names can be listed, as long as they exist and are IRIS Explorer module development subdirectories.

You then build a new top-level Makefile, remake subdirectory Makefiles, and then proceed with development of the new module.

For example,

cd DIR
mkdir mycontour
vi Imakefile

 [add mycontour to the MODULESUBDIRS list]

cxmkmf
make Makefiles
cd mycontour
make all

There are several environment variables that affect the way modules are built and installed. See Configuring the Build Environment for more information on these variables.

Each time you add a directory to the MODULESUBDIRS list, you must recreate the top-level Makefile with cxmkmf. Do not be tempted to edit the Makefile directly.

Once you have done this, the following commands will work in each module subdirectory:

make all
compiles and links all modules
make clean
deletes object files
make depend
adds dependencies to your Makefile

The typical development cycle is to write your module user function, use the Module Builder to describe it, then use the Build option in the Module Builder to build it. Alternatively, you can describe the module first, then use the Module Builder to generate the function prototype. You can then fill in the code for the user function, and after that, build the module.

From this point on, you can rebuild the module at any time using the make all command. For more information, refer to Appendix A - Using Makefiles.


Last modified: Feb 23 17:00 1999
[ Documentation Home ]
© NAG Ltd. Oxford, UK, 1999