Skip to content

cisstCommon Data Generator

Anton Deguet edited this page May 12, 2014 · 17 revisions

Table of Contents generated with DocToc

Introduction

In the cisst libraries, as in many other libraries, we often need to handle different data objects in a transparent manner. For example:

  • Serialize and de-serialize to send data between process
  • Save in text file for post processing
  • Display current value in a human readable format
  • Extract numbers from a data object for plotting.

There are multiple ways to provide a similar interface to multiple objects but the most common are class inheritance and type traits.

Class inheritance

Class inheritance is one possible solution. Amongst its advantages:

Amongst the drawbacks:

  • All objects must be derived from a cisst base type (let's call it cmnGenericObject). If one wants to use an object from another library (of type X) within the cisst "framework" one can either:
    • Create a new class derived from cmnGenericObject and X. Multiple inheritance can be tricky and can lead to strange problems. For example, Qt QObject need to be derived from QObject first as Qt tend to cast (reinterpret_cast objects to QObject and this only works if the "vtable" for the QObject comes first). Keep in mind that constructors and assignment operators have to be redefined for all derived classes.
    • Create a new object derived from cmnGenericObject that has a data member of type X. To access the object data the user will now have to write object.data. Keep in mind that all constructors of class X still need to be duplicated.
  • Run-time type checks are fine but in many cases the type is or should be known at compilation time. A compilation error might be preferable to a run-time error ...

Type traits

An alternative solution is to use type traits. Type traits can be implemented using an overloaded global function or templates static methods. Let's consider a simple functionality: std::string HumanReadable(void). This can be implemented using a base class and a virtual method virtual std::string cmnGenericObject::HumanReadable(void) const = 0. Using the type traits approach one can use a global function:

std::string cmnDataHumanReadable(const double & data);
std::string cmnDataHumanReadable(const vtkSphereSource & data);
template <typename _elementType>
std::string cmnDataHumanReadable(const std::vector<_elementType> & data);

Or even better a templated class with a static method:

This works very well at compilation time but doesn't solve the issue of polymorphism. In the current implementation of cisstMultiTask, Peter Kazanzides has introduced a templated proxy which allows to treat these objects in a polymorphic way. The proxy object is derived from a base class (in this case mtsGenericObject). The proxy is created at compilation time, each method of the proxy simply calls the global overloaded function. Internally the library can now manipulate all the data objects using their proxies (we can call these envelopes, stubs, ...).

Ultimately, we might want to get rid of the generic base types cmnGenericObject and mtsGenericObjects altogether and rely on proxies based on each set of features required.

If you want to use custom data types in a generic interface, there currently are two strategies: (1) Inherit from a generic base type (cmnGenericObject for cisstCommon and mtsGenericObject for cisstMultiTask) and invoke a few macros in the header and implementation files, or (2) Define some "helper" functions to enable cisstMultiTask to use a "wrapper" object (mtsGenericObjectProxy) for your type. The data generator (cisstDataGenerator) can be used to generate code for both cases.

The data generator was initially developed for the cisstMultiTask library but since it can be used for any data type it is part of the cisstCommon package. The documentation itself uses many examples for the cisstMultiTask framework.

The cisstMultiTask library itself uses both methods. For example, there is a class mtsDouble3 that inherits from mtsGenericObject and vctFixedSizeVector<double,3> (this is one of the few places in the cisst libraries where multiple inheritance is used). But, you can also directly use vctDouble3 (and its typedef vct3) because cisstMultiTask defines all the "helper" functions that are needed by the wrapper object, mtsGenericObjectProxy<vct3> (which is not the same as mtsDouble3).

Hand written code

Inheriting from mtsGenericObject

For example, if you have the following class:

MyClass.h

class MyClass {
  public:
    MyClass();
};

MyClass.cpp

#include <MyClass.h>

MyClass::MyClass() { }

In order to use this in a cisstMultiTask interface, you would need to make the following modifications:

MyClass.h

// CISST Includes
#include <cisstCommon/cmnGenericObject.h>
#include <cisstCommon/cmnClassServices.h>
#include <cisstCommon/cmnClassRegisterMacros.h>

class MyClass : public mtsGenericObject {
  CMN_DECLARE_SERVICES(CMN_NO_DYNAMIC_CREATION, CMN_LOG_ALLOW_DEFAULT);

  public:
    MyClass();
};

CMN_DECLARE_SERVICES_INSTANTIATION(MyClass);

MyClass.cpp

#include <MyClass.h>

MyClass::MyClass() { }

CMN_IMPLEMENT_SERVICES_TEMPLATED(MyClass);

You should also implement the ToStream and ToStreamRaw methods. If you plan to use this data type over the network, you must implement the SerializeRaw and DeSerializeRaw methods.

For more info on interfacing custom data types with CISST, see [wiki:cisstCommonFAQ].

Not inheriting from mtsGenericObject (i.e., using proxy wrapper)

This can be used to wrap any type for use with cisstMultiTask. In fact, the cisstMultiTask library already does this for common types such as int, double, vct3, etc.

For example, if you have a class MyClass that is not derived from mtsGenericObject, you can do the following:

MyClass.h

typedef mtsGenericObjectProxy<MyClass> mtsMyClassProxy;
CMN_DECLARE_SERVICES_INSTANTIATION(mtsMyClassProxy)

// Define "stream out" operator, if not already defined for your class
CISST_EXPORT std::ostream & operator << (std::ostream & output, const MyClass & object);

// overload cmnSerializeRaw and cmdDeSerializeRaw if you intend to send your data over the network
void CISST_EXPORT cmnSerializeRaw(std::ostream & outputStream, const MyClass & data);
void CISST_EXPORT cmnDeSerializeRaw(std::istream & inputStream, MyClass & data);

MyClass.cpp

CMN_IMPLEMENT_SERVICES_TEMPLATED(mtsMyClassProxy)

std::ostream & operator << (std::ostream & output, const MyClass & object)
{
    output << object.member1 << ", " << object.member2
           // ... repeat for other data members
           << std::endl;
    return output;
}

void CISST_EXPORT cmnSerializeRaw(std::ostream & outputStream, const MyClass & data)
{
    cmnSerializeRaw(outputStream, data.member1);
    cmnSerializeRaw(outputStream, data.member2);
    // ... repeat for other data members
}

void CISST_EXPORT cmnDeSerializeRaw(std::istream & inputStream, MyClass & data)
{
    cmnDeSerializeRaw(inputStream, data.member1);
    cmnDeSerializeRaw(inputStream, data.member2);
    // ... repeat for other data members (make sure order is the same as cmnSerializeRaw)
}

Data generator

There are a few issues with hand written data types:

  • this is a tedious task
  • it is fairly easy to introduce some bugs in the code by omitting one or more data members or base class in the serialize/de-serialize methods
  • each and every data type class has to be updated when a new feature is introduced

To avoid these issues, many libraries rely on a high level description (see for example Corba IDL, ICE, ROS messages, ...) and a code generator to produce the appropriate code in C, C++, ObjectiveC, Python, ... For the cisst libraries, we developed yet another data description format. One of the decisions made is to allow inline C/C++ code in the data description and therefore restrict the target language to C++. On the other hand, this allows us to:

  • create customized API for our data types while the code generator handles the common and repetitive part of the code.
  • use any C/C++ class within your data structures, i.e. message. The only requirement is that a few global functions need to be overloaded to handle non standard data types (e.g. a VTK mesh).

File format

The syntax is fairly simple and light:

  • the file contains a list of scopes, a scope is defined by a keyword followed by {, the scope's content and a closing };
  • each scope can contain other scopes or fields
  • each field is defined by a keyword followed by =, the field's content and a closing ;
  • one can use C++ style line comments, i.e. //

The file format (supported scopes and fields) can be retrieved using the command line option -s or --syntax-only:

cisstDataGenerator --syntax-only
File syntax:
  class {
    base-class {
      is-data = <value must be one of 'true' 'false': default is 'true'>; // (optional) - indicates if the base class is a cisst data type itself
      type = <user defined string>; // (required) - C++ type for the base class, e.g. cmnGenericObject
      visibility = <value must be one of 'public' 'private' 'protected': default is 'public'>; // (optional) - determines if the base class should be public, ...
    }
    typedef {
      name = <user defined string>; // (required) - name of the new type defined
      type = <user defined string>; // (required) - C/C++ type used to define the new type
    }
    member {
      accessors = <value must be one of 'none' 'references' 'set-get' 'all': default is 'all'>; // (optional) - indicates which types of accessors should be generated for the data member
      default = <user defined string>; // (optional) - default value that should be assigned to the data member in the class constructor
      description = <user defined string>; // (optional) - user provided description of the data member
      is-data = <value must be one of 'true' 'false': default is 'true'>; // (optional) - indicates if the data member is a cisst data type itself
      is-size_t = <value must be one of 'true' 'false': default is 'false'>; // (optional) - indicates if the data member is a typedef of size_t or size_t
      name = <user defined string>; // (required) - name of the data member, will also be used to generate accessors
      type = <user defined string>; // (required) - C++ type of the data member (e.g. double, std::string, ...)
      visibility = <value must be one of 'public' 'protected' 'private': default is 'protected'>; // (optional) - indicates if the data member should be public, ...
    }
    inline-header {
      C++ code snippet - code that will be placed as-is in the generated header file
    }
    inline-code {
      C++ code snippet - code that will be placed as-is in the generated source file
    }
    attribute = <user defined string>; // (optional) - string place between 'class' and the class name (e.g. CISST_EXPORT)
    name = <user defined string>; // (required) - name of the generated C++ class
  }
  inline-header {
    C++ code snippet - code that will be placed as-is in the generated header file
  }
  inline-code {
    C++ code snippet - code that will be placed as-is in the generated source file
  }

It is important to note that the cisst data generator doesn't force any specific implementation related to cisstMultiTask, i.e. it is possible to use either inheritance from mtsGenericObject or the proxy approach.

Examples

See examples with inlined comments:

CMake

We provide a CMake macro that simplifies the build process. This macro manages the dependencies as well as build rules between the description file, generated header and source files and object files. The macro is defined in the file cisstMacros.cmake which is automatically included when you include (${CISST_USE_FILE}). Here are two examples of use:

  # create data type using the data generator
  cisst_data_generator (cmnExDataGenerator    # prefix for the CMake variables that will contain the lists of headers/sources
                        ${CMAKE_CURRENT_BINARY_DIR}    # destination directory where do you want the generated files to go
                        ""    # subdirectory for the header file, see next example.  This will be appended to the destination directory.
                        demoData.cdg)    # one or more cisst data description files

  # to compile cisst generated code, need to find header file
  include_directories (${CMAKE_CURRENT_BINARY_DIR})

  add_executable (cmnExDataGenerator
                  ${cmnExDataGenerator_CISST_DG_SRCS}   # variable automatically created and populated by cisst_data_generator macro using the provided prefix
                  main.cpp)

Another example using the include subdirectory and the list of header files generated:

# create data type using the data generator
cisst_data_generator (cisstParameterTypes
                      "${cisst_BINARY_DIR}/include" # where to save the files
                      "cisstParameterTypes/"           # sub directory for include, header files will in "include/cisstParameterTypes"
                                                                    # and can be included using #include <cisstParameterTypes/prmPositionCartesianGet.h>
                      prmPositionCartesianGet.cdg  # using multiple data description files
                      prmPositionJointGet.cdg)

# to compile cisst generated code, need to find header file
include_directories (${CMAKE_CURRENT_BINARY_DIR})

# ${cisstParameterTypes_CISST_DG_SRCS} contains the list of generated source files (absolute paths)
# ${cisstParameterTypes_CISST_DG_HDRS} contains the list of generated header files (absolute paths)
Clone this wiki locally