How to build a SMARTSOFT/ACE Component

Before implementing a SMARTSOFT/ACE Component the overall system should be known. This includes the development of a Component Model and the definition of Communication Objects. As a result a developer has to know the overall system (e.g. something like the System Example). If this level is reached, the components can be implemented as described in this section.

The following two steps are necessary to implement a component.

  1. Implement the component hull.
  2. Define User-tasks and Communication Ports, which is described in the second section.

First the component hull initializes the framework environment, which encapsulates the user code area and offers the possibility to initialize communication ports to communicate with other components. This step is done typically once at the beginning and is after this mostly stable. In the second step component's core functionality is added using User-Tasks and the communication with other components is initialized using Communication Ports. This step depends highly on user needs like use cases and other requirements. Hence this part is more likely to change with fluctuating requirements.

How to build a component hull

The very first step in implementing a SMARTSOFT/ACE Component is to initialize a component hull. There are many ways of doing that, however, there are two approved ways as shown in this section.

 

First the simplest version is shown in the following code listing:

#include "smartSoft.hh"

int main(int argc, char * argv[])
{
        try {
                // initialize a SmartComponent
                CHS::SmartComponent component("MySimpleComponent", argc, argv);

                // User-task(s) initialization comes here...

                // start component management
                component.run();
        } catch (std::exception &e) {
                std::cout << e.what() << std::endl;
        } catch(...) {
                std::cout << "Uncaught exception" << std::endl;
        }

        return 0;
}

That's it, the component hull is ready in its simplest form and the component is now executable - of course till now the component does nothing reasonable, because user code is missing. The first noticeable part in this code listing is the try-catch block around a SmartComponent. SMARTSOFT/ACE has a strict policy for exceptions. Exceptions could be thrown only during initialization or aborting. They won't be thrown at runtime, instead SMARTSOFT/ACE provides StatusCode return values for that purpose (see Enumerations declaration in Doxygen for more details). Second, the component is started in two steps. First an instance of a SmartComponent has to be initialized (with a component name and the standard main parameters). Than the run() method starts all the internal mechanisms of a SMARTSOFT/ACE Component. To understand what happens inside the component see the following listing.

  • The client side of the NamingService will be started
    • A configuration file (usually cfg.conf) to parameterize the internal NamingService client will be searched (make sure the file is accessible from the component binary)
    • The client side of the NamingService will be started (make sure the NamingServcie Daemon is running)
  • All the internal mechanisms to handle Communication-Ports, User-tasks, internal Timings and States will be initialized
  • The main-thread loop is used for the internal component cycle which runs infinite till the component is aborted (e.g. with STRG+C command)

If one starts the component (see code listing above) the following output should display on a console.

This is ACE Version 5.7.0

ACE/SmartSoft - version 1.6.0 (major release).

naming_service: initialized successfully!

Component MySimpleComponent started
compiled: Jan 12 2010 10:08:40

This output indicates successful initialization and a ready to use SMARTSOFT/ACE environment. Otherwise an exception indicates on an error (see Installation for possible problems). The first line shows the current version of ACE used by SMARTSOFT/ACE. This helps to eliminate problems by using different ACE versions on the same machine concurrently. The version is read directly out of the ACE dynamic library, which is first found in the underlying OS. Second line shows the SMARTSOFT/ACE version, which is hard-coded inside of SMARTSOFT/ACE API and is changed with every new release. This helps to eliminate problems by using different SMARTSOFT/ACE versions, which could be incompatible. Third line indicates successful initialization of NamingService. Otherwise an error description indicates on the problem. Finally the component name is shown with a success message and the compile date, which is changed every time when the sources are recompiled.

From here on the User-task(s) and the Communication Port(s) can be added (see next section) or a more advanced initialization of a component hull can be used (see in the following).

A more advanced initialization procedure

Till now a simple version of a component hull was used. In most cases this is the preferred way. On the other hand a SmartComponent has further methods, which give a user the possibility to change the default component behavior (see the Doxygen documentation for more information on SmartComponent). In the following the user defined clean-up procedure is chosen to demonstrate this functionality. This approach is sometimes needed if for example hardware drivers are used, that has to be closed down manually before the component aborts - or to make sure that all opened files are flushed before aborting. The next code listing shows an example for this approach.

#include "smartSoft.hh"

