This chapter discusses some advanced aspects of module writing. It covers some of the more esoteric functions of the Explorer product, which you may be interested in if you are writing complex and powerful modules that push the capabilities of 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 the firing algorithm. An in-depth knowledge of how Explorer operates allows you to make the most of the benefits that Explorer offers.
You need not read this chapter unless you are interested in learning more about Explorer or in implementing these techniques.
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 Explorer without a lot of hard work, by adding cx* API routines and the Explorer data type structures to the basic function or subroutine. Modules in division 3 are written specifically for 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.
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.
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-MCW uses the X mechanism for handling scheduling.
The Module Data Wrapper (MDW) performs conversions between 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 Explorer data types. Since you can create 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:
Extracting data from input ports and placing data on output ports.
Controlling the interface with the Module Control Wrapper for special callback events.
Coercing input data into a form that the module can handle.
The Generic Wrapper is not accessible by the module writer, except indirectly through the Hook Functions menu. It contains:
Hook function interfaces
User function table (where the name of the user function or MDW routine is declared to the MCW)
Declaration of the metatypes (Explorer's internal form of a data type) for the necessary Explorer and user-defined types, for example, cxLatticeMetaType.
Port checking code for run-time data constraint checking on lattices.
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 Windows MCW if so selected. You make these choices using the Build Options window.
To display the Build Options window, select “Options” from the Build menu on the Module Builder window. The Build Options window offers you options for expanding individual control over the interface between the user function and Explorer.
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.
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; the information in these panes is used to generate the MDW.
Open the Build Options menu from the main Module Builder window and select “Options”.
Toggle the “Write wrapper code” button to No.
The user function must create the access to input and output ports using API subroutines, such as:
For details on these subroutines, see the IRIS Explorer Reference Pages.
When you build a module, you can choose whether to generate a default Module Control Wrapper or an X Window System MCW. You will require an X-MCW only if you have a DrawingArea widget, or otherwise need access to the X server.
The X-MCW provides Explorer with the ability to recognize and manipulate X Window 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. Explorer modules that contain X Window drawing areas and use an X-MCW include GenerateColormap and Histogram.
The code examples in “Examples Using X Widgets”illustrate how to incorporate some of these X widgets into an Explorer module.
These are the advantages of using the X Window System for customized graphics:
It is the only way to incorporate such graphics into an Explorer module.
You may get better performance from the graphics because there is no traffic from the Explorer user interface to the module.
You have complete freedom in creating the user interface. You may even be able to use X Window System code that has already been written for another application.
These are the disadvantages:
You have to manage the entire user interface for the X window yourself, which takes a lot of work.
You cannot use parameter functions with widgets managed directly by the module.
The module binary file will be larger than that for an Explorer module, since it must be linked with X Windows, and that consumes memory.
Your module may look different from other Explorer modules, and a different interface may confuse your users.
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 |
cxXtAreaCallback Add | Registers a callback function for a cxXtArea (primarily for Fortran users) |
cxXtGLAreaInitialize | Creates a GL window inside a control panel drawing area |
cxXtGLAreaRedraw | Registers a redraw procedure with a cxXtGLArea |
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 /usr/explorer/src/MWGcode/Advanced/C/XDrawInput.c.
![]() | Note: It is not possible to write this module in Fortran. |
#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);
}
|
This example uses a GLXWidget to create a drawing area. This program builds in C or C++. The code is in /usr/explorer/src/MWGcode/Advanced/C/GLXwidget.c and in /usr/explorer/src/MWGcode/Advanced/Fortran/GLXwidget.f.
#include <stdio.h>
/* X includes */
#include <X11/Intrinsic.h>
#include <X11/Xm/Xm.h>
#include <X11/Xirisw/GlxMDraw.h>
/* GL includes */
#include <gl/gl.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 action routine
*/
void xaction(void *client, XEvent *ev)
{
float p[2]; /* temporary storage for coordinates */
short ysize; /* height of GLX widget */
switch (ev->type) {
case ButtonPress:
/*
*IMPORTANT NOTE: X origin is *upper* left;
*GL origin is *lower* left.
*/
XtVaGetValues(top, XmNheight, &ysize, NULL);
printf(“button pressed at %d, % d\n”,
ev->xbutton.x, ysize - 1 - ev->xbutton.y);
break;
case Expose:
/* some sample GL stuff
*/
reshapeviewport();
cpack(0x00b070a0);
clear();
perspective(400, 1.0, 0.5, 5.0);
translate(0.0, 0.0, -4.0);
cpack(0xffffffff);
bgnclosedline();
p[0] = 0.0;
p[1] = 0.0;
v2f(p);
p[0] = 1.0;
p[1] = 1.0;
v2f(p);
p[0] = 1.0;
p[1] = -1.0;
v2f(p);
p[0] = -1.0;
p[1] = -1.0;
v2f(p);
p[0] = -1.0;
p[1] = 1.0;
v2f(p);
endclosedline();
swapbuffers();
break
}
}
/*
* Module initialization routine
*
* This routine must be listed as the “initialization” hook function
*/
void xex2_init()
{
long wid; /* window id from UI */
Widget da; /* drawing area widget */
XtTranslations translations; /* X translations structure */
static XtActionsRec actions[] ={/* X actions record */
“xaction”,(XtActionProc) xaction
};
static GLXconfig config[] ={/* GL configuration */
GLX_NORMAL, GLX_RGB, TRUE,
GLX_NORMAL, GLX_DOUBLE, TRUE,
GLX_NORMAL, GLX_ZSIZE, GLX_NOCONFIG,
0, 0, 0
};
/*
* 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”, glxMDrawWidgetClass, top,
GlxNglxConfig, config,
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);
}
/*
* 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;
}
|
c redraw procedure
subroutine refresh(widget,client)
integer widget
integer client
real p(2)
c configure the viewport
call reshap()
c clear the screen
call cpack(11563168)
call clear()
c do some example GL stuff
call perspe(400,1.0,0.5,5.0)
call transl(0.0,0.0,-4.0)
call cpack(-1)
call bgnclo()
p(1) = 0.0
p(2) = 0.0
call v2f(p)
p(1) = 1.0
p(2) = 1.0
call v2f(p)
p(1) = 1.0
p(2) = -1.0
call v2f(p)
p(1) = -1.0
p(2) = -1.0
call v2f(p)
p(1) = -1.0
p(2) = 1.0
call v2f(p)
call endclo()
call swapbu()
return
end
c input procedure
subroutine input(widget,client,info)
integer widget
integer client
integer info(5)
integer xevptr
integer xev(32)
pointer (xevptr,xev)
c get the pointer to the X event
c xev will now point to the XEvent structure. Be *very* careful extracting
c fields from this C struct. This is definitely not portable code.
xevptr = info(2)
c switch on the kind of event
c Event numbers may be found in /usr/include/X.h. Here are a few examples:
c KeyPress 2
c KeyRelease 3
c ButtonPress 4
c ButtonRelease 5
if ( xev(1) .eq. 4 ) then
c IMPORTANT NOTE: X origin is *upper* left;
c GL origin is *lower* left.
type *,'button pressed at ',xev(9),', ',xev(10)
endif
return
end
c user function
SUBROUTINE FGL ( WIN )
INTEGER WIN
C
C Complete the user function by adding subprogram lines here.
C
integer widget
integer config(12)
logical first
integer cxXtGLAreaInitialize
# include “cx/XtGLArea.inc”
c NOTE -- the redraw and input procedures MUST be declared external
external refresh
external input
common widget
data first/.true./
if ( first ) then
c first time initialization
first = .false.
c set up the GL configuration information
config(1) = GLX_NORMAL
config(2) = GLX_RGB
config(3) = 1
config(4) = GLX_NORMAL
config(5) = GLX_DOUBLE
config(6) = 1
config(7) = GLX_NORMAL
config(8) = GLX_ZSIZE
config(9) = GLX_NOCONFIG
config(10) = 0
config(11) = 0
config(12) = 0
c create the GL area
widget = cxXtGLAreaInitialize(WIN,config)
c register the redraw and input procedures with the widget
call cxXtGLAreaRedraw(widget,refresh,0)
call cxXtAreaCallbackAdd(widget,'inputCallback',input,0)
endif
c match the GL area size with the window size
call cxXtAreaResize(widget,WIN)
RETURN
END
|
These example programs use XtGLArea routines. The code is in /usr/explorer/src/MWGcode/Advanced/C/XtGLArea.c and in /usr/explorer/src/MWGcode/Advanced/Fortran/XtGLArea.f.
/*
* This program creates a GL window and draws inside it.
*
* In order to link this code in a module, the proper set of geometry
* and X Window System libraries must be defined in the user Imakefile
*/
#include <X11/Intrinsic.h>
#include <gl/glws.h>
#include <cx/XtArea.h>
#include <cx/XtGLArea.h>
/*
* Redraw procedure
*/
void refresh(void *w, void *client)
{
float p[2]; /* coordinate vector */
/*
* Configure the viewport and clear the display
*/
reshapeviewport();
cpack(0x00b070a0);
clear()
/*
* Now do some sample GL stuff
*/
perspective(400, 1.0, 0.5, 5.0);
translate(0.0, 0.0, -4.0);
cpack(0xffffffff);
bgnclosedline();
p[0] = 0.0;
p[1] = 0.0;
v2f(p);
p[0] = 1.0;
p[1] = 1.0;
v2f(p);
p[0] = 1.0;
p[1] = -1.0;
v2f(p);
p[0] = -1.0;
p[1] = -1.0;
v2f(p);
p[0] = -1.0;
p[1] = 1.0;
v2f(p);
endclosedline();
swapbuffers();
}
#include <cx/DataTypes.h>
#ifdef __cplusplus
extern “C” {
#endif
/*
* User function
*/
void cgl ( long win )
{
static first = 1;
/* GLX configuration information */
static GLXconfig config[] ={
GLX_NORMAL, GLX_RGB, TRUE,
GLX_NORMAL, GLX_DOUBLE, TRUE,
GLX_NORMAL, GLX_ZSIZE, GLX_NOCONFIG,
0, 0, 0
};
static Widget widget;
/*
* First time, do initialization
*/
if (first) {
first = 0;
/*
* Create the GL area and register the redraw proc
*/
widget = cxXtGLAreaInitialize(win, config);
cxXtGLAreaRedraw(widget, refresh, 0);
}
/*
* Match the GL area size with user interface window size
*/
cxXtAreaResize(widget, win);
}
#ifdef __cplusplus
}
#endif
|
c
c This example creates a GL window and draws inside it.
c
c redraw procedure
subroutine refresh(widget,client)
integer widget
integer client
real p(2)
c configure the viewport
call reshap()
c clear the screen
call cpack(11563168)
call clear()
c do some example GL stuff
call perspe(400,1.0,0.5,5.0)
call transl(0.0,0.0,-4.0)
call cpack(-1)
call bgnclo()
p(1) = 0.0
p(2) = 0.0
call v2f(p)
p(1) = 1.0
p(2) = 1.0
call v2f(p)
p(1) = 1.0
p(2) = -1.0
call v2f(p)
p(1) = -1.0
p(2) = -1.0
call v2f(p)
p(1) = -1.0
p(2) = 1.0
call v2f(p)
call endclo()
call swapbu()
return
end
c user function
subroutine fgl ( win )
integer win
C
C Complete the user function by adding subprogram lines here.
C
integer widget
integer config(12)
logical first
integer cxXtGLAreaInitialize
include “/usr/explorer/include/cx/XtGLArea.inc”
c NOTE -- the redraw procedure MUST be declared external
external refresh
common widget
data first/.true./
if ( first ) then
c first time initialization
first = .false.
c set up the GL configuration information
config(1) = GLX_NORMAL
config(2) = GLX_RGB
config(3) = 1
config(4) = GLX_NORMAL
config(5) = GLX_DOUBLE
config(6) = 1
config(7) = GLX_NORMAL
config(8) = GLX_ZSIZE
config(9) = GLX_NOCONFIG
config(10) = 0
config(11) = 0
config(12) = 0
c create the GL area
widget = cxXtGLAreaInitialize(win,config)
c register the redraw procedure with the widget
call cxXtGLAreaRedraw(widget,refresh,0)
endif
c match the gl area size with the window size
call cxXtAreaResize(widget,win)
return
end
|
Figure 9-3 shows what the sample output from this module looks like.
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, 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.
A combination module determines its actions in two ways. It can:
keep a table of known actions and make a selection from the action table based on the module name which invoked the executable. ImageVision Library modules such as AddImg and XorImg act in this way.
make use of an interpreter module that reads a script or program provided on an input port, then performs the actions dictated by the script. The DataScribe and LatFunction modules function on this principle.
Both AddImg and XorImg have module resource files that describe their control panels and ports. In addition, on the Build Options menu, 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 XorImg Makefile rules cause 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.
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 Explorer ports.
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™ 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:
Select whether you wish to have such an alternate executable.
Type in the name of the executable file.
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.
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.
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.
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. It is safest to use cxLoopBreak in all cases.
The While and Repeat loop controller modules simply examine the condition for the loop, which is a parameter value, and copies 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 and modify each one slightly to create loop controllers that pass additional data sets through. Just rename the module and augment the port list.
While module has the ports listed in Table 9-2:
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 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 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.
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 window has a text slot for listing them. Any module can cause extra files to be sent to $EXPLORERUSERHOME/modules when a make install command is issued.
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. Interperter Module Files
Interpreter Module | Module Executable |
|---|---|
LatFunction.interp | LatFunction |
GenericDS.interp | GenericDS |
MyInterp.interp | MyInterp |
These are the rules that the Module Builder follows during installation.
If there is no file, then no addition is made to the list of files to install.
If it finds a modulename.interp file, then all alternate executable modules relying on modulename will have the contents of the text or file-browser widget added to their file list.
If the modulename.interp file has anything other than NULL for suffix, then in the absence of the named widget, the file mod.suffix will be added to the installation list.
If the modulename.interp file has NULL for suffix, then no other files will be added to the installation list.
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 seven events in an Explorer session. These events are:
creating the module
initializing the module
destroying the module
making a connection to an input or output port in the module
breaking a connection to an input or output port in the module
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 Inventor Library and the GL 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 (GW) of every module. The table is empty by default. To fill in the default table with your own hook functions, you must:
Write the hook functions you want and define them in the user function
List each hook function in the Hook Functions window, located on the Build menu.
Hook functions written in C++ must have a C linkage. You can do this by declaring the function:
extern “C” my_hook_fcn( )
{ ...
}
|
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 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, particularly drawing area window IDs, are meaningless at this time.
Connecting input ports:
| C: | void ConnectInput(char *portName,int linkID); | |
| Fortran: | subroutine ConnectInput(portName,linkID); |
The MCW calls this function after the user has connected input portName to some output.
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.
The easiest way to generate hook function prototypes is to define them in the Module Builder as follows:
Select “Hook Funcs” from the Build menu in the Module Builder window to open the window (see Figure 9-4).
Type the name you have given each hook function in the slot next to its description. For example, you may type “DeleteHook” as the function name in the slot next to “Remove Hook Function.”
When you have finished listing the hook functions for the module, click on OK. When you select “Build,” the Module Builder creates the hook function table.
It also creates hook function prototypes in the correct language and places them in the prototype user function file created when you select “User Function” from the Prototypes menu. You need merely fill in the computation.
Then create the function prototype in the Module Builder(see “Hook Function Prototypes”).
Here are some points to note when you are writing a hook function.
When you request that a module be deleted, a “destroy” request is sent to the module. If the module is firing, the “destroy” request is not processed until the firing is completed., at which point the removal hook function will be called. However, if you delete the module a second time before it finishes firing, you kill the module's process and the removal hook function cannot be called.
If the module retains cached data, you must decrement the reference counts when the module is deleted. The module removal hook is responsible for removing any persistent data in shared memory. If the process is killed, however, this cleanup does not take place.
![]() | 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 on machines such as the Sun, the module will not build. |
Here are some examples of hook functions. This hook function initializes the Geometry Library for a module that requires it.
void myInitFunc()
{
/*
* Initialize the geometry library
*/
cxGeoInit();
}
|
This is an example of a connection hook function.
void myConnectFunc( portName, linkID )
char *portName;
int linkID;
{
/*
* Save the association of port and linkID
*/
myLinkArray[linkID] = portName;
}
|
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.
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. 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. |
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.
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. from the burden of 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.
All 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 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.
The 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.
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.
The shared memory arena is a fixed-size resource. It is at most 256 MB, and may not reach that size. 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 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.
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. 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 Explorer routine that allocates data memory. The cxDataAllocErrorGet routine returns TRUE (non-zero) if data could not be allocated. See cxDataAllocErrorGet for a list of the routines that allocate data memory.
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.
The 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:
retry the request itself, in the hope that memory has become available
reduce its memory requirements before attempting a new request
However, each failed attempt still generates the pop-up dialog box in the Map Editor.
The following examples show how to recover from allocation errors for some of the 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. |
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.
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;
}
.
.
}
|
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.
void userFunction()
{
cxParameter *parm = NULL;
cxLattice *lat1 = NULL;
cxLattice *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;
}
|
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.
![]() | Note: Do not decrement data or coords (although you can still use the pointers). Explorer has no way of tracking their values, and will not attempt to decrement the reference counts. |
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;
{
.
.
/* normal return */
return;
}
|
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.
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;
}
.
.
/* normal return */
return;
}
|
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. |
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;
}
|
Several subroutines in the API that allow you to increase the flexibility of your module. You can:
communicate with other devices and programs outside the Explorer environment, using cxInputAdd
time the occurrence of events, using cxTimerAdd
expand a filename into the directory path, using cxFileNameExpand
The use of these routines is described briefly in the next three sections.
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 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 Explorer environment. For example, you can start up and communicate with a data base server from your Explorer 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 /usr/explorer/src/MWGcode/Advanced/C/InputAdd.c.
#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 client 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
|
You can create a sample module in the Module Builder with this code as the user function file. Then type a command, for example, “ls,” in the text slot and observe the results.
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.
Here is a sample module in C.
/*
* 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>
#ifdef __cplusplus
extern “C” {
#endif
/*
* Callback routine for the timer.
* All this does is causes the module to fire.
*/
void myTrigger()
{
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, 0);
}
printf(“triggered\n”);
}
#ifdef __cplusplus
};
#endif
|
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, ~ or $<environment variable>, 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 examples is in /usr/explorer/src/MWGcode/ Advanced/C/FilenameExpand.c.
#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 cxExpandFilename must be copied
if it is to be saved for future use. Subsequent calls
to cxExpandFilename will write over this buffer.
*/
printf(“delivered filename: %s\n”,filename);
printf(“expanded filename: %s\n”,cxExpandFilename(filename));
/* increment counter for %n */
cxFilenameIndexIncrement();
}
|
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. 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”). 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:
Try building the sample Explorer modules provided in source format within your own module tree.
Set up your own build system, skipping the attempt to build the Explorer modules.
Path #1 provides a good test of the installation of 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.
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 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.
If you decide to build the sample 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 /usr/explorer/src/* ~/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.
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 /usr/explorer/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 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” in Chapter 2 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.”
The Module Builder can be used to create parallel or multiprocessing modules, which can use several processors to speed up the module's computation. While the approach to building such a module depends on your choice of multiprocessing programming, it uses the same compiler and loader flags in all cases. You may wish to add additional control panel widgets for more control over the aspects of the multiprocessing version of a module.
There are essentially three types of multiprocessing module, based upon:
explicit parallelism (for instance, using the m_fork system call)
The standard build options in Explorer cause modules to be linked against libmalloc.a for memory allocation routines. However, multiprocessing modules must link with the memory allocation routines from libmpc.a. To ensure that this happens, enter -lpmc in the Libraries slot on the Module Builder main window when you build the module.
You must arrange for the -mp flag to be passed to the C or Fortran compiler by setting the CDEFINES or FDEFINES symbol in the user Makefile (see Appendix A for a description of these and other user Makefile flags). For instance, the Fortran symbol is set by the following line in the user Makefile:
FDEFINES = -mp |
automatic parallel compilation (Fortran PFA and C PCA)
You must arrange for the -pfa keep flag to be passed to the C or Fortran compiler by setting the CDEFINES or FDEFINES symbol in the user Makefile. For instance, the Fortran symbol is set by the following line in the user Makefile:
FDEFINES = -pfa keep |
With these compiler settings, the resulting module will execute several threads in parallel.
There are two main run-time considerations for multiprocessing modules:
There are two approaches to controlling the number of threads to run in parallel. The shell environment variable MP_SET_NUMTHREADS controls the number of threads in all processes not otherwise controlled. This can be used to cause all multiprocessing modules to use a fixed number of threads.
In many cases, a finer degree of control over the number of threads is warranted. Since there is no Explorer routine for controlling the number of threads, the best approach is often to add a control panel widget for the number of threads, which a user or a module is then free to vary at run-time according to problem or map characteristics.
It is often desirable to block the threads of a multiprocessing module when the module stops firing so that the idle processes do not consume cycles needed elsewhere in the Explorer map. The shell environment variable MP_BLOCKTIME provides an external method of controlling this blocking, but its default value may be too large for efficient Explorer execution. A better approach is for the module to block its own threads as it is about to return control to the MCW (using the system call mp_block) and then unblock them on a subsequent firing (using the system call mp_unblock).