Alpha_1 Programmers's Manual


C_Shape_Edit Developer's Guide

This document details procedures for extending and modifying c_shape_edit functionality, also know as libModel. See the C_shape_edit Programmer's Overview for a brief description of c_shape_edit and libModel. See the C_shape_edit Programmer's Guide for a description of how to write applications using libModel.

Table of Contents

Model Objects

Model objects (class model_obj) are nodes in a dependency graph. Model objects are constructed from a symbolic function name (e.g.: "lineThru2Pts") and a list of arguments. The arguments are the prerequisites of the model object and the model object is a dependent of each argument that is a model object itself (arguments are not necessarily model objects). Dependency graphs are acyclic, directed graphs.

The data members of a model_obj are:

param (object_type *)
This is the (cached) result of applying the constructor function to the supplied list of arguments.
prereqs (prereq_obj *)
The prerequisites (the argument list).
depends (model_list_obj *)
The dependents. A list of model_obj's that depend on this.
consfn (cons_fp_type)
The function (pointer) used to construct param.
consfn_name (string_type)
Symbolic name of consfn.
model_id (id_type)
Unique ID used for identification via the remote procedure call interface (mi_type).
mark (boolean_type)
A flag used in graph traversal algorithms.
out_of_date (boolean_type)
A flag to indicate that param is out of date with respect to consfn or prereqs.
ancestor_count (int)
Counter used in dependency propagation.
name (string_type)
Symbolic name for the object.

Model Object Construction

Construction of model_obj's is a bit different than usual C++ objects. There are really two constructions: 1) the model_obj itself, and 2) the secondary param member object. The parametric object is a generic object (object_type *) of unknown type. This member is created by applying a function pointer to a list of arguments and only indirectly constructed in the C++ object sense.

Wrapper Functions

There is a function pointer typedef which is used for model_obj constructors called cons_fp_type (constructor function pointer). It's definition is:
	typedef object_type *(*cons_fp_type)( prereq_obj * )
A cons_fn_type is a pointer to a function that takes a list of prereq_obj's (an prereq_obj is a list_obj) and returns a pointer to an object_type. These functions are called wrapper functions since they wrap C++ constructors, member functions, or regular external C++ functions. This indirect construction of the param member is used both when a model_obj is constructed and when dependency propagation updates an existing model_obj.

Since we are basically subverting the inherent type checking of C++ by using a generic argument list, we must provide our own type checking and conversion before the actual C++ function can be called. This functionality is provided by a set of accessors for the prereq_obj class. Error handling for type mismatches is implemented by raising exceptions and providing handlers. This is aided by the fact that wrapper functions are only called in a limited (and small) number of places where exception handlers can be installed.

If all the types are correct, the underlying functions can still have data dependent failures. In some cases, these will also raise exceptions. In other cases a NULL pointer is returned. The exception handling code in the private model_obj methods is designed to catch these cases as well.

A Note On Exceptions

In the model_obj class, all public methods assume that no exception handler is in place and therefore should not raise exceptions. Only private methods (and a limited set) raise exceptions.

Fixed Model Object

A model_obj can also be constructed from an existing object (used for the param member). In this case there are no prerequisites or consfn. There can be subsequent dependents.

C++ Constructors

The C++ constructor for a model_obj that uses a wrapper function is private because it may raise exceptions (see above). It's declaration is:
  private:
    model_obj( const string_type cons_fn, prereq_obj *args );
Note that the consfn is specified symbolically and looked up in a symbol table.

Due to the policy of public methods not raising exceptions, the public method that corresponds to this must be a static method. Here are the public constructors:

  public:
    static model_obj   	*new_model( const string_type cons_fn, 
				    prereq_obj *args );
    static model_obj   	*new_model( object_type *param );

    model_obj( object_type *param );
    model_obj();
    model_obj( const model_obj *_obj );
The model_obj( object_type *param ) constructor is public because it will not raise an exception. The second static method just adds a layer on this constructor for consistency. The last two constructors are the default and the copying constructors required for general Alpha_1 objects.