class MyComponent: public CHS::SmartComponent
{
protected:
    // User-task(s) and Communication Port(s) come here...
public:
    /// Default constructor
    MyComponent(std::string name, int argc, char* argv[])
        :    CHS::SmartComponent(name, argc, argv)
    {  }

    /// Default destructor
    virtual ~MyComponent() {  }

    /// Signal handler to handle STRG+C
    virtual int handle_signal (int signum, siginfo_t *, ucontext_t *)
    {
        std::cerr << "Component Signal " << signum << " received" << std::endl;
        if (signum == SIGINT) {
            // STRG+C signal received

            // 1) disable blocking waits on communication ports of this component
            this->blocking(false);

            //------------------------------------------------------------------
            // 2) do clean-up of global resources (closing files, drivers ...)
            //------------------------------------------------------------------

            // 3) abort immediately
            exit(0);
        }
        return 0;
    }
};

// MAIN function
int main(int argc, char* argv[])
{
    try {
        MyComponent component("MyComponent", argc, argv);
        component.run();
    } catch (std::exception &e) {
        std::cout << e.what() << std::endl;
    } catch (...) {
        std::cout << "Uncaught exception" << std::endl;
    }

    return 0;
}

At the beginning an own class MyComponent is derived from SmartComponent. Thus MyComponent inherits all functionality from SmartComponent. In this example the default constructor of MyComponent simply delegates the parameters (component-name and the main-parameters) further to SmartComponent. Additionally the handle_signal(...)method from SmartComponent is overloaded. Overloading the handle_signal(...) method allows the user to catch all software signals in the component. In this case the STRG+C signal is of interest. Hence the first instruction in the handle_signal method should be always the comparison of signum parameter with SIGINT number (which in fact is the STRG+C signal). Now three steps are quite common at this level.

First the blocking(false) command is called. This function of SmartComponent is quite powerful. With that all blocking calls caused by communication patterns work flow are instantly unblocked. This causes all threads that wait on communication port responses to wake up and to resume their execution. The corresponding method that a thread was blocked on returns an error code indicating a SmartSoft interrupt. This allows a thread to react on such interrupts and to break up its infinite loop at a reasonable state that can be only resolved local.

Second the global resources can be cleaned up before aborting the component. Closing files and/or devices that would be otherwise stay locked for the process are typical instructions here. Another example is a logfile containing XML structure that can be completed with a closing XML-tag. This is not the right place to clean up User-tasks, because typically only User-tasks are aware of its component hull and not the Hull of User-tasks. Instead the clean-up should be done inside of destructors (for more information read the following section).

Finally after cleaning-up global resources the component can be safely aborted. This is done with the exit(0) command.

Till now we used two different approaches to initialize a SMARTSOFT/ACE component hull. On the one hand the first approach is quite simple and initializes a component with automatic thread abortion which should be feasible in most cases. On the other hand a user sometimes needs more control over global resources, which is demonstrated in the second approach. Finally there is one further approach possible - using ManagedTasks - which will be described in the following section.

How to add User-tasks and Communication Ports

This section focuses on the internals of a component hull namely User-tasks and Communication-Ports.

User-tasks give a user the possibility to implement concurrent task execution which is portable to different platforms as long as a user does not add OS specific code. To implement User-tasks the classes SmartTask or ManagedTask can be used. SmartTask is a simple implementation of an active object using ACE_Task as a basis. ManagedTask is similar to SmartTask with the difference of added centralized thread management. Thus the thread creation and abortion is handled inside of SMARTSOFT/ACE Kernel. This offers a user the option to be informed about thread abortion inside of a ManagedTask (try out the eleventh example for more information on MangedTasks).

Communication among Components is done using Communication Ports which implement Communication Patterns. The server-side ports add a service to a component hull which can be used by client-side ports. There are seven different Communication Patterns. The table below summarizes the documentation of patterns and corresponding ports.

Communication Pattern Implementation
Table: Overview of Communication Patterns and their implementation
Send Pattern SendClient and SendServer
Query Pattern QueryClient and QueryServer
PushNewest Pattern PushNewestClient and PushNewestServer
PushTimed Pattern PushTimedClient and PushTimedServer
Event Pattern EventClient and EventServer
Wiring Pattern WiringMaster and WiringSlave
State Pattern StateClient and StateServer

There are several example components that demonstrate the usage of each communication pattern. Additionally a practical component example demonstrates the steps described above on a real world example component. For that a component for a laser range finder is developed step-by-step describing the design decisions made at vital points.