Getting Started Documentation Glish Learn More Programming Contact Us
Version 1.9 Build 1556
News FAQ
Search Home


next up previous contents home.gif
Next: Binding Distributed Objects to Glish Up: How to Write an AIPS++ Application Previous: Documenting Your Application

Subsections



Creating Distributed Objects

Although substantial applications can be written entirely in Glish, in many cases one has to provide functionality via C++ code. The binding of C++ to Glish is done using distributed objects. This section takes you through a trivial example of a distributed object.

First we have to understand the AIPS++ ``tasking'' model. The basic model is as follows:

1.
Computation occurs in C++ objects defined by the application programmer.
2.
A process (running executable) can contain many objects from one or more classes. The system takes care of creating the objects and invoking their functions when the user requests it.
3.
For each C++ class, a glish ``proxy'' that manipulates it is created by the application programmer. The system takes care of the link between this proxy and the actual C++ object.

The overhead for the application programmer is approximately 10 lines of C++ and 10 lines of glish per method (more if there are many arguments). It might plausibly however require less total lines of code than previously since, e.g., the programmer has less error checking and parameter conversion to do, does not have to write an event loop, and so on. Perhaps more importantly, because these things are done by the system, the user will see a more uniform interface, and improvements to that interface can occur in a centralized fashion.

The execution overhead is such that you shouldn't expect more than about 150 DO no-op method invocations per second on a Sparc-20 sized machine (less if the methods are transferring a lot of data or for methods which have parameter logging turned on). For weighty or infrequently called computations this should be sufficiently fast, otherwise you may have to batch or cache to improve the performance.

For our example, suppose we want to provide a ``squarer'' class that merely squares an integer. Obviouly you would never create a distributed object to do this, but it makes a small example that can be entirely contained in this paper. It is also checked into the system as trial/Tasking/test/dsquarer.cc and trial/Tasking/test/dsquarer.g.

Implement your computation

