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 processes
  • Save data in text files 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. The new type is often referred as a proxy object. 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);

Or even better a templated class with a static method:

template <>
class cmnData<double> {
  public:
    static std::string HumanReadable(double & data) {
      return someStringBasedOnDataValue;
    }
 };

The later is preferred because it doesn't leave any room for interpretation or implicit cast for the compiler. For the end user, this allows to use the type X as he or she is used to. For the cisst libraries, all objects can now be handled using the same set of features. Within the cisst libraries, we can still use heterogenous containers using the proxy pattern but these proxies don't interfere with the default API of X.

cmnData

cisst implementation

Unfortunately there are different approaches implemented within the cisst libraries, mostly reflecting the history of the libraries.

The oldest code relies on inheritance from a generic base type (cmnGenericObject for cisstCommon and mtsGenericObject for cisstMultiTask). The cisstMultiTask library also uses type traits and internal proxies. 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).

Ultimately, cisstMultiTask data types should all be using the type traits approach with cmnData and internal proxies.

Data generator

Whichever approach is chosen, there is a fair amount of code to write for each data type:

  • This is a tedious task
  • It's 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. The only requirement is that a few global functions need to be overloaded to handle non standard data types (e.g. a VTK mesh). See Hand Written data types.

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)

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)
}
Clone this wiki locally