| Previous | Index | Next |
This chapter gives a short introduction to CLAM library development issues to those who intend to write a new processing class. The chapter assumes certain familiarity with the use of the library, so if you are completely new to it, you might want to read the usage tutorial first.
The rest of this section will give an overview to the library class hierarchies. Section 37 gives a brief overview of the main tasks you will need to perform while implementing a processing class. In section 38 the construction and configuration interface of processing classes will be described. Section 39 will describe their execution interface. Finally, the following sections will summarize some implementation details to keep in mind while writing a processing class, especially exception handling and code testing.
When making a new processing class, it is important to be familiar from the beginning with the Processing and the ProcessingConfig classes hierarchies shown in figure f 5. This figure shows a small collection of the available processing classes, and their related configuration classes.
Figure 5: Processing and Configuration classes hierarchy
As you can see in the figure, there is a base Processing class from which all other processing classes must derive. This class provides an abstract interface for processing classes. The class is defined in the following source file:
$(TOP)/src/Processing/Processing.hxx
You should also be familiar with ProcessingData classes, at least with the fundamental ones. They are described in a different chapter of this document.
You may never need to create a new ProcessingData class, but you will have to know how to use them in order to write a new Processing class.
If your class will be part of the CLAM library, you should write it keeping in mind these objectives, in suggested order of priority.
Safety.
Your class should work as expected, contain as few bugs as possible, and should detect as often as possible when it is not being used properly. More about this in section 43 and section 44 .
Clarity.
Your code should be easy to understand. Reading it should give a clear idea of the algorithms that are being used.
Efficiency.
Your class should work fast. This is, after all, why are we using C++ instead of a more sane language.
Once you have chosen an efficient algorithm, it is usually not a good idea to think too much about code efficiency while you are writing your class. It is usually hard to figure out what will be the real bottleneck in your code until you run a profile on it, and you can not do that until it is finished and working correctly.
Also, clearly written code is easier to modify later, and thus to optimize. If you obfuscate your code trying to avoid some imaginary efficiency problem, and you later find that efficiency problems are caused by a different reason, it will be harder to fix the problem. If your code is clear, optimizing it later will usually be an easy task.
The mayor tasks you need to undertake when writing a new processing class are briefly described below. All of them will be further described in the next sections.
Ports
: Your processing class will have a set of inputs and outputs. You should declare the related Port attributes in it.
Warning: Ports, although recommended, are still not mandatory so you may find classes in the repository that still make no use of Ports. These classes though will necessarily be re-factored soon when the Ports interface becomes mandatory
Controls
: If your class is to have input or output Controls, you also have to declare the related Control attributes in it (section 40). Note that you must declare as a control any attribute that is supposed to be modified while the Processing object is running.
This requires writing a helper configuration class and writing your processing class constructors, in which all the non-configuration related initialization can be performed.
Processing objects reconfiguration is performed using the Configure() method provided in the Processing base class. You don't have to write this method. But you do need to write a ConcreteConfigure() method that will be called from the Configure() method in the base class (more in section 38.3). This method must do all the initialization stuff dependent on configuration parameters.
You have to provide a Do() method that reads the data in the ports and runs a processing ``cycle'' on it (section 39.2).
You may also write a Do(...) method with the data arguments specific to your processing class. This is usually the method where the actual processing algorithm is implemented.
Processing object execution state (section 39.1) is controlled using the Start() and Stop() methods implemented in the Processing base class. You can not overload these methods.
If objects of your class need to perform any special operations at start or stop time, you can overload the ConcreteStart() and ConcreteStop() methods, which will be called from the base class Start() and Stop() methods when the user calls them. See section 40.3 to see what kind of operations need to be done in your ConcreteStart and ConcreteStop methods.
You should always write a ``class test'' program for your processing class, the why and how are explained in section 44.
The most important item related to object construction and configuration are the processing configuration classes, which will be described in section 38.1. Section 38.2 will describe how to write the Processing constructors, and section 38.3 will describe how Processing objects can be reconfigured, and what needs to be implemented in the concrete classes to support this mechanism.
As figure f 5 suggested, each processing class requires a related configuration class.
The role of the configuration class is to store all the necessary information to configure an object of the related Processing class. In fact, configuration classes may be described as a place-holder for parameters which would otherwise be specified as individual arguments in the processing class constructors.
Processing classes must provide both a constructor and a ConcreteConfigure method taking an object of this configuration class as argument (more about this latter, in section 38.2 and section 38.3.
Configuration classes are implemented as dynamic types that store collections of configuration attributes (implemented as dynamic attributes).
When writing a configuration class, you will usually include configuration attributes such as:
It is important to keep in mind that the configuration mechanism can not be used to change parameters of a processing object during processing execution, only during an initial configuration stage (see section 39.1 for more details on this). Your processing class should provide input controls (section 40) for run-time parameter changes (a configuration attribute can be used to set an initial value for those controls, though).
Processing configuration classes must derive from the base ProcessingConfig class, defined in the file
src/Processing/Processing.hxx
The name of the configuration class should be the same as the name of the processing object, adding the Config suffix.
In some cases several processing classes may share the same configuration class. The FFT is a clear example of this. For example, figure f 5 shows that several FFT classes exist for different algorithm implementations, but they all derive from a common FFT_base class, and they share a common configuration class: FFTConfig.
In other words: you will have to write a Configuration class for your processing class, unless the latter is a different implementation of an existent processing class, and may share its existent configuration class.
You will typically place the declaration of both classes (configuration and processing) in the same header file. This file should have the same name as the processing class.
Once you have written your configuration class, you can write the declaration of your processing class constructors and configuration methods.
Processing classes must provide two constructors: a default constructor and a constructor with the configuration class as its argument type. In most classes they will look just like this (explicit member constructor calls are omitted):
SpectrumProduct::SpectrumProduct()The Configure method called inside the constructors is implemented in the base class, and is described in section 38.3.
{
Configure(SpecProductConfig());
}
SpectrumProduct::SpectrumProduct(const SpecProductConfig &c)
{
Configure(c);
}
You will usually need to include some member constructor calls from your constructors. If you are using controls, for example, you should call the constructors of the control attributes, as described in section 40
You may need to perform more tasks in the constructor, but most of them will probably be better implemented in your ConcreteConfigure method (see below), which will be automatically called from Configure.
You should not try to provide a copy constructor for a processing object. It will not work well3.
Once a processing object is instantiated, it can be (re)configured using the Configure method, implemented in the Processing base class4. This method will check that the object can be actually reconfigured (i.e., that the object is not ``running'') and will call the concrete configuration method (which you have to write), ConcreteConfigure(), in the concrete processing class.
These two methods (The Configure method and the concrete configuration method) are declared in the base class as follows:
protected:
virtual bool ConcreteConfigure(const ProcessingConfig&) = 0;
public:
bool Configure(const ProcessingConfig&) throw(ErrProcessingObj);
So you only have to implement a protected or private ConcreteConfigure method in your new processing class, and you may forget about the Configure method (although it is useful to call it from the constructors, as described above).
Additionally, you need to provide a configuration accessor:
const ProcessingConfig &GetConfig()const;
This method must return a reference to a configuration object holding the current object configuration. The easiest way to do this is to store a copy of the object passed to the ConcreteConfigure method, so GetConfig only has to return it.
You should keep in mind while writing other processing class parts that the configuration attribute (if you keep it) must store only initial state of the object. The object should not change configuration parameters itself. This means that the object returned by the GetConfig method must show the latest values passed either to the constructor or to the Configure method.
Processing objects are in a certain execution state at any moment. This is best shown in a state diagram, figure f 6. The object is initially in the Unconfigured state.
Figure 6: Processing Objects execution states.
All the methods shown in the state diagram, except the Do() method are implemented in the Processing base class. These are briefly described below. Note that all the state transitions are done via these methods; the state variable is also kept in the base class, so you do not need to worry about execution state management when implementing a new processing class.
You may need to perform some specific operations in your class at certain state transitions. There are several virtual methods that you can override to do so, and which are described in the following table:
| concrete method | called from | mandatory |
| ConcreteConfigure(ProcessingConfig&) | Configure(ProcessingConfig&) | yes |
| ConcreteStart() | Start() | no |
| ConcreteStop() | Stop() | no |
Note that the last two methods are very tied to supervised mode operation.
The main execution methods are the Do methods. They are the ones which actually perform the processing action.
There are two different kinds of Do methods:
Both kinds of Do() operations work in the same way: they read a certain number of data objects from each of the inputs, and write a certain number of data objects to each of the outputs. The difference is that the concrete Do() method takes this data objects as arguments (and therefore does not use ports), while the generic Do() operation has no arguments, and accesses the Data through the Ports objects.
figure f 7 shows a sequence diagram for the execution of a processing object with a single input and a single output.
Figure 7: Execution sequence
Processing objects must provide a Do method which takes the input and the output objects as its arguments. You may provide several Do methods for different object configurations (such as different sets of active inputs and outputs).
You should carefully check that the dynamic attributes you need are instantiated in the input and output objects passed to a Do.
A good approach to this is having some kind of prototype state variable which is updated when SetPrototypes is called. Your Do methods can then use a switch statement to choose the processing code to execute.
You can, for example, do a fast execution when the inputs and the outputs have the attributes most convenient for your computations, and a slow execution for other cases, in which you perform previous and subsequent data conversions.
In this slow cases, if the data classes you are handling provide attribute conversion routines, you can usually just add the attribute you need to the data object, and call a proper conversion routine to update it. See the FFT_rfftw code for an example of this.
Some processing classes need to allow external entities to change the behavior of the objects asynchronously during their execution. Input controls are the mechanism to perform this kind of run time changes.
Also, a processing class may be used to detect some kind of event. Output controls are the way to make notifications on asynchronous events.
An application can connect output controls from some processing objects to the input control of others.
When we use the ``asynchronous'' word here, we mean that control values do not flow with a given rate, as data does.
In CLAM, control values are floating point numbers.
There are two different mechanism to implement input controls. Controls using the first mechanism simply store a value, and allow an externally connected output control to change this value. These controls are described in section 40.1.1.
For the second mechanism, you have to write a special method in your class, a call-back method which will be called whenever a new value is sent to the input control. This mechanism is described in section 40.1.2
To use a regular input control, you need to
Declare an InControl attribute in your class with a descriptive name. For example, if you have a couple of input controls with pitch and amplitude values, you should declare them like:
InControl mInPitch;
InControl mInAmplitude;
Call the InControl constructors from your processing class constructors. They take two arguments: the control textual name, and a pointer to the processing object containing the control. For example, the constructor of a processing class called MyClass containing two input controls would look something like:
MyClass(const MyClassConfig &c)
: mInPitch("Pitch", this),
mInAmplitude("Amplitude", this)
{
Configure(c);
}
Give an initial value to the control. You should do this in the ConcreteStart() method of your class. Input controls provide a DoControl(value) method to change the value.
In some cases you may want to have a call-back method executed each time an input control changes its value. Some reasons for this might be:
In order to use this call-back mechanism, you have to:
Declare your control attributes to be of type InControlTmpl<MyClass> (where MyClass is the name of your processing class). Following the example in previous section, you would have:
InControlTmpl<MyProcObj> mInPitch;
InControlTmpl<MyProcObj> mInAmplitude;
Define the call-back method(s) in your class, which must take a single argument of type TControlData. For example
int InPitchControlCB(TControlData val);
int InAmplitudeControlCB(TControlData val);
Call the InControlTmpl constructors from your processing class constructors. They take three arguments: the control textual name, a pointer to the processing object containing the control, and the address of the call-back method. Following the example:
MyClass(const MyClassConfig &c)
: mInPitch("Pitch", this, &MyClass::InPitchControlCB),
mInAmplitude("Amplitude",
&MyClass::InAmplitudeControlCB))
{
Configure(c);
}
You add output controls to your class in the same way you add regular input controls, but taking into account that the name of the control class is now OutControl. So, you need to:
Now you can send values through the output control. You will usually do it from time to time in your Do() method, using the SendControl(TControlData val) method of the OutControl class.
Input controls must be initialized in the ConcreteStart() method. If the initial value of a control should be chosen by the user, a configuration attribute can be provided in the configuration class for this task.
``Object state'' usually refers to the specific values that the attributes of this object have in a given moment of time.
For processing objects, it is useful to consider two different kinds of attributes, and thus two different kinds of ``state'':
Initialization, usage and destruction of these attributes should be done in different ways, as described below.
The first ones would be attributes which may only change when the processing object configuration is changed (i.e., when the Configure() method is called).
For example, if your configuration class includes an attribute which defines the size of some internal buffers in your processing objects, you should create or resize these internal buffers when the ConcreteConfigure() method is called, instead of doing so directly in the class constructors.
Some processing classes may need to keep internal computed values between different calls to Do methods. This is usually done using normal private class members.
Some examples of this are:
Sometimes it may be a good idea to provide a public accessor (getter) method to some of these internal values, so that applications using the class can easily implement some run time debugging. But it is usually a better idea to provide this access in the form of input or output Controls.
The initialization of internal state attributes related to object execution (such as execution counts, accumulated values, time references, etc) must be performed in the ConcreteStart() method.
Also, if you want to liberate some resources when the object stops being run, you can implement this in the ConcreteStop() method.
Sometimes you need to write a large processing object which uses other processing objects internally to perform some parts of the algorithm.
There is a standard way to do this:
See the file examples/POCompositeExample.cxx for more details.
Your classes may some times throw exceptions. This will usually happen in two circumstances:
As you probably remember from section 36.2, one of the main self imposed requirements in the CLAM library is code safety.
One of the best tools to achieve this is using ``assertions'' (condition checks) in your code to check that things are like they are supposed to be.
Assertions are a general mechanism inside the CLAM library, and are thus also discussed elsewhere. But it is worth making some comments about how they should be used in processing classes.
There are mainly two kinds of assertions:
A clear example of this kind of check is testing the value of an index provided by the user to see if fits the size of a data array.
Easy. Just use the CLAM_ASSERT macro. This will usually be fine for you. If not, the assertions mechanism is fully described in a different section of this document.
In some circumstances, you may want to make a check while debugging, and remove it later for efficiency reasons. An example of this is checking the value of an index in the Array<T> class indexing operators. For this kind of check, you can use the CLAM_DEBUG_ASSERT macro. The checks made using this macro will be disabled when compiling the library in ``release'' mode.
These may rise when you are performing some operation which may fail, such as trying to open a file, trying to allocate some memory, trying to start an input/output system device, etc.
These are not assertions, in the sense that even if the program is absolutely correct, these conditions may fail.
The behavior when you find one of these problems depends on the context where you find it:
If you have this kind of situation in your code, you should use one of the standard exception classes defined in CLAM library. See the src/Errors/ directory for the collection of available exception classes. If no one fits your needs, it may be a good time to write a new exception class, but you can always throw the base CLAM::Err class meanwhile.
Mainly for two reasons:
A non-trivial bug may take just a minute to detect, understand and fix if you run a test program often while you are coding, because you will always be almost sure that the problem belongs to the latest changes you have made.
It will usually take you more than ten times longer (say, 10 minutes) if you first encounter this bug when you are using your class in a larger system, after making many modifications without testing hard.
If it is someone else who runs into this bug while using your class in a bigger program, it can take him a hundred more times (say, a couple of hours) to trace the problem and fix it (or ask you for a fix).
Some argue that you should write the class test even before you start programming your class, so that you already have a program against which you can try it all along the process of coding.
You should definitely have a look around the tests/ directory in CLAM sources to take a feeling of how the test thing goes. There you can find a test file ``skeleton'' (TestSkeleton.cxx) to take as starting point, but you usually have complete freedom in the way to implement a test. Anyway, it should follow a standard convention to communicate its results:
In the situations where you would normally use a standard C++ enum type, you should consider using a CLAM Enum class instead.
CLAM Enums are much like C++ enums, with the advantage that they have storage capabilities built in. In run-time, C++ enums only provide the integral value, CLAM::Enum's also use the symbolic value (the string). See the src/Standard/Enum.hxx class Doxygen documentation for more information.
CLAM::Flags<N> also provides symbolic usage and storage capabilities to the std::bitset<N> class. You should use the Flags CLAM class, instead of a std::bitset, or instead of an integer plus bit-mask mechanism. That way you gain storage capabilities.
See the src/Standard/Flags.hxx Doxygen documentation for more information.
NOTE1: In previous CLAM releases the Prototype interface was included in the Processing base class and was indeed a recommended way of working. Experience has demonstrated that this feature is only necessay in very specific cases and has therefore been removed from the base class interface. Nevertheless, some particular classes such as the FFT still keep this functionality, which can indeed improve efficiency. Only read the following paragraphs if you are really worried about efficiency issues and have the suspicion that the use of prototypes at the input/output of your processing may help
NOTE2: This section discusses some concepts related to dynamic types. If you are not familiar with them, you should read some relevant documentation on the issue.
The processing data objects used as inputs and outputs for a processing object can sometimes have multiple alternative dynamic attributes, which are often different representations of the same data. In other words, they can have different dynamic prototypes. An example of such multiplicity is the Spectrum class.
Trying to read a dynamic attribute that is not instantiated is a run-time error, so it should never happen. One should use the Has...() dynamic method to check if a certain attribute is instantiated before using it.
Some structural parameters of the data objects must also be checked before using them. Buffers size, for example, is often stored as a dynamic attribute in the data object. One can not assume these values, they must be checked for each data object before relying on them.
If the processing data classes you are handling do not have such kind of prototype alternatives or structural parameters, you don't need to provide the SetPrototypes() method.
Otherwise, processing classes must be ready to handle this attribute diversity. Instances of a well written processing class should be able to cope with every valid prototype at its inputs, probably with different performance in different cases.
In normal cases all of the above requires quite a few checks, which may degrade the performance of the execution method.
The SetPrototypes functions can be used to avoid this performance problem. When the application (or the flow control) calls a SetPrototypes method, the object can assume that subsequent calls to the execution method will pass data objects with the same prototypes and structural parameters as the ones specified with SetPrototypes, until a new call to SetPrototypes is made, or until the UnSetPrototypes method is called.
These methods are just informative; the object is not required to perform any specific action. It can safely ignore these calls. If you want to take advantage of them, you will typically do an exhaustive prototype checking, and set some internal state attribute in your processing object according to what you find in the data. This state variable can be checked with a single switch statement in your Do method.
3 This is due to some missing functionality in the processing object composite system
4 You must not override the base class Configure method.
| Previous | Index | Next |