Getting Started | Documentation | Glish | Learn More | Programming | Contact Us |
Version 1.9 Build 1556 |
|
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:
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.
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.
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 ¶meters, Bool runMethod); };
The changes from before are as follows:
It would probably be useful to see the actual implementation for runMethod():
MethodResult squarer::runMethod(uInt which, ParameterSet ¶meters, 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
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:
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:
// return record like [mean=0, variance=1.0] for any type GlishRecord class::statistics(const GlishArray &a) const;
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 ¶meters, 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.
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:
Parameter<Double> theta(params, "theta", ParameterSet::In); theta.setConstraint(ParameterRange<Double>(0.0, C::pi/2.0);
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.
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>.
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:
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.
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);
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:
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 ¶meters, 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:
The other place the names of the constructors will be used is in the Glish construction functions, see section 8.1.