Once you have decided what your interface is to be, implement the computational portions of it as you would any other class. If the class is merely relaying functionality that already exists in the library (e.g., FFT's) the class should be checked into the applications own directory, otherwise it should be checked into a general library directory so other C++ users can access it directly. All interesting functionality programmed into AIPS++ should be available in the libraries, it must not be ``locked up'' entirely in an application.

Unlike the rest of the AIPS++ library, DO class names will be entirely lowercase. Because of this, if you check the class into the general system, do so in the ``distributed'' directory to avoid having different case conventions in general library modules.

In our case, the class has no intersting functionality, and we would just check it into the application directory that contains the server that we will use. If this was a real example, we would probably use the ``numerics'' server.

Here is the ``computational'' class before becoming a distributed object (DO):

#include <aips/aips.h>

class squarer
{
public:
    squarer() {}
    squarer(const squarer &other) {}
    squarer &operator=(const squarer &other) {return *this;}
    ~squarer() {}

    Int square(Int  x ) { return  x * x  };
};

Some comments on the above:

You should test and debug your class at this point using test programs etc., until you are convinced that the computational part of your class is correct. If the class is to be checked into the general library the test programs should also be checked in with the class.

Create an ApplicationObject

All that is needed to turn your computational class into a distributable one is that you define what parameters your methods need, and you tell the system what method to actually run for a given method name (i.e., a String). The is done by publicly deriving your class from the base class ApplicationObject and filling out three required methods.

Here is the class as a DO:

#include <aips/aips.h>
#include <trial/Tasking.h>                                          // 1

class squarer : public ApplicationObject                            // 2
{
public:
    squarer();
    squarer(const squarer &other);
    squarer &operator=(const squarer &other);
    ~squarer();

    Int square(Int  x );

    virtual String className() const;                               // 3
    virtual Vector<String> methods() const;                         // 4
    virtual MethodResult runMethod(uInt which,                      // 5
                                   ParameterSet &parameters,
                                   Bool runMethod);
};

The changes from before are as follows:

1.
This file defines the class ApplicationObject and other things that will be needed latter.
2.
All DO's must publicly inherit from ApplicationObject.
3.
It is required that this method be provided. It returns the name of the class, squarer here.
4.
This method must also be provided. It returns a Vector with the name of all the methods that you want to be attached to the DO system. That is, you do not need to list ``helper'' methods you implement for your own use, or system methods like methods itself. Here you would only return square.
5.
This is the method that the system will call, and from which you will in turn call the ``real'' routine. The parameters have the following meanings:
which
Which parameter number to run. This is the (0-relative) index of the function name returned by methods().3

parameters
This is the object into which you will get the argument for square(Int x) and set the return value.

runMethod
If False, define the parameters, otherwise get the parameters, run the method, and return the result.

It would probably be useful to see the actual implementation for runMethod():

MethodResult squarer::runMethod(uInt which,
                                ParameterSet &parameters,
                                Bool runMethod)
{
    static String xName = "x";                                  //  1
    static String returnvalName = "returnval";                  //  2
                                                                //  3
    switch (which) {                                            //  4
    case 0:                                                     //  5
    {                                                           //  6
        Parameter<Int> x(parameters,                            //  7
                            xName, ParameterSet::In);           //  8
        Parameter<Int> returnval(parameters, returnvalName,     //  9
                                 ParameterSet::Out);            // 10
        if (runMethod) {                                        // 11
            returnval() = square(x());                          // 12
        }                                                       // 13
        break;                                                  // 14
    default:                                                    // 15
        return error("Unknown method");                         // 16
    }                                                           // 17
    return ok();                                                // 18
}                                                               // 19
1.
A small point, but it will make things slightly faster if strings which have the same values upon each invocation are created static -- this will prevent the same values from being created over and over again.
2.
3.
4.
Even though this class only has one method, I would still set up a switch so that it will be easier to insert more methods later if we need to.
5.
Zero-relative method number, i.e. you would use this to index into the methods() vector.
6.
7.
We have a single integer parameter named x. It is an input only value, i.e. it does not need to be copied back. InOut is another alternative for ``reference'' parameters. Parameter names may not begin with an underscore.
8.
9.
This is the returned value. returnval is a special parameter name, and you may not use it for any other argument. Void functions merely omit having a parameter named returnval. Note that pure output parameters should be set to ParameterSet::Out, whereas any parameters which are both input and output (used and set) should be marked ParameterSet::InOut.
10.
11.
Only run the method if we're told to (otherwise we're just defining the parameters we need).
12.
Run the function. The Parameter function operator (operator()) returns a reference to a value of the template type, <Int> here.
13.
14.
Don't forget the break or you'll end with an error even though all is well with your method.
15.
While the system should never call this function with an invalid which, defensive programming is always a good idea.
16.
error() is a predefined function that takes a string and returns an error to the system. Use it whenever you can detect an error before running the function (which probably won't be very often). Do not bother catching unexpected exceptions and then returning error() -- the system will do this for you.
17.
18.
ok() should be returned whenever runMethod ends normally.

Other ApplicationObject methods

Other methods inherited from ApplicationObject which it might be useful for you to be aware of are shown next:

class ApplicationObject
{
    ...
    virtual Vector<String> noTraceMethods() const;
    const ObjectID &id() const;
    static MethodResult ok();
    static MethodResult error(const String &message);
    ...
};

Some comments:

noTraceMethods()
Normally, methods in a DO have some automatic logging applied when executed (input and output parameter values, execution times). This is the right thing to do for ``weighty'' computations (e.g., a deconvolution), but is inappropriate for small computations that might be called many times, or for functions whose return value says everything there is to be known about the function (e.g., image.shape(). Return the names of functions which you don't want logged. This is the only function in this section which may be over-ridden -- the default is to return no function names, i.e., log all functions.

id()
Return our ObjectID. This function is useful in setting up the LogOrigin for logging, and possibly for retrieving objects from the ObjectController as described in Section 7.6.

ok()
A convenient way to produce a ``normal'' error return: return ok();

error()
A convenient way to indicate a problem occurred in this function: return error("message");

Parameters and Constraints

The goal of the Parameter<T> class is to have the system provide objects of the class the programmer actually wants, and error checking them for the user. You should tell us if there are unsupported types that would make your life easier.

Presently, the supported <T>'s are:

Scalars
Bool, Int, Float, Double, Complex, DComplex, String.
Arrays
of the scalar types.
Vectors
of the scalar types. Vectors are provided explicitly so you don't have to check that an Array is of the correct dimensionality.
Index
Index and Vector<Index> This types is used for values which are zero-relative inside C++, but one-relative to the user. So, you would use a Vector<Index> for a pixel in an image, but a Vector<Int> for its shape. You would use an Index for the row number of a Table, and an Int for the number of rows it contains. Correct use of this class should greatly reduce the number of ``off-by-one'' errors in the system.
Glish values
GlishArray and GlishRecord and GlishValue are available as parameter types. You would use the former for methods that, e.g., take the FFT for a wide variety of pixel types, and you would use the latter where you want an inhomogeneous container (``header''). For example, the signature of a function that computes statistics on an array of any type and returns those statistics in a record might be:
        // return record like [mean=0, variance=1.0] for any type
	GlishRecord class::statistics(const GlishArray &a) const;
Data classes
PagedImage<Float> and Table.
Quantum
If you need a quantity with a unit, you should use this type rather than just a double and assuming the units. Only Quantum<Double> is available so far. For example, use this if you need a duration, or a shift of an image.
ObjectID
Unique object identifiers, as returned by the id() member function.

Note that the normal Glish conversions will happen automatically for the scalar and array types,. In particular, a numeric type may be retrieved as any other numeric type.

At present we envisage that ultimately we will provide in addition to the above:

MeasurementSet, The measure types (MPosition et al.), some Functionals (Gaussian and Polynomials), Matrix, Cube(?), Record.4.
Let us know when you need parameters of the above, or other, types.

As described briefly in the previous section, Parameters are defined with the following constructor:

template<class T> class Parameter
{
    ...
    Parameter(ParameterSet &parameters, const String &name, 
              ParameterSet::Direction direction);
};

The first time this constructor defines is called a parameter with the given name and direction is defined in the given parameter set. The parameter value may not be used, since the parameter has just been defined -- it cannot have been filled in yet! The second time time it is called the parameter to the value in the supplied parameter has been set, and hence you may use it. Note that it is a mistake to define more than one parameter with the same name (this mistake will be caught by the ParameterSet.

The name may be any string except one beginning with an underscore, which are reserved for system use. The name returnval is used for function return values -- do not use it for any other purpose. The direction must be oe of ParameterSet::In, ParameterSet::Out, or ParameterSet::InOut.

Constraints

While defining a Parameter<T>, a Constraint<T> may be attached. This is done so that the system can perform extra error checking on your behalf.

The following constraints are available:

ParameterRnge
for scalar values, i.e., min$ \le$param$ \le$max. For example, if you wanted to restrict an angle to the range 0$ \le$$ \theta$$ \le$$ \pi$/2 you could do so as follows:

Parameter<Double> theta(params, "theta", ParameterSet::In);
theta.setConstraint(ParameterRange<Double>(0.0, C::pi/2.0);

NewFile
for strings which are to be used for output files. If the file already exists, the use has an opportunity to delete it. The default is to not overwrite the file.
Parameter<String> outfile(params, "outfile", ParameterSet::In);
outfile.setConstraint(NewFile());

If you find yourself doing error-checking on your parameters that you think would be usefully embedded in a constraint, please let us know.

Embed your class in a server

Once you have created your DO class, it is remarkably easy to create a server executable to embed it in.

Here is the main program for a DO server executable:

#include <aips/Tasking.h>
#include <your includes go here>

int main(int argc, char **argv)
{
    ObjectController controller(argc, argv);
    controller.addMaker("squarer", new StandardObjectFactory<squarer>);
    controller.loop();
    return 0;
}

You can call addMaker() for as many different classes as you want to embed in the current server. Generally you will want to group multiple classes of similar purpose (e.g, synthesis processing) into one server executable to amortize the amount of disk and memory use used by the greedy C++ compilers. So, for example, the numerics server is used for FFT's, fitting and the like, and the image server is used to serve DO's for all image-plane only functionality. Note that the ObjectController will delete the factory when it is done with it -- you must not delete the pointer yourself.

A Factory is a class that knows how to create another class. In particular, StandardObjectFactory<type> is a class that know how to create objects of any type that has a default constructor. Currently with the GNU compiler you will need to create an extra line in the templates for each new use of StandardObjectFactory<T>.


Application services

An application services is some utility class or function that application programmers can call to interact with their users or the system. The presently implemented or envisaged services are:

Logging
Messages posted to the global sink will be sent to Glish and displayed to the user in some fashion he specifies, and eventually saved to a log table. As described above, parameter values and time information will be logged automatically by the system, so you do not need to log that. Otherwise, you should err on the side of logging too much than too little -- you can always change the filter to DEBUGGING level if the user is seeing too much information. Logging is described in Logging.h.
Hints
The following hints are available as static functions in class ApplicationEnvironment. The hints are not guaranteed to be correct -- they (will) rely on values set at installation time -- however the default values are lowest-common-denominator values. [We need to figure out where to document these sorts of installation values -- aipsrc in this case].
memoryInMB()
Physical memory of the host system.
nProcessors()
Number of processors.
isInteractive()
Are we being run interactively -- if not you might, e.g., change your data display strategy.
choice()
Present some descriptive text and set of (string) possible answers to the user (the choices will appear in buttons). For example, ``File exists, over-write? no/yes.'' For non-interactive use the first (i.e. default) choice will be returned.
Object access
It might be useful to obtain access to another DO that is running in the same server. You can do this as follows:
  void myclass::method(const ObjectID &id)
  {
      ObjectController *controller = 
                           ApplicationEnvironment::objectController();
      if (controller == 0) { ... no controller is running - error ...}
      ApplicationObject *obj = controller->getObject(id);
      if (obj == 0) { ... no object `id' in the controller - error ...}
      // Presumably you'd call obj->runMethod here
  }
Do not delete either of the pointers! Similarly, your class can make an object and put it into the ObjectController using a similar process.
Aipsrc
This is a method to get resources out of the .aipsrc files. [reference?] These will often be used to, e.g., over-ride the location of standard files, e.g. IERS data. not yet implemented (but soon)
Progress
Display a ``sliding bar'' visual indication of how far a (typically) long-running process has progressed. A no-op for non-interactive sessions. not yet implemented
PGPlotter
Plotting (both line and graphics) is available through the PGPlotter class. The preferred way for an application to get its PGPlotter is via ApplicationEnvironment::getPlotter.

Exception strategy

If you can meaningfully handle an exception from lower down in the library you should of course handle it and proceed with execution. If you cannot handle an exception, there is no point in catching it. One exception might be to rethrow an exception with a more comprehensible error message5 -- often you, the ``high-level'' programmer will know what the likely user-level error is apt to be, whereas the ``low-level'' library programmer may not be able to provide a meaningful error message.

Similarly, if you detect some error condition you cannot handle, you should throw an exception with an informative error message -- ideally one that states what the likely problem and cure is.

Since we don't want to discourage defensive programming, assertions testing for ``cannot happen'' errors need have no descriptive text, e.g.:

    Type *ptr = new ...;
    AlwaysAssert(ptr != 0, AipsError);

Non-standard constructors

You can ignore this section if your class only has a default constructor and (possibly) a copy constructor. In this case StandardObjectFactory<type> suffices.

If, however, your DO class does not have a default constructor, or if it has multiple constructors which you want to give users access to, you must create your own factory class. To do this entails two steps:

1.
Derive a new class from the abstract base class ApplicationObjectFactory.
2.
Override its make() method using a technique essentially identical to the runMethod() technique we saw earlier.

Suppose we are creating an image DO that lets the user directly manipulate an AIPS++ PagedImage object. There is no sensible ``null'' object for such a class, and in fact we might wish to have multiple constructors:

class image : public ApplicationObject
{
public:
    // Attach to an existing image
    image(PagedImage<Float> &theimage);
    image(const String &imagefile, const String &fitsfile, Index whichhdu);
    ...
};

To create objects of this class, we need to create a special purpose ApplicationObjectFactory and override its make() method. This would probably be the only member function in the class.

class imageFactory : public ApplicationObjectFactory
{
    virtual MethodResult make(ApplicationObject *&newObject,
                              const String &whichConstructor,
                              ParameterSet &inputRecord,
                              Bool runConstructor);
};

This would be implemented with a technique essentially the same as we saw earlier with runMethod(), however since we expect the object creation rate to be low compared to the member function creation rate, we if on the constructor name directly. The constructor name is assigned by us, and may be anything so long as each constructor has a unique name.

Here is a sketch of an implementation for an Object factory with two constructors.

MethodResult imageFactory::make(ApplicationObject *&newObject,
                              const String &whichConstructor,
                              ParameterSet &parameters,
                              Bool runConstructor)
{
    MethodResult retval;                                           //  1
    newObject = 0;                                                 //  2
    
    if (whichConstructor == "image") {                             //  3
            Parameter< PagedImage<Float> > theimage(parameters,    //  4
                                            "theimage",
                                             ParameterSet::In);
        if (runConstructor) {                                      //  5
            newObject = new image(theimage());                     //  6
        }
    } else if (whichConstructor == "fits2image") {                 //  7
        ...
    } else {                                                       //  8
        retval = String("Unknown constructor ") + whichConstructor;
    }

    if (retval.ok() && runConstructor && !newObject) {             //  9
        retval = "Memory allocation error";
    }
    return retval;
}

Some comments:

1.
Assume this routine works until we discover otherwise.
2.
This point at an object of the created type, allocated with new. Initialize with zero.
3.
Write an ``if'' for each possible constructor. You can use arbitrary names, however I suggest using the names that are the same as the name you will give the corresponding function in Glish.
4.
Just like in runMethod first we define the parameters. Here we want an image.
5.
If runConstructor is false, we are only defining parameters. If true, we actually want to create an object.
6.
Create the object. We will check that new succeeded later. The image parameter has already been instantiated, so you don't need to check that it exists, has the right pixel type, and so on.
7.
Another constructor.
8.
Defensive programming -- make sure we catch unknown constructors. Since newObject was initialized with zero, the system would have caught the error even without this line, but the error message would be less descriptive.
9.
Here we check once for all constructors that the new succeeded. We could also have done this in each ``if'' branch.

The other place the names of the constructors will be used is in the Glish construction functions, see section 8.1.


next up previous contents home.gif
Next: Binding Distributed Objects to Glish Up: How to Write an AIPS++ Application Previous: Documenting Your Application   Contents
Please send questions or comments about AIPS++ to aips2-request@nrao.edu.
Copyright © 1995-2000 Associated Universities Inc., Washington, D.C.

Return to AIPS++ Home Page
2006-10-15