Dependency Links

Once a valid param object has been created by a wrapper function call, the model_obj being constructed is linked into the existing dependency graph. Links are created to all of the arguments (prereq_obj's) that represent other model_obj's. More specifically, for each model_obj in the prereq_obj list, the newly constructed model_obj becomes a dependent. The prereq_obj list given to the model_obj constructor becomes the prereqs list for the constructed model_obj. The dependency links are thus bidirectional although each direction is implemented via a different data type (prereq_obj's for prerequisites and a model_list_obj for dependents).

For model_obj's constructed from an existing param object, there are no prerequisites and therefore it cannot be a dependent of any other model_obj. However, a model_obj constructed this way can subsequently become a prerequisite for another model_obj.

Dependency Propagation

Dependency propagation is a set of methods for generating a consistent set of cached param objects starting from a set of known changed model_obj's and propagating those changes through the graph of dependent objects. The top-level propagation function is:
  private:
    static void	propagate_changes_from( model_list_obj *objs );
Propagation can fail due to type conflicts and data dependent errors (same as constructors) so this is a private member function. Propagation is aborted when an error is detected.

Model Object Updates

Dependency propagation is started by updating a model_obj (model editing). An existing model_obj can be updated in two ways, similar to the two main construction methods: 1) a new wrapper function or a new argument list can be provided (or both) or 2) an existing param object can be provided. In either case, the existing state of the model_obj (whether is has a wrapper function and arguments, or only a param object) does not matter. The public update methods are:
  public:
    boolean_type update_model( object_type *param );
    boolean_type update_model( const string_type cons_fn, 
			       prereq_obj *args );
These report an error with a boolean_type return value and do not raise exceptions. The private methods raise exceptions:
  private:
    void	update( object_type *param );
    void	update( const string_type cons_fn, 
   			prereq_obj *args );

An interpreter is provided as part of the model_obj package using static member functions and data in the model_obj class. The language that is interpreted is called SCL or Shape_edit Command Language. By convention, the file-name extension .scl is used.

A token scanner and a parser (written in YACC) convert text input into a parse-tree representation using sym_exp_obj's. This parse-tree representation is similar to a LISP S-expression, or functional specification. Function and object names use the arg_type A_SYM tag and are looked up in a symbol table during evaluation.

Functions are divided into three categories:

Model constructors
Functions that construct or update model_obj's.
Side-effect functions
Functions that produce arbitrary side-effects. May or maynot return a value.
Built-in functions
Functions built into (or implied by) SCL (e.g.: equality comparisons, logical operators, assignment statements).

The model_server_type class is used to provide the server-side network interface for remote procedure call clients. The MILib defines an instance of a model_server_type:
extern model_server_type ms;
When ms is initialized with the set_up_server method, it is stored in a static variable of the model_obj class as the current server. The presence of this pointer signifies to the model_obj class that it is to provide model server functionality and handling of remote clients via the given model_server_type.

Adding New Model Constructors

Wrapper functions are used to add functionality to the command language and add to the available set of model constructors that can be used for dependency propagation. See the Wrapper Functions section for a more detailed definition.

Writing Wrapper Functions

The main purpose of a wrapper function is to provide a type conversion from a generic function call specification to a specific function call. A cons_fp_type is a pointer to a wrapper function. It's definition is:
	typedef object_type *(*cons_fp_type)( prereq_obj * )
A symbol table maps symbolic names (character strings) to cons_fp_type's. When adding new wrappers, the programmer writes the function and specifies the symbolic name for the symbol table.

Type Conversion

