Developers' Guide

(for SCIRun Version 1.24.2)


Table of Contents

Introduction
I. The src Tree
1. Core
Alorithms
Containers
Datatypes
Geom
Geometery
Math
Persistent
Tester
Winfix
2. Dataflow
Comm
Constraints
GUI
Modules
Ports
Widgets
XML
3. Nets
II. Development
4. SCIRun C++ Coding Standards
Required
Recommended
5. Dataflow Development
Introduction
Developing new SCIRun dataflow elements
Creating new modules
Creating Modules with the Module Wizard
Creating a new algorithm
Creating new ports
Creating new datatypes
6. Dynamic Compilation
Introduction
Disclosure
Programming with SCIRun dynamic compilation
get_type_description()
7. The SCIRun Interface to Tcl
Introduction
Programming with the SCIRun GuiInterface
GuiVar
8. SCIRun Memory Management
Introduction
Programming with SCIRun Memory Management
Environment Variables
Overloaded Functions
9. Persistent Data
Introduction
Programming with SCIRun Persistent data
Persistent Data
Persistent Object
PersistentTypeID
Maker Functions
Pio Streams
Pio() versus io()
Examples
10. Custom Tk Widgets for SCIRun
Introduction
tk command
tkOpenGL
11. SCIRun Utilities
Introduction
Programming with SCIRun Utilities
Assertion
Timer
CPUTimer
WallClockTimer
TimeThrottle
12. SCIRun Exceptions
Introduction
Programming with SCIRun Exceptions
ASSERT
ASSERTFAIL
13. 3D Widgets and Constraints in SCIRun
Introduction
Programming 3D Widgets

List of Figures

5.1. The basic package directory structure.
5.2. The basic Modules directory tree structure
5.3. The Module Wizard with a blank module
5.4. Building the Sort module

Introduction

The src Tree

Chapter 1. Core

Alorithms

Containers

Datatypes

Geom

Geometery

Math

Persistent

Tester

Winfix

Chapter 2. Dataflow

Comm

Constraints

GUI

Modules

Ports

Widgets

XML

Chapter 3. Nets

Development

Chapter 4. SCIRun C++ Coding Standards

Table of Contents

Required
Recommended

Required

  1. All code and comments are to be written in English.

  2. All files must include appropriate license information.

  3. Use the C++ mode in GNU Emacs and sci-c++-style.el to format code.

  4. Include files in C++ always have the file name extension .h.

  5. Implementation files in C++ always have the file name extension .cc.

  6. Every include file must contain a “guard” that prevents multiple inclusions of the file, for example:

    
    #ifndef Geometry_BBox_h
    #define Geometry_BBox_h
    
    // Code...
    
    #endif // Geometry_BBox_h
    
    

    The name of the guard should be of the following form: libraryname_filename_h

  7. Use forward declarations wherever possible as opposed to including full definitions for classes, functions, and data:

    
    // Class
    class PointToMe;
    
    // Function
    void my_function(PointToMe &p, PointToMe *ptm);
    
    // Data
    PointToMe *m;
    
    

  8. The names of variables and functions will begin with a lowercase letter and include underscore word separators. Names of constants should be in all CAPITALS, with underscore word separation:

    
    static int CONSTANT_INT_FIVE = 5;
    void my_function_name();
    int my_local_variable_name = 0;
    
    

  9. The names of class member variables are to end with an underscore, “_”:

    
    struct SomeObject {
      int count_;
    };
    
    

  10. The names of abstract data types (eg: Classes), and structs are to begin with an uppercase letter and each new word in the name should also be capitalized.

    
    class MyNewClassName {
        // ...
    };
    
    

  11. Declare as const a member function that does not change the state of an object.

  12. Constants are to be defined using const or enum. Never use #define to create constants.

  13. A class which uses new to allocate instances managed by the class must define a copy constructor and an assignment operator.

  14. All classes which are used as base classes and which have virtual functions, must define a virtual destructor.

  15. Always use delete[] to deallocate arrays.

  16. Use exception handling to trap errors (although exceptions should only be used for trapping truly exceptional events).

    Our exception model includes two levels of exceptions. The top level of exceptions are defined in Classlib/Exceptions.h and are thrown when a class specific exception is not appropriate.

    The bottom level of exceptions are class specific, defined in the class that throws them, and are subclassed off of the top level exceptions. These class specific exceptions are exceptions that can be caught and handled from the calling function (1 level above the class.) However, if the calling function chooses not to (or cannot) handle the class specific exception, the exception will propagate to the next level at which point it can be trapped and handled in the form of a top level exception. An example of a class specific exception would be a StackUnderFlowException for a stack class.

  17. Comments should support generated documentation format. Comments in declarations should be of the form:

    //! …
    

    or

    /*! … */ 
    

    These comments will then be visible in our online documentation.

    Comments in the definition part of your code need not have the !, as all the code is visible online including comments.

    Create comments that help the reader navigate your code. Comments should help the reader find the area of code he/she is looking for quickly.

  18. Do not use identifiers which begin with one ('_') or two ('__') underscores.

  19. Never use the $Id or $Log cvs directives. This confuses merging between branches.

  20. Do not use #define to obtain more efficient code— use inline functions instead.

  21. Avoid the use of numeric values in code; use symbolic values instead. This applies to numeric values that are repeated within the code but represent the same value. Eg: MAX_ARRAY_SIZE = 1024.

  22. Do not compare a pointer to NULL or assign NULL to a pointer; use 0 instead.

  23. Avoid explicit type conversions (casts). If you must cast use dynamic_cast and insert a comment explaining why.

  24. Never convert a constant to a non-constant. Use mutable if necesary. However, be aware of the thread safety problems this causes.

  25. Never use goto.

  26. Do not use malloc, realloc, or free.

