-
Notifications
You must be signed in to change notification settings - Fork 48
cisstCommon Data Generator
Table of Contents generated with DocToc
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 is one possible solution. Amongst its advantages:
- Allows heterogeneous containers of objects
- Possibility to perform run-time type check using Run-Time Type Information (RTTI).
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 typeX
) within the cisst "framework" one can either:- Create a new class derived from
cmnGenericObject
andX
. Multiple inheritance can be tricky and can lead to strange problems. For example, QtQObject
need to be derived fromQObject
first as Qt tend to cast (reinterpret_cast
objects toQObject
and this only works if the "vtable" for theQObject
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 typeX
. To access the objectdata
the user will now have to writeobject.data
.
- Create a new class derived from
- 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 ...
An alternative solution is to require global functions to be overloaded for any type that one wants to use with cisst. Let's take the serialization example and consider a simple functionality: HumanReadable. This can be implemented using a base class and a virtual method std::string cmnGenericObject::HumanReadable(void) const. The main advantage is that all objects are now derived from the base class and we can now use basePointer->HumanReadable(). the main drawback is that we now have to redefine all our data objects using derivation and very likely multiple inheritance. This makes it harder to use with external data types, say VTK classes.
One can also use a global function overloaded for each type one wants to use. For example:
std::string cmnDataHumanReadable(const double & data);
std::string cmnDataHumanReadable(const vtkSphereSource & data);
template <typename _elementType>
std::string cmnDataHumanReadable(const std::vector<_elementType> & data);
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
).
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].
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)
}
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).
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.
See examples with inlined comments:
- Classes not used with cisstMultiTask: demoData.cdg
- Classes used with cisstMultiTask:
- mtsComponentState.cdg and user implementation for extra code mtsComponentState.cpp
- prmPositionCartesianGet.cdg
- prmPositionJointGet.cdg
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)
- Home
- Libraries & components
- Download
- Compile (FAQ)
- Reference manual
- cisstCommon
- cisstVector
- cisstNumerical
- cisstOSAbstraction
- TBD
- cisstMultiTask
- cisstRobot
- cisstStereoVision
- Developers