The job of a wrapper is to unpack the generic argument list into correctly-typed variables and call the type-specific underlying function. (See the C_shape_edit Programmer's Overview for brief descriptions of the various argument list classes.) The type conversion is provided by accessor methods of the prereq_obj class and value_obj class (base class for prereq_obj). The idea behind these accessors is to make it as simple as possible to write wrappers functions. In particular, error handling is hidden from the programmer by raising exceptions instead of using error return values. This means the wrapper code can assume the prereq_obj accessors always return valid results. This leads to simpler code.

Example

Here is an example for the function ptInterp. Note that the function name of a wrapper is irrelevant and, in fact, wrappers are static functions. The symbol table provides the mapping from a name to the actual function pointer.
static object_type *
wrap_pt_interp( prereq_obj *args )
{
    pt_obj     *pt1 = (pt_obj *)args->arg_param( 0, T_PT_OBJ ),
               *pt2 = (pt_obj *)args->arg_param( 1, T_PT_OBJ );
    real_type  t = args->arg_num( 2 );

    return new pt_obj( pt1->interpolate( *pt2, t ));
}
The underlying function is a method of pt_obj called interpolate and requires a pt_obj and a real_type for arguments. The SCL specification for ptInterp is:
    ptInterp( Point1, Point2, Param )
The wrapper is written assuming that the arguments are correct. It's only job is to extract them and call the underlying function. The above code will handle type checking errors and missing arguments. The accessor methods are guaranteed to return valid results. If they can't, they don't return at all (they raise exceptions).

In this particular example, the method

    object_type *arg_param( const int, tag_type )
is used. This returns an object_type pointer that has been checked against the given type tag and extracted from the argument list at the given position (i.e.: for pt1 extract the first argument which should be a pt_obj). The only thing left for the wrapper to do is cast the pointer to the appropriate type. The
    real_type arg_num( const int )
method returns a real_type extracted from the given argument. The argument may be an integer or floating point constant, or a named model_obj with a numeric value.

The above example will handle omissions, ordering errors, and general type checking in the argument list. It will not detect extra arguments. The wrapper will work fine with extra arguments if the first three arguments are indeed correct. Detecting errors for extra arguments is a desirable convenience for users that can be added as follows:

    args->num_args( 3, "ptInterp" );
This methods checks the overall number of arguments and only returns if it is correct. If not, an exception is raised. The provided "name" is used in an error message. If a wrapper provides this check, it is generally the first thing done in the wrapper.

Available Accessors

The prereq_obj class is derived from the value_obj class. The set of accessor methods is defined in the value_obj base class.

Here are the available accessors for (value_obj) argument lists. All of these have an optional last argument for a description or name of argument. If provided, it is used in any error messages that are generated with respect to the given argument. The first argument is the position to access in the argument list (0-based).

    int			arg_int( const int, string_type descr = NULL )	const;
    real_type		arg_fl( const int, string_type descr = NULL )	const;
    string_type		arg_str( const int, string_type descr = NULL )	const;
    boolean_type	arg_bool( const int, string_type descr = NULL )	const;
    real_type		arg_num( const int, string_type descr = NULL )	const;
    real_type		arg_nonnegative_num( const int index,
					     string_type descr = NULL ) const; 
    real_type		arg_positive_num( const int index,
					  string_type descr = NULL )	const; 
    object_type		*arg_param( const int index, tag_type tag,
				    string_type descr = NULL )		const;
    model_obj	        *arg_model( const int, 
				    string_type descr = NULL )		const;
    value_obj	        *arg_lst( const int,
				  string_type descr = NULL )		const;
The meaning of most of these is obvious from the name and return type. The arg_param method is for an argument that is an object with the given type tag (see above example). The arg_lst method is for an argument that is a SCL array (represented as a value_obj list). Note that the arg_param method can be called with T_NO_TAG to match any object_type.

For checking the overall number of arguments use:

    void 	  num_args( int right_num, string_type cons_name );
    void 	  num_args( int min_num,
		  	    int max_num,
		  	    string_type cons_name );
The second case is for wrappers with optional arguments. Note that a firm policy on option arguments has not been established yet.

The next set of accessors are used less frequently, or internally by the other accessors:

  tag_type	  arg_tag_type( const int index ) const;
  const value_obj *arg_index( int ) const;
  value_obj       *arg_index( int );
Finally, there are two methods for directly raising exceptions for error handling. These should be used only in very rare cases. If you think you need to use them, you should consider writing a new arg_obj accessor that does what you need. If that is not possible, then these may be used:
  void arg_type_exception( const int index, 
			   string_type object_names ) const;
  void arg_exception( const int index,
		      string_type exception_string) const;

Return Types

The return type of a wrapper is object_type *. Any C++ constructor or external function which returns an object_type * or pointer to any class derived from object_type can be returned from a wrapper. For constructor wrappers, this returned object becomes the "param" slot of a model_obj and is "owned" by the model_obj. Also note that objects extracted from the argument list via the arg_paramaccessor are "owned" by their model_obj's and must be copied if they are to be modified or become part of the return value of a wrapper.

There are some special cases for model_obj "param" values. To return a basic data type (integer, float, or string) the wrapper should construct a prereq_obj (derived class of value_obj) for the return value. To return a list (SCL array), the wrappers should return an array_val_obj which contains a list of value_obj's. The value_obj's in the list can be prereq_obj's (for basic data types plus shared model_obj references) or ptr_val_obj's for generic objects.

Here are some examples:

Basic data types
The cosine constructor (SCL: ct = cos( theta );) returns a prereq_obj:
object_type *
wrap_cos( prereq_obj *args )
{
    return new prereq_obj( cos( args->arg_num( 0 ) ) );
}
Return a list (SCL array)
The basic array constructor (SCL: a = {x, y, 1};) returns an array_val_obj:
object_type *
wrap_array( prereq_obj *args )
{
    return new array_val_obj( args->cp_obj_list() );
}
Note that prereq_obj's that refer to model_objs are always shared references. In other words, cp_obj applied to such a prereq_obj produces a new prereq_obj that points to the same model_obj. This means the array constructor produces a list of shared model_obj references.

Another option is to return a list of objects constructed internally by the wrapper itself. For example you might want wrapper that took an argument list of four numbers and returned a list of two points. In this case we use a ptr_val_obj to wrap each point and then an array_val_obj to hold the list of value_obj's.

object_type *
wrap_pt_list_from_4_nums( prereq_obj *args )
{
    pt_obj *p1 = new pt_obj( E2, args->arg_num( 0 ), args->arg_num( 1 ) ),
	   *p2 = new pt_obj( E2, args->arg_num( 2 ), args->arg_num( 3 ) );

    ptr_val_obj *v1 = new ptr_val_obj( p1, FALSE );
    v1->append( new ptr_val_obj( p2, FALSE ) );
    return new array_val_obj( v1 );
}
The ptr_val_obj class can "own" its object or share it. The default is to share it which is intended for use in side-effect functions (see below). In this case (a constructor wrapper) we want to own the objects so they are subsequently owned by the model_obj.

Note that for a wrapper that accepts a list (SCL array), there is no need to differentiate between the two types of lists above (model_obj references or "raw" object references). The arg_param accessors handle the differences. So if a constructor required a list of points, it would work for either of the following in SCL:

PtList1 = { pt(0,0), pt(1,1) };
PtList2 = ptListFrom4Nums( 0, 0, 1, 1 );
Side-effect functions may treat the two differently, however.

Programming Procedures

Wrapper functions are loosely organized into packages, one package per file. The existing files can by found by ls $a$molib/*wrap.C.

Wrapper functions are written as static functions. By convention, the names are taken from the RLISP functions they are replacing by appending wrap_ and using _'s instead of capitalization.

The symbol name for the SCL interpreter is grouped together with the wrapper function pointer in a class called fn_info_type. Finally, a static array of fn_info_type's plus a package name are used to construct a model_pkg_type object. An external model_pkg_type object is defined in each wrapper file. These objects are used to initialize the symbol table at run-time and provide the mapping from symbol name to function pointer.

To add a new wrapper to an existing package:

To define a new package, create a new file similar to an existing package. The file should define a model_pkg_type object which should be declared in $a$aimo/model_pkg.h. Add functions to the new package as above. Finally, the c_shape_edit main program must be modified to load the new package by adding a line to a static array in $a$modeld/c_shape_edit.C.
Compiling and Testing
Compiling and testing consists of modifying, or adding files to $a$molib, compiling the changes, and linking a new c_shape_edit ($a$modeld) with your changes.

Test with motif3d or use "dumpA1File," or "dbgObj" from the command language.

RLISP to C++ Conversion

SCL functions are meant to replace their RLISP shape_edit counterparts. Therefore, the RLISP shape_edit command interface is the specification, with exceptions for differing syntax between SCL and RLISP. In most cases, the SCL interface (number, type, and ordering of arguments, and the function name itself) should be identical to the RLISP version. If there is a good reason, the function interface can be changed, but this is discouraged for reasons of backward compatibility. One notable exception is quoted identifiers (see next section).

When adding a wrapper function an assessment has to be made about the underlying C++ code. There are a number of possibilities:

RLISP Identifier Arguments
Many RLISP constructors use quoted identifiers as arguments. By choice, SCL does not have this facility. SCL functions should use strings instead. Usually, the underlying C++ code requires an enumeration value. If this is the case, a helper function should be written to convert strings to a given enumeration type. This function should raise an exception if the string does not match any of the enumeration values. See $a$molib/str_to_enum.C for some examples.

Writing Side-effect Functions

The procedures for writing and adding side-effect functions are essentially identical to those for wrapper functions. Side-effect functions are looked up and called in the interpreter in the same way as wrapper functions. The typedef for a pointer to a side-effect function is called a side_fp_type:
typedef value_obj     *(*side_fp_type)( value_obj * );
The difference between wrapper functions (constructors) and side-effect functions is the return type and argument list type are value_obj which is the base class "value" type. Thus, side-effect functions are more general.

The interpreter will handle side-effect functions that return NULL (true side-effect functions), and will simply print the results if not null. Wrapper functions, on the other hand, raise an exception if NULL is returned. Side-effect functions cannot be used on the right-hand-side of an assignment statement, but can be nested.

As a rule, if a function is ever meant to be used as a model_obj constructor (i.e.: used with assignment statements and dependency propagation) then it should be a wrapper function. Wrapper functions can be used as side-effect functions in that they can be called as a user query.

For example ptX is a wrapper function. It can be used to create named model_obj's (with numeric values) and dependency propagation will flow through these objects:

p = pt( 1, 2, 3 );
px = ptX( p );
p2 = pt( 1 + px, 0, 0 );
However ptX can also be called as a query without creating any new model_obj's in the dependency graph:
# What is p2's current X value?
ptX( p2 );
In this case the interpreter simply prints the result of the wrapper function and immediately deletes it.

Return Types

The return type of a side-effect is value_obj *. This is a base class for prereq_obj and ptr_val_obj. Between these two classes you can return int, real_type, string, model_obj *, object_type *, and array_val_obj for a list of all of the above.

The ptr_val_obj class can be used to return shared object references for nested side-effect functions. Here are two examples to illustrate:

static value_obj *
dbg_obj( value_obj *args )
{
    args->get_param( T_NO_TAG )->dbg_obj();
    return NULL;
}

value_obj *
wrap_shell_srf( value_obj *args )
{
    args->num_args( 2, "shellSrf" );
    
    shell_obj *shl = (shell_obj *)args->arg_param( 0, T_SHELL_OBJ, "Shell" );
    int       index = args->arg_int( 1, "Shell surface index" );
    
    /* Return shared srf_obj pointer. */
    return new ptr_val_obj( shl->srfs()->nth_element( index ) ); 
}
With these two side-effect functions we can do this in SCL:
Shell = ...;
dbgObj( Shell );
dbgObj( shellSrf( Shell, 0 ) );

Alpha_1 Programmer's Manual Home Page
Alpha_1 Programmer's Manual. Version 95.06.
Copyright © 1995, University of Utah
alpha1@gr.utah.edu