Recommended

  1. Never use more than 80 columns per line.

  2. An include file for a class should have a file name of the form class name.h. Use uppercase and lowercase letters in the same way as in the source code.

  3. Never include /usr/include/*.h, for example iostream.h in any header file. This causes a huge amount of code to be recursively included and needlessly compiled. Use forward declarations to avoid this

  4. Group local includes together, then group system includes together.

  5. Avoid global data if possible.

  6. Optimize code only if you know that you have a performance problem. Think twice before you begin.

  7. Always force your compiler to compile with the maximum warning setting, and before you check in code, fix all warnings.

  8. Place machine-dependent code in a special file so that it may be easily located when porting code from one machine to another.

  9. Encapsulate global variables and constants, enumerated types, and typedefs in a class.

  10. Functions in general should not be more than 25 lines long. If you find this situation, break the function into several smaller functions.

  11. If a function stores a pointer to an object which is accessed via an argument, let the argument have the type pointer. Use reference arguments in other cases.

  12. When overloading functions, all variations should have the same semantics (be used for the same purpose).

  13. Do not assume that you know how the invocation mechanism for a function is implemented.

  14. Do not assume that an object is initialized in any special order in constructors.

  15. Do not assume that static objects are initialized in any special order.

  16. Use a typedef to simplify program syntax when declaring function pointers, or templated types.

  17. When two operators are opposites (such as == and !=), it is appropriate to define both.

  18. Use constant references (const &) instead of call-by-value, unless using a pre-defined data type or a pointer.

  19. Minimize the number of temporary objects that are created as return values from functions or as arguments to functions.

  20. Do not write code which is dependent on the lifetime of a temporary object.

  21. Do not assume that the operands in an expression are evaluated in a definite order. Use parenthesis to enforce an order.

  22. Use parentheses to clarify the order of evaluation for operators in expressions.

  23. Avoid using shift operations instead of arithmetic operations.

  24. Use cout instead of printf.

  25. Do not assume that an int and a long have the same size.

  26. Do not assume that pointers and integers have the same size.

  27. Do not assume that longs, floats, doubles or long doubles may begin at arbitrary addresses.

  28. Avoid pointer arithmetic.

  29. Always use plain char if 8-bit ASCII is used. Otherwise, use signed char or unsigned char.

  30. Do not assume that a char is signed or unsigned.

  31. Do not depend on underflow or overflow functioning in any special way.

Chapter 5. Dataflow Development

This chapter describes dataflow programming in general, and provides specific details for dataflow programming within SCIRun.

Introduction

The goal of SCIRun is to provide a problem solving environment in which a scientist, with little programming experience, can easily solve a problem using powerful tools such as parallel super computing for number crunching, and high performance graphics for interactive visualization. SCIRun accomplishes this goal by employing a programming paradigm called dataflow.

Dataflow programming essentially provides coarse grained, configurable algorithms that, when tied together, act as a program for solving a problem. A dataflow programmer needs to know very little about how to program a parallel super computer, or how to use the latest graphics hardware to generate a visualization, in order to use those tools - all she needs to do is focus on the science behind the problem being solved.

In SCIRun the dataflow paradigm is manifested visually as a set of boxes, called modules, each of which contains a variety of pre-implemented algorithms. The modules have data ports, both input and output, for accepting and relaying data. The flow of data through the modules is dictated by connections made between input and output ports: The output port of one module is connected to the input port of one or more other modules. A series of connected modules is called a network, which can be thought of as a program for solving a particular type of problem.

There are several pre-implemented modules and ports in SCIRun that allow a dataflow network programmer to build networks that solve a variety of novel problems. However, the the real power of SCIRun is in it's ability to be extended through the development of new modules and new ports which expand the number of types of solvable problems.

The rest of this guide explains how to develop new SCIRun dataflow elements, and explains how to use powerful tools such as 3D widgets, threads, etc. in SCIRun. Development within SCIRun requires a working knowledge of C++ programming, Unix file systems etc. knowledge of Tcl/Tk scripting will be useful.

Developing new SCIRun dataflow elements

Creating new modules

From the most abstract perspective, a module is one or more algorithms that solves a single, specific and coarse grained problem. For example, suppose you have a large list of english words that you want to sort. There are several algorithms that you may employ to to solve this problem such as bubble sort, quick sort, etc. A module that solves this problem, likely named Sort, would be a collection of one or more of these sorting algorithms.

On a more concrete level, a module is comparable to a function or procedure (of a high level text based programming language like C) that also implements one or more algorithms to solve a specific problem. The prototype for such a function that solves the sort problem might look like this:


	void Sort(vector<string> words_to_sort, int alg_to_use);
      

Where words_to_sort is a random, possibly large, list of words to be sorted (the data), and alg_to_use is a single integer (the control) which is used to select one of the implemented algorithms.

The Sort module is similar to the Sort function in many ways: They both have input (data and control), they both have output, they both have a single point of entry for execution, and they can both be used modularly in any program. The ports of a module serve some of the same functions as the formal parameters to a function: they can accept and relay data and they enforce type matching. One important difference is that the Sort module seperates the mode for communicating inputs of different kinds, i.e. data is treated differently and seperately from control.

On the lowest level a module is almost exactly like the function. The module is actually implemented as a C++ class with one member function named execute(). The execute member function is identical to the C function in many ways. In fact, it is possible to cut the contents out of the C function and paste them into the execute function of the module and achieve the same functionality, with one caveat: the module does not pass data or control to the execute function through formal parameters, like the C function. Instead, the execute function acquires the data and control by using additional function calls. Here's what the execute function for the example Sort module might look like:


	void Sort::execute() 
	{
	vector<string> *words = scinew vector<string>;
	int alg;

	get_data(words);    // get the data from the input port
	get_control(alg);   // get the control from the GUI

	//
	//  one can either paste the contents of the sort
	//  function here, or simply call it:
	//
	
	if (words.size()>1) sort(words,alg);
	
	send_data(words);  // send the result to the output port
	}
      

Then a module is simply a C++ class, and in order to develop a new module, one simply needs to create a new class. Creating a new module can be done semi-automatically using SCIRun's Module Wizard, or can be done by hand. The Module Wizard will be discussed in greater detail later in this chapter. Regardless of how the new module is created, there are some conventions that must be followed in order to have the new module be usable from within SCIRun, for example, the class needs to inherit from the Module base class, and it needs to implement a specific set of functions. Let's build the Sort module by hand as an example. By convention, a module is declared and defined in a single C++ file (.cc extension) with the base name of the module i.e. Sort will be completely implemented in the single file "Sort.cc". It is not incorrect to have two files to declare and define a module (.h and .cc), but it is easier to just use one.

To get started, we need to declare a class that will become the Sort module:


	#include <Dataflow/Network/Module.h>     // module base class
	#include <Dataflow/Ports/WordListPort.h> // wordlist port classes 
	#include <Core/GuiInterface/GuiVar.h>    // GUI data interface

	#include <string>
	#include <vector>

	using std::string;
	using std::vector;

	class Sort : public Module
	{
	protected:

	// most modules have data-members and member-functions that are 
	// unique to them

	// Sort-specific port and gui data members
	WordListIPort *iport_;
	WordListOPort *oport_;
	GuiInt algo_;

	// Sort-specific member functions
	bool get_data(vector<string>*);
	bool get_control(int&);
	bool send_data(vector<string>*);

	public:

	// all modules need to declare and implement at least these
	// functions:
	//
	// - a constructor
	// - a virtual destructor
	// - a virtual execute function

	// constructor (with appropriate initializers)
	Sort(const string& id) :
	Module("Sort", id, Source, "WordList", "SCIRun"),
	algo_("alg_to_use", id, this),
	iport_(0),
	oport_(0),
	algo_(0) { /* do nothing */ }

	// virtual destructor
	virtual ~Sort() { /* do nothing */ };

	// virtual execute function
	virtual void execute();
	
	};
      

We've already implemented the execute function, but it depends on get_data(), get_control() and send_data(). Let's write those now. Assuming that our module actually has the GUI and ports needed (how to provide a GUI and ports to a module will be discussed later on), we now have to get the data and control:


	bool Sort::get_data(vector<string> *list)
	{
	// data is passed between modules as handles
	WordListHandle wlh;

	// first get a pointer to the input port named "InList"
	iport_ = (WordListIPort*)get_iport("InList");

	// verify that the port was found
	if (!iport_) return false;

	// verify that the port is connected and has data.
	// if so, the handle will be associated with the data.
	// get() is a blocking call when the the port is connected,
	// and simply returns with a NULL value when not connected.
	if (!iport_->get(wlh)) return false;

	// get a pointer to the data from the handle
	vector<string> *inlist = wlh.get_rep();

	// copy the data to the reference parameter "list".
	// the nature of dataflow requires a module
	// to copy the incoming data, if the data is to 
	// be modified.  If the data is only examined, 
	// no copy is necessary.
	unsigned length = inlist->size();
	list->resize(length);
	for (int loop=0; loop<length; ++loop)
	(*list)[loop]=(*inlist)[loop];

	return true;
	}

	bool Sort::get_control(int &alg)
	{
	// prep the GUI element associated with algo_
	algo_.reset();

	// get the state of the GUI element
	alg = algo_.get();

	return true;
	}
      

Now we have enough information (the data and control) to call the sort function. After that we'll want to relay the results to the next module (or modules) in the network, i.e. we have to send the results of the sort function to the output port:


	void Sort::send_data(vector<string> *outlist)
	{
	// get a pointer to the output port named "OutList"
	oport_ = (WorldListOPort*)get_oport("OutList");

	// send the data to the port.  the pointer is automatically
	// wrapped into a WordListHandle by the send
	oport_->send(outlist);
	}
      

We have now implmented a new module. But it isn't quite complete. We made a couple assumptions while writing the class: the module has one input port and one output port and the module has a GUI with one element capable of representing an integer. This leads us to the next point. A module is not just a C++ file which defines a new module class. A module needs at least one, and possibly two, more files: an XML file and a TCL file.

The XML file is used to describe how many and what type of ports a module has, which category it belongs to and much more. The TCL file describes the GUI of a module, if it has one. Let's create these files for the Sort module. These files, also by convention, have the base name of the module.

First, the Sort.xml file:


	<component name="Sort" category="WordList">
	<overview>
	<authors>
	<author>
	Eddie Murphy
	</author>
	</authors>
	<summary>
	<p>
	This module sorts a word list
	</p>
	</summary>
	</overview>
	<io>
	<inputs lastportdynamic="no">
	<port>
	<name>InList</name>
	<datatype>SCIRun::WordList</datatype>
	</port>
	</inputs>
	<outputs>
	<port>
	<name>OutList</name>
	<datatype>SCIRun::WordList</datatype>
	</port>
	</outputs>
	</io>
	<component>
      

Second, the Sort.tcl file:


	itcl_class SCIRun_WordList_Sort {
	inherit Module
	
	constructor { config } {
	set name Sort
	
	set_defaults
	}

	method set_defaults {} {
	global $this-alg_to_use
	set $this-alg_to_use 1
	}

	method ui {} {
	set w .ui[modname]
	if { [winfo exists $w] } {
	raise $w
	return
	}

	toplevel $w

	label $w.title -text "Sort Algorithms"
	label $w.option1 -text "1. Quick Sort"
	label $w.option2 -text "2. Bubble Sort"
	label $w.option3 -text "3. Insertion Sort"

	entry $w.choice -textvar $this-alg_to_use

	pack $w.title $w.option1 $w.option2 $w.option3 $w.choice -side top
	}
	}
      

Now that we have all the files needed for a new module, we need a place to put them. All SCIRun modules are members of groups - categories and packages - which are directory structures for organizing modules. These directory structures are converted to .so libraries at build time by compiling and linking the files within them. Packages, categories and the modules inside them are only usable by SCIRun in their .so form, so let's put the Sort module into a category and a package, and convert it to a .so library file.

All SCIRun packages have the same basic directory structure:

Figure 5.1. The basic package directory structure.

The basic package directory structure.

In fact, that directory structure is required in order for a package to be considered valid. It is possible for a package to have more directories, but generally not fewer. Even the main SCIRun source tree, which is a package in itself, exhibits this structure. Each package has two sides in it's source tree. Core defines datatypes and algorithms, which are not necessarily associated with dataflow programming. Dataflow defines ports and modules, which are based on datatypes and algorithms found in the Core side.

Packages contain one or more categories, which reside inside the Dataflow/Modules directory. Each category contains one or more modules:

Figure 5.2. The basic Modules directory tree structure

The basic Modules directory tree structure

Suppose we would like to put Sort inside the WordList category of the SCIRun package. When the package and category already exist, then we just need to copy the files we made into the appropriate directories:

	cp Sort.cc SCIRun/src/Dataflow/Modules/WordList
	cp Sort.xml SCIRun/src/Dataflow/XML
	cp Sort.tcl SCIRun/src/Dataflow/GUI
      

If the package and/or category does not already exist, then we would first have to build the appropriate directory structure to put the files into.

Recall that any given package is only useful to SCIRun in it's .so form. In order to use Sort, we'll have to make sure that it gets included into the building of the SCIRun package. Fortunately, SCIRun comes with a makefile system that knows how to build all the .so's belonging to itself and all external packages. The makefile system is composed of makefile fragments found in every directory within the SCIRun source tree and it's packages. The fragments all live in files named "sub.mk". The contents of a sub.mk file depend on which directory it lives in. The following are sub.mk files for the SCIRun modules directory (SCIRun/src/Dataflow/Modules) and the WordList category directory (SCIRun/src/Dataflow/Modules/WordList), respectively:


	SRCDIR := Dataflow/Modules
	
	SUBDIRS := \
	$(SRCDIR)/DataIO\
	$(SRCDIR)/Fields\
	$(SRCDIR)/Math\
	$(SRCDIR)/Render\
	$(SRCDIR)/Visualization\
	#[INSERT NEW CATEGORY DIR HERE]
	
	include $(SCIRUN_SCRIPTS)/recurse.mk
      

	include $(SCIRUN_SCRIPTS)/smallso_prologue.mk
	
	SRCDIR   := Dataflow/Modules/WordList
	
	SRCS     += \
	#[INSERT NEW CODE FILE HERE]
	
	PSELIBS := Dataflow/Network Dataflow/Ports \
	Core/Datatypes Core/GuiInterface \
	Core/Persistent Core/Util \
	Core/TkExtensions
	
	LIBS := -lm
	
	include $(SCIRUN_SCRIPTS)/smallso_epilogue.mk
      

To add the new module to the build system, all we have to do is add the category directory to the first sub.mk file, just before the #[INSERT ... HERE] comment:

	
	SRCDIR := Dataflow/Modules
	
	SUBDIRS := \
	$(SRCDIR)/DataIO\
	$(SRCDIR)/Fields\
	$(SRCDIR)/Math\
	$(SRCDIR)/Render\
	$(SRCDIR)/Visualization\
	$(SRCDIR)/WordList\
	#[INSERT NEW CATEGORY DIR HERE]
	
	include $(SCIRUN_SCRIPTS)/recurse.mk
      

and then add the .cc file to the second sub.mk file, again just before the #[INSERT ... HERE] comment:


	include $(SCIRUN_SCRIPTS)/smallso_prologue.mk
	
	SRCDIR   := Dataflow/Modules/WordList
	
	SRCS     += \
	$(SRCDIR)/Sort.cc\
	#[INSERT NEW CODE FILE HERE]
	
	PSELIBS := Dataflow/Network Dataflow/Ports \
	Core/Datatypes Core/GuiInterface \
	Core/Persistent Core/Util \
	Core/TkExtensions
	
	LIBS := -lm
	
	include $(SCIRUN_SCRIPTS)/smallso_epilogue.mk
      

Now we can build the newly created Sort module by issuing a make command in the build directory. Enter cd BUILD_DIR and then gmake. After that we can run SCIRun and use the new module.

Some important aspects of developing a new module that we've glossed over are all the conventions used. We already know that each of the module files (.cc, .xml and .tcl) must all have the same base name (i.e. the name of the module), but there are others as well. Make sure that the name of the package, category and module are spelled exactly the same, including case, in each of the files, .cc, .xml, and .tcl, respectively:


	...
	Module("Sort", id, Source, "WordList", "SCIRun"),
	...
      

	...
	<component name="Sort" category="WordList">
	...
      

	...
	itcl_class SCIRun_WordList_Sort { 
	...
      

After editing sub.mk files, make sure that each of the entries in the SUBDIRS or SRCS are followed by a backslash-endline pair, and that the #[INSERT ... HERE] line isn't changed, other than moved down a line:


	SUBDIRS := \
	$(SRCDIR)/DataIO\
	$(SRCDIR)/Fields\
	$(SRCDIR)/Math\
	$(SRCDIR)/Render\
	$(SRCDIR)/Visualization\
	$(SRCDIR)/WordList\
	#[INSERT NEW CATEGORY DIR HERE]
      

	SRCS     += \
	$(SRCDIR)/Sort.cc\
	#[INSERT NEW CODE FILE HERE]
      

Creating Modules with the Module Wizard

Until you become a seasoned SCIRun developer, all the work discussed above will seem too daunting to get start on development of a new module. For this reason SCIRun comes with the Module Wizard, a tool for automatically generating all the files needed to start a new module from scratch. It even edits the makefile system to add the new module.

The Module Wizard has a visual interface in which you graphically construct the module you wish to create. Once you are finished, the Module Wizard uses the information gathered to automatically create skeletons of all the needed files, which are fully ready to be built. All that's left to do is fill in the execute function and design a GUI.

Access the Module Wizard from the main SCIRun menu: File->Wizards->Create Module Skeleton… The Module Wizard starts up with a blank module in the Wizard's IO tab, as shown in figure Figure 5.3.

Figure 5.3. The Module Wizard with a blank module

The Module Wizard with a blank module

Add the module's name, its package, and its category in the appropriate text fields.

Check Has GUI if the module will have a graphical user interface.

Add ports by pressing buttons Add Input Port and Add Output Port. For each port you will be prompted for the port's name, it's data type, and a short description of data passing through the port. See Figure 5.4.

Figure 5.4. Building the Sort module

Building the Sort module

Ports can be deleted by selecting Delete from a port's popup menu. A port's popup menu is activated by pressing mouse button 3 while the pointer is over a port. Port information can be edited by selecting Edit from the popup menu. To do that, select Edit from the popup menu. See Figure 5.4.

After completing information in the I/O tab click the Description tab. Information in the Description tab documents the module's function. Add the names of one or more authors. Provide a one or two sentence summary of the module's function in the Summary field.

Once all information has been provided, press Create to generate the new module's skeleton.

Creating a new algorithm

In SCIRun an algorithm is simply a function or set or functions that can be used stand-alone. That is, they aren't necessarily part of code that is only useful from within SCIRun or dataflow programming in general. Algorithms often make up the "guts" of a module. Algorithms are chunks of code considered useful enough to be used by many modules, while being both general and specific enough to be useful in many places. Algorithms generally have a single point of entry, and most are implemented as templated functions. Algorithms do not require a GUI, but do require data and control given via formal parameters.

The sort function used by the Sort module is an excellent example of a SCIRun algorithm. It's a simple C function that can be used by any module that needs to sort a list of words.

Creating a new algorithm for SCIRun is as simple as writing any such function. There are no other conventions for writing or using algorithms, except in the case of dynamically compilable algorithms which are discussed in a later chapter. Algorithms are stored in files located in the Core/Algorithms directory of a package directory. They are usually declared in a .h file and defined in a .cc file.

Creating new ports

Ports are used in SCIRun to pass data from one module to another. Data is sent out an output port of one module and received in the input port of another module. Ports enforce type matching, which prevents network programmers from sending the wrong kind of data to a module.

As of the writing of this document, there is no automatic way to create new ports in a manner similar to the Component Wizard for modules. Instead, ports must be created by hand. Fortunately, most new ports require very little code, and are easily created for existing datatypes.

Port, just like modules, are just C++ classes. So, in order to create a new port we, again, just need to write a new class. However, most ports have the exact same behavior, they just enforce connections for different types. For this case there is a standard templated port (called Simple), and all that is needed is to declare such a standard port that accepts a new datatype.

To create a new standard port you need to generate two files: a .h file for declaring your new port and a .cc file for defining it. The .h file is used only to declare the input and output port types. The .cc file is used to statically assign a color and name (which is usually just the name of the type it accepts). Let's create a port for the WordList datatype used by the Sort module.

First, the WordListPort.h file:


	#ifndef SCIRun_WordListPort_h
	#define SCIRun_WordListPort_h 1
	
	#include <Dataflow/Ports/SimplePort.h>
	#include <Core/Datatypes/WordList.h>
	
	namespace SCIRun {

	// declare new type of port (both input and output) 
	// based upon the standard "Simple" port
	typedef SimpleIPort<WordListHandle> WordListIPort;
	typedef SimpleOPort<WordListHandle> WordListOPort;
	
	} // End namespace SCIRun
	
	#endif
      

Second, the WordListPort.cc file:


	#include <Dataflow/Ports/WordListPort.h>
	#include <Core/Malloc/Allocator.h>
	
	namespace SCIRun {
	
	// declare maker functions for the ports 
	extern "C" {
	IPort* make_WordListIPort(Module* module, const string& name) {
	return scinew SimpleIPort<WordListHandle>(module,name);
	}
	OPort* make_WordListOPort(Module* module, const string& name) {
	return scinew SimpleOPort<WordListHandle>(module,name);
	}
	}
	
	// assign values to the static members port_type and port_color
	template<> string SimpleIPort<WordListHandle>::port_type("WordList");
	template<> string SimpleIPort<WordListHandle>::port_color("forestgreen");
	
	} // End namespace SCIRun
      

Creating new datatypes

Chapter 6. Dynamic Compilation

This chapter introduces the concept of Dynamic Compilation within SCIRun.

Introduction

SCIRun makes extensive use of C++ templates. Templates are a powerful mechanism that allows writing algorithms or data structures once, while allowing them to be usable with many types of data. However, in order to use any particular type of data, the templates need to know about them at compile time. Dynamic compilation allows templates written in the past to be applied to new data types at run time by dynamically compiling code.

Disclosure

The Disclosure directory contains objects that discover information about types, as well as objects that produce previously uncompiled types.

This directory also contains the TypeDescription object, as well as the DynamicLoader. TypeDescription gives a recursive definition of type at runtime. One can query a string that indicates the type from the this object. It also provides information that can by used by the DynamicLoader to compile types that support TypeDescription.

The DynamicLoader writes a .cc file, compiles it, then loads in the .so all at runtime. The main function is to compile algorithms that are templated on various field types in SCIRun. This gives SCIRun a minimal set of template instantiation based on how a user happens to use SCIRun, as opposed to the combinatorial explosion of template bloat that is the alternative.

Programming with SCIRun dynamic compilation

A developer is likely to use this code from within modules that has a field port. A FieldHandle can be one of *many* types of fields. An algorithm can be templated on the field, and use the DynamicLoader to manage compilation and loading of the algorithm without the module code ever needing to know the exact type of the field.

The main use of code in this directory is to compile and load templated algorithms for use in modules.

get_type_description()

There are many of these functions. Each is overloaded on the type that the returned TypeDescription supports. There must be one of these if your type is to be supported in dynamic compilation.

An example of a simple get_type_description for int.

       const TypeDescription* get_type_description(int*)
       {
       	 static TypeDescription* td = 0;
       	 static string nm("int");
       	 static string path("builtin");
       	 if(!td){
       	   td = scinew TypeDescription(nm, path);
       	 }
       	 return td;
       }
      

An example of a templated get_type_description function.

       template <class T >
       const TypeDescription* get_type_description(vector <T >*)
       {
         static TypeDescription* td = 0;
         static string v("vector");
         static string path("std::vector"); 
         if(!td){
           const TypeDescription *sub = SCIRun::get_type_description((T*)0);
           TypeDescription::td_vec *subs = scinew TypeDescription::td_vec(1);
           (*subs)[0] = sub;
           td = scinew TypeDescription(v, subs, path);
         }
         return td;
       }
      

Chapter 7. The SCIRun Interface to Tcl

This chapter describes how to easily communicate between the GUI (Tcl/Tk) and the C++ elements of SCIRun modules.

Introduction

SCIRun uses TCL/TK as its GUI font end. However, the TCL/TK was not designed with a clean interface between TCL code and C/C++. The purpose of this directory is to provide an abstraction layer between the two and make the task of moving data between the TCL and the C++ portions of SCIRun transparent to the user.

Most of the code in the Dataflow/GuiInterface directory is used internally in SCIRun. The only exception are the GuiVars which can be access from both the tcl and the C++ codes. These variables provide a transparent mechanism in C++ to set or get the tcl variables they represent. Each such tcl variable is associated with a Module and thus contains information about the module tcl id and a pointer to the module.

Programming with the SCIRun GuiInterface

On the TCL side, the code should access the variables as regular tcl variables. On the C++ side, the code needs to declare these variables inside a Module and access them via the get() and set() functions.

GuiVar

GuiVars are variables that encapsulate the interaction between the C++ code and the GUI (Graphical User Interface) code. The variable does not hold the actual value, rather it holds information which is used to access the corresponding variable on the GUI side. From the C++ side the user may set the variable value via a set() function and retrieve the value via a get() function.

There are several specialization of GuiVar for particular variable types such as GuiInt, GuiString and GuiPoint.

In a tcl code, i.e. in a tcl module:

      itcl_class foo {
         ...
        method set_defaults {} {
	  global $this-min
	  global $this-max

	  set $this-min 0                 # set to 0
	  set $this-max [set $this-min]   # '[set $this-var]' returns its value
         }

         ...
       }
    

In the C++ side, i.e. in a module:

       class Demo : public Module {
         ...
         GuiInt gui_min, gui_max;         // define GUI variables

	 Demo( const clString& id );
	 void init();
       };

       Demo::Demo( const clString &id ) 
         : Module(...), 
	   gui_min("min",id, this),       // initialize a variable with
	   gui_max("max",id,this)         // its name on the tcl side.
       {
	...
       }

       void Demo::init() 
       {
	  gui_min.set(7);               // set a tcl variable
	  int i = gui_max.get();        // get a value from the tcl side
       }
    

Chapter 8. SCIRun Memory Management

This chapter explains how and why SCIRun manages memory usage.

Introduction

The SCIRun memory allocation and tracking system is defined in Core/Malloc. This system is an abstracted layer built upon the normal memory management tools provided in C/C++.

At one point or another, most programs run into problems with dynamically allocated memory such as stray pointers and memory leakage. Because SCIRun is a large and complex collection of core routines as well as a framework for the creation and use of user modules (used to extend its base functionality,) it is important to be able to track the allocation and usage of memory.

SCIRun provides a transparent, non-invasive method of tracking memory usage. By overloading the basic C memory management routines (malloc, free, calloc, realloc, memalign, and valloc) as well as the C++ routines (new and delete,) SCIRun can record and monitor the allocation of memory throughout the system. This makes it much easier to perform two very important functions: 1) Know where memory was allocated, and thus, if appropriate, where it was not freed, and 2) easily track the amount of memory being used by the entire system. Core/Malloc provides one other important benefit: Memory allocation can be, and often is, faster because of the smarter algorithms it uses.

Programming with SCIRun Memory Management

For the reasons mentioned above, it is important for developers to use the Core/Malloc routines in order for themselves or others to localize and fix and memory problems encountered when using SCIRun.

Anytime a developer needs to allocate memory, he or she should use the Core/Malloc routines, thereby receiving for free, the ability to track memory usage. The important thing to note is that the developer need only do one thing to make this happen: Use the SCIRun macro "scinew" anywhere he or she would normally use "new". Because "scinew" is a macro, it can be easily configured to allow the default "new" to be used when desired.

Environment Variables

The following environment variables can be set in order to help monitor and control the allocation of memory. The environment variables are only used if DISABLE_SCI_MALLOC is NOT defined during compilation. If DISABLE_SCI_MALLOC is defined, then SCIRun will use the built in new, free, alloc, etc.

MALLOC_STRICT: Places markers in unused memory and uses them to verify memory integrity. Unless you wish to check memory integrity explicitly using the "audit()" function, MALLOC_LAZY should NOT be set.

MALLOC_LAZY: By default, memory is audited for problems on each allocation and deallocation. If MALLOC_LAZY is set, then auditing is turned off. This can speed up code that allocates and deallocates memory frequently.

MALLOC_TRACE [filename]: If MALLOC_TRACE is set, then every memory allocation, reallocation, and deallocation will be logged. If "filename" is not provided, this information will be printed to "stderr".

MALLOC_STATS [filename]: If MALLOC_STATS is set, when SCIRun exits, it will output a list of statistics regarding memory usage during the run. This includes the number of alloc/free calls, the amount of fragmentation, the amount of memory that is free and that is in use, etc. If "filename" is not provided, then "stderr" is used.

Overloaded Functions

The following memory management functions are overloaded to allow Core/Malloc to provide memory management functionality:

C Functions:

        void* malloc(size_t size);
	void free(void* ptr);
	void* calloc(size_t n, size_t s);
	void* realloc(void* p, size_t s);
	void* memalign(size_t alignment, size_t size);
	void* valloc(size_t size);
    

C++ Functions:

	void* operator new(size_t, SCIRun::Allocator*, char*);
	void* operator new[](size_t, SCIRun::Allocator*, char*);
	#define scinew new(SCIRun::default_allocator, __FILE__)
    

To take advantage of the SCIRun memory management utilities provided by Core/Malloc, you should do the following:

  1. Make sure that DISABLE_SCI_MALLOC is not set.

  2. Allocate memory using the following syntax (This can be transparently done by changing every "new" to "scinew"):

          int * int_array = scinew int[ 128 ];
    
          Object * obj = scinew Object();
           
  3. Delete objects as normal:

          delete int_array[];
          delete obj;
          

Chapter 9. Persistent Data

This chapter explains how SCIRun reads and writes data to and from streams.

Introduction

To maximize their usefulness, all software systems must be able to store data to disk at the end of execution, and retrieve that data later for additional processing. As the SW system becomes larger and more complex, especially if numerous different groups are contributing to it, there needs to be a consistent, powerful, and straight forward strategy for performing this data storage and retrieval. Core/Persistent encapsulates the SCIRun data storage and retrieval philosophy which attempts to provide such a solution to this problem.

Core/Persistent provides a direct and uniform method for saving to disk the complex data structures used by SCIRun. Through its use, a user can save and later retrieve data that is distributed across a large number of classes and sub-classes, and need not worry about manually handling dynamically allocated (and possibly cross referenced) memory.

Programming with SCIRun Persistent data

Because most of the code added to SCIRun will use a large number of complex SCIRun data structures, it is necessary for the developer to be able to manage storing data in a consistent manner. Core/Persistent provides this ability by specifying a set of routines that are implemented by each of the SCIRun data structures. It also allows the user to implement these routines in his or her code and thus have a complete and consistent method of storing and retrieving data.

Many module implementors will need to include routines in their module for saving to and retrieve data from disk. Software engineers who develop parts of the SCIRun core will also many times need to add routines to provide the IO for these Core codes. By using Core/Persistent, programmers will be able to easily define routines to save the data specifically created by their codes as well as save the data stored in any of the SCIRun data structures that they use.

Specifically, programmers will, for the most part, use the Core/Persistent paradigm in "Datatypes" files. Programmers are also encouraged to use Core/Persistent for their own data structures for two reasons: 1) If their data structure is every used by other people (migrated to the "Datatypes" directory) then it will need to use Persistent. 2) Using Core/Persistent encourages the implementor to consider and provide for the ability to handle multiple "versions" of data files, transparently to the rest of the code.

Persistent Data

Persistent Data is a general term for any data that will at some point need to be written to disk, and then, later, read back in for further processing.

Persistent Object

A Persistent Object is a specific instance of "Persistent Data" and include all SCIRun datatypes that have been subclassed from the Persistent class. (Classes found in Core/Datatypes usually inherit from Class Datatype, which in turn inherits from Persistent.) Persistent Objects have the ability to save and restore themselves from disk. The data is stored on disk by specifying the "type" of object that created the data and version of that object.

PersistentTypeID

Each "Persistent Object" has an unique "type id". This type id allows SCIRun to load the object back from disk by first, creating a "new" object of this type, and then telling the object to load its data. "PersistentTypeID"s consist of a string representing the type (similar to the C++ character string that specifies the type), the parent class of the object, and the "maker" function that will be used to create new objects of this type.

For templated types, the "type_name()" function is implemented. This function turns, for example, the type vector<int>, into the string, "vector<int>".

Maker Functions

Maker functions are used to created a new Persistent Object. They are very straight forward consisting of code that allocates an object and then returns a pointer to the object.

Pio Streams

Pio Streams are used by Persistent Objects to read/write data from/to disk. There are several types of Pio Streams, all of which inherit from the base class Piostream. The subclasses of Piostream are: BinaryPiostream, TextPiostream, and GzipPiostream. These correspond to binary output, text output, and zipped output. (The type of output is usually set by the user at the time the data is to be written out.)

Pio() versus io()

Every persistent object has an a member function (and thus needs to implement) the io() function. This function (an example is given below) implements the saving and loading of the data unique to that object. The Pio() functions are not member functions. They are defined for all data types and simply are used to start the saving of the data (see below.)

Examples

In order to make a "Datatype" (or any data structure for that matter) persistent, you must follow these steps:

  1. Inherit from class Persistent (or from class Datatype.)

  2. Add the following to your class:

     	 void    io(Piostream &stream);
    	 static  PersistentTypeID type_id;
    
             static const string type_name(int n = -1);
             virtual const string get_type_name(int n = -1) const;
    
           private:
             static Persistent *maker();
          

To save a persistent object, you simply create a persistent stream:

      Piostream * stream = auto_istream( file_name );
    

Then you call the Pio function for the object, with the stream:

      Pio( *stream, object );
    

The Pio() function will use the io() function provided by (or, in the case of built in types, provided for) the type.

All Persistent objects need to define their current version:


      // Pio defs.
      const int MY_CLASS_VERSION = 1;      
    

The version information can be used to successfully read in old data files. Various "new" object data members can be defaulted to a reasonable value for these old data files.

All Persistent objects must implement the io() function. This is the function that will be called by Pio() to save/load the object. This is an example io() function:

      void 
      MyClass::io( Piostream & stream )
      {
        // All input/ouptut starts with this line:
#IF THERE IS MORE THAN ONE VERSION
        int version = stream.begin_class("ColorMap", MY_CLASS_VERSION);
#ELSE IF YOU ONLY HAVE ONE VERSION CURRENTLY
        stream.begin_class( type_name().c_str(), MY_CLASS_VERSION );

	// Save/Load Base Class fields first:
	MyBaseClass::io( stream );

	if( version > 1 ) {
	  Pio( stream, new_data_field_ );
        }

	// Save/Load individual fields:
	Pio( stream, my_data_ );
	Pio( stream, more_data_ );

	// All input/output ends with this line:
	stream.end_class();
      }
      

Chapter 10. Custom Tk Widgets for SCIRun

This chapter describes extensions made to Tk by SCIRun to provide for custom widgets.

Introduction

The main purpose of this directory is provide additional GUI component that are needed by SCIRun but are not provided by Tk nor are easily implemented (if at all) in native TclTk. These extensions include support for an OpenGL window, three dimension look and feel and moving the cursor to a specified window. These extensions are implemented here in C++ and are added to the Tk built in commands in the initialization stage.

There are three components to this interface: Initialization, extensions and a hack of the Tk internals. GUI extensions to Tk should be implemented here and added to the Tk built in commands via the initialization in tkAppInit.c

A special attention should be given to the tclUnixNotify-*.c files. These files are a hack of a similar file internal to Tk. Their purpose is to enable Tk to work in a multithreaded environment even though it was not designed to do so. Since the corresponding file in the Tk distribution changes between different versions of Tk we have included a modified version for the two versions of Tk that are supported by SCIRun, namely 8.0.4 and 8.3 .

The code in this directory is meant to be used only via the tcl commands that it provides. Any tcl code can call these commands to create the GUI they provide.

tk command

Tk provides a set of builtin GUI commands. One can augment them by providing additional functions written in C++ (or C). These additional functions can interface the TCL/TK system via C function calls as describe in the TCL/TK documentation.

tkOpenGL

A Tk window that embodies an OpenGL window.

Chapter 11. SCIRun Utilities

This chapter describes some simple utilities available in SCIRun.

Introduction

Core/Util is a miscellaneous collection of code. The code can be classified into 3 groups: debugging tools, timing routines, and SCIRun internal code.

The debugging tools and timing routines are generally useful. The SCIRun internal code is not.

The SCIRun internal code consists of routines for accessing shared libraries and work arounds of bugs on Linux platforms.

Programming with SCIRun Utilities

Use the the debugging and timing tools when developing any kind of code.

Use the debugging tools to specify data invariants, catch programming errors, and log messages during program execution.

The timer tools can be used to analyze code bottlenecks and perform other timing tasks.

Assertion

Assertions have 2 (related) uses. They are used to catch programming errors and they may used to make promises between a routine and its caller, i.e. "If you the caller send me data that does not violate my assertion(s) then I, the routine, promise to do the right thing by you."

Assertions express a set of valid states a data object (or objects) may possess at some point in the code. If a piece of data is not in a valid state, i.e. it violates the assertion, then the assertion will catch this violation, report the violation, and perhaps terminate the program.

SCIRun supports a number of assertion styles which can be found in Assert.h and FancyAssert.h. Here are a few examples:

Ensure that the variable 'n' is greater than 0:

      ASSERT(n < 0);
    

Ensure that 'a' is equal to 999:

      ASSERTEQ(a, 999);
    

Other examples:

      ASSERTNE(a, 0);
      ASSERTRANGE(a, 0.0, 1.0);
    

Timer

A Timer keeps track of time. Class Timer is an abstract base class - you can't create a Timer object. But you can create objects of the derived types CPUTimer, WallClockTimer, and TimeThrottle. These all provide the functions start(), stop(), clear(), time(), and add(). A TimeThrottle also provides the function wait_for_time(). Functions common to all timers are:

start() starts or resumes a timer.

stop() stops a timer. It does not clear accumulated time. The timer may be resumed with start().

clear() sets the timer's accumulated time to 0. clear() may be executed while a timer is running (although a warning will be written to std error).

time() returns the timer's accumulated time. It may be executed while the timer is running or stopped.

add(double t) adds t seconds to the current elapsed time.

Individual timer types are discussed next.

CPUTimer

CPUTimer records the elasped CPU time (seconds) used by the calling process (including system CPU time used on behalf of the calling process).

Sample use of a CPUTimer:

      CPUTimer cpuTimer;
      cpuTimer.start();
      for (i =0; i<3; ++i) {
        .
        .
        .
      }
      cpuTimer.stop();
      cout << "Loop used " << cpuTimer.time()
           << " seconds of cpu" << endl;
    

WallClockTimer

WallClockTimer records elasped real time (seconds).

Sample use of a WallClockTimer:

      WallClockTimer wcTimer;
      wcTimer.start();
      for (i =0; i<3; ++i) {
        .
        .
        .
      }
      wcTimer.stop();
      cout << "Loop used " << wcTimer.time()
           << " seconds of real time" << endl;
    

TimeThrottle

TimeThrottle is a WallClockTimer with the added function wait_for_time().

wait_for_time(double endtime) will suspend the calling thread until the given endtime (seconds) has elapsed. This function is only implemented on SGI systems.

Sample use of a TimeThrottle:

      TimeThrottle timeThrottle;
      timeThrottle.start();
      double startTime = timeThrottle.time();
      // Start doing stuff.
      .
      .
      .
      // Make sure that not less than 1 second has elapsed since we started
      // doing stuff.
      timeThrottle.wait_for_time(startTime + 1);
    

Chapter 12. SCIRun Exceptions

This chapter explains SCIRun exceptions.

Introduction

The Exceptions directory contains common exceptions and the Exception base class. Developers can create new exception classes, and these will need to be in other directories if they depend on other data structures outside of the Exception dir.

All exceptions in SCIRun are derived from the SCIRun::Exception base class. All derived classes implement a message method and a type method. The type method just returns a string (const char* actually) that indicates the classname of the exception. The message returns a human readable string that can be printed out when the exception is caught.

This directory also contains some general exceptions used throughout SCIRun.

  • AssertionFailed - thrown when an assertion fails.

  • ArrayIndexOutOfBounds - thrown to indicate a failed bounds check.

  • DimensionMismatch - thrown to indicate dimensionality differences.

  • FileNotFound - thrown when a file is not found.

  • InternalError - thrown when an internal error occurs.

Programming with SCIRun Exceptions

A developer is likely to use this code from within written modules as well as within new datatypes.

A couple development specific hints follow.

All exception classes must implement a copy constructor (for some compilers).

On the SGI, the exception classes will give you a stacktrace when they are caught (uses -lexc).

Exceptions in SCIRun, are only to be used for exceptional cases. General error handling should use another mechanism. Exceptions are expensive, and as such should only be used in cases that warrant it. One of the most common uses in this directory are ASSERT, and ASSERTFAIL.

ASSERT

This is used to verify that some condition is true before continuing. if the expression geven evaluates to false, the exception is thrown, which will eventually abort scirun.

This macro expands to nothing in an optimized build.

       myClass *p = get_my_class();
       ASSERT(p != 0);
       p->do_something();
    

ASSERTFAIL

This is used to abort with a nice message when something is wrong.

This macro expands to nothing in an optimized build.

       switch(var) {
         case NICE:
         ...
         break;
        case EVEN_NICER:
         ...
         break;
        default:
         ASSERTFAIL("Unknown case, what happened?");
       }
    

Chapter 13. 3D Widgets and Constraints in SCIRun

This chapter introduces the concepts of 3D widgets and the Constraints used to make them functional.

Introduction

SCIRun has a set of 3D interaction widgets. These widgets have a graphical representation within the scene graph of the Viewer module, and can be directly manipulated within a visualization. Furthermore, the module that creates a widget maintains access to and control of the widget, and is alerted through a callback when a widget is manipulated within a scence. Through this callback mechanism, the module can respond appropriately as the widget interaction is taking place - thereby allowing compuational feedback to be directly coupled with the visual feedback.

Widgets are useful in any context where direct manipulation has clear advantages over indirect manipulation. For example, when specifying a cutting plane for a scene, it is possible to specify that plane by entering the A, B, C, and D of a plane equation (Ax + By + Cz + D = 0) with sliders or text-entry boxes; however, it is often much more natural to place a graphical representation of that plane (for example, a rectangular frame widget) into the scene and provide the user handles to translate and rotate the clipping plane directly. By manipulating the object directly, a greater sense of immersion is obtained, and the user has a more natural mechanism for setting parameters.

One important aspect of 3D widgets in SCIRun is the notion of Constraints. Constraints are used to ensure that the user will not introduce degenerate situations when providing input to SCIRun. For example, with the DistanceConstraint, two points are constrained to be a fixed distance apart. If one point moves to a new location, the other point must be moved in order to maintain that constraint. Some of the simpler constraints are solved using direct methods, while more complex constraints are solved using interative methods.

While the primary use for constraints within SCIRun is to provide robust 3D widgets, it doesn't preclude their use in other situations where constraints may be needed, such as in modeling particle interactions in a finite element simulation.

Programming 3D Widgets

Widgets should be constructed within the module that will be using them, and should be sent to a Viewer module through a Geom outport. The developer should implement the virtual get_pick() method for their module if they want catch the callbacks when the widget is manipulated.