Object Wrappers

Overview

Starting with MeVisLab 2.1, MeVisLab offers object wrappers that allow access to C++ objects from Python. An object wrapper can be registered for any class, and is retrieved by calling the method object() on a Python object. Typically, object wrappers are used to work directly with Base objects, but of course wrappers can be implemented for ML or Inventor modules to make them scriptable without an intermediate layer of fields. For more information on the underlying Qt binding, see its web page (http://pythonqt.sourceforge.net/).

How to create an object wrapper

Object wrappers are implemented in C++ and register a wrapper name and a wrapper class to an arbitrary C++ class. On the MeVisLab side, a definition file for this wrapper name is required.

A simple object wrapper

We will start by creating an object wrapper for a simple example Base object: the BaseMessengerExample class that can be found in the package MeVisLab/Examples. The BaseMessengerExample contains four attributes of different types (Vector3, double and a custom MessengerShapeType). For the full example wrapper code, see the MLBaseCommunicationExampleWrappers project in MeVisLab/Examples.

Setting up the wrapper project

An object wrapper object looks very similar to an ML module project, but there are some fundamental differences. Because we are writing a class for the PythonQt binding, we need to include the script interface header, but can omit most ML headers normally used in ML modules. Of course we need to include the headers for the classes we want to wrap.

MLBaseCommunicationExampleWrappersInit.cpp:

  #include "MLBaseCommunicationExampleWrappersSystem.h"

  #include <Scripting/mlabScriptInterface.h>

  #include "mlBaseMessengerExampleWrapper.h"

  MLAB_PLUGIN_INIT()
  {
    // The following line is for debug purposes
    mlabInfoConst("MLBaseCommunicationExampleWrappers loaded.");
    MLAB_REGISTER_OBJECT_WRAPPER("BaseMessengerExample", ml::BaseMessengerExample, MLBaseMessengerExampleWrapper);
  }

MLBaseCommunicationExampleWrappers.pro:
Add any projects you need, e.g., the module or class you are wrapping.

  CONFIG += dll ML MLBase MLABNetwork MLABBase QtNeeded
  CONFIG += MLBaseCommunicationExample

MLBaseCommunicationExampleWrappers.def:

  ObjectWrapper BaseMessengerExample {
    DLL            = "MLBaseCommunicationExampleWrappers"
    author         = "put your name here"
  }

mlBaseMessengerExampleWrapper.h:
Wrappers are derived from QObject.

  #include <QObject>

  class MLBaseMessengerExampleWrapper : public QObject
  {
    Q_OBJECT;
  };

Currently, there is no wizard for object wrapper project creation available.

The wrapper class above is useless in its current state, since it doesn't know about any objects, let alone the one we want to wrap. In fact, this code would not compile.

The wrapped object

Now let's add some lines that introduce the class to be wrapped and will make the code compile.

When object() is called, a pointer to the underlying object is passed to the wrapper constructor. The wrapped class usually is not aware of the wrapper.

mlBaseMessengerExampleWrapper.h:

  #include <QObject>
  #include "mlBaseMessengerExample.h"

  class MLBaseMessengerExampleWrapper : public QObject
  {
    Q_OBJECT;

  public:
    MLBaseMessengerExampleWrapper(ml::BaseMessengerExample* baseMessengerExample);

  private:
    ml::BaseMessengerExample* _baseMessengerExample;
  };

mlBaseMessengerExampleWrapper.cpp:

  #include "mlBaseMessengerExample.h"

  MLBaseMessengerExampleWrapper::MLBaseMessengerExampleWrapper(ml::BaseMessengerExample* baseMessengerExample)
  {
      _baseMessengerExample = baseMessengerExample;
  }

Now we would be able to get the wrapper object in a scripting console, but we can't do much with it, since the wrapper class does only define a constructor. Now our BaseMessengerExample class has several attributes we would very much like to read and modify.

Setters and getters

mlBaseMessengerExampleWrapper.h:
Wrapper methods are declared as slots.

  #include <QObject>
  #include "mlBaseMessengerExample.h"

  class MLBaseMessengerExampleWrapper : public QObject
  {
    Q_OBJECT;

  public:
    MLBaseMessengerExampleWrapper(ml::BaseMessengerExample* baseMessengerExample);

  public slots:
    double getDiameter();
    void setDiameter(double diameter);

  private:
    ml::BaseMessengerExample* _baseMessengerExample;
  };

mlBaseMessengerExampleWrapper.cpp:

  #include "mlBaseMessengerExample.h"

  MLBaseMessengerExampleWrapper::MLBaseMessengerExampleWrapper(ml::BaseMessengerExample* baseMessengerExample)
  {
    _baseMessengerExample = baseMessengerExample;
  }

  double MLBaseMessengerExampleWrapper::getDiameter()
  {
    return _baseMessengerExample->getDiameter();
  }
  void MLBaseMessengerExampleWrapper::setDiameter(double diameter)
  {
    _baseMessengerExample->setDiameter(diameter);
  }

Our wrapper object now has a method getDiameter() that returns the value of the diameter property, and a method setDiameter that sets that value. This is pretty straightforward, but let's have a look at that code again. Because diameter is of an elemental type that's available in Qt, we could simply use that type. But a script wrapper can only use Qt types for it's setters and getters. So how are types like strings, Vector3 or STL vectors converted?

How types are converted

When writing object wrappers, we have to be aware that attributes will change their type two times on the way from the wrapped class to the scripting console. First they have to be converted to something PythonQt can understand, and then PythonQt will convert it again to something Python can understand.

The first part is pretty easy, since C++/STL types and Qt types are quite similar. An std::string becomes a QString, an std::vector<T> a QList<T> (actually a QVector<T> would be more appropriate, but we anticipating the conversion to Python types here), elemental types remain elemental types (except for integers, which become qints), and so on. Now with ML types, it's a little more difficult. Elemental types still map to the corresponding elemental Qt type, that is, MLdouble becomes double and MLint becomes qint64. But how about Vector3? For a custom data type, we need to know how it is structured. A Vector3 is basically a list of three double values, so an obvious equivalent would be a QList<double>. More complex types, especially ones with custom behavior (i.e., more than just collections of values), cannot be converted properly. Note that the class we're wrapping in the first place is such a complex type, and we're doing the wrapping piece-by-piece.

The second part is even easier, because PythonQt knows how to convert types between Qt/C++ and Python. Please have a look at the data type mapping (http://pythonqt.sourceforge.net/Developer.html). Note that you can return and receive almost everything with QVariants and QVariantList. If you need to transfer a Python list with differently typed values in it, for example to serialize a more complex object, you can do so with a QVariantList, but you'll need to take care when unpacking that list. Depending on your knowledge about the transfered structures, you'll need to cast values on their way in or out.

mlBaseMessengerExampleWrapper.h:

  #include <QObject>
  #include "mlBaseMessengerExample.h"

  class MLBaseMessengerExampleWrapper : public QObject
  {
    Q_OBJECT;

  public:
    MLBaseMessengerExampleWrapper(ml::BaseMessengerExample* baseMessengerExample);

  public slots:
    double getDiameter();
    QList<double> getPosition();
    QList<double> getColor();
    qint64 getShapeType();

    void setDiameter(double diameter);
    void setPosition(QList<double> position);
    void setColor(QList<double> color);
    void setShapeType(qint64 shapeType);

  private:
    ml::BaseMessengerExample* _baseMessengerExample;
  };

mlBaseMessengerExampleWrapper.cpp:
Note that conversion methods are omitted here.

  #include "mlBaseMessengerExample.h"

  MLBaseMessengerExampleWrapper::MLBaseMessengerExampleWrapper(ml::BaseMessengerExample* baseMessengerExample)
  {
    _baseMessengerExample = baseMessengerExample;
  }

  double MLBaseMessengerExampleWrapper::getDiameter()
  {
    return _baseMessengerExample->getDiameter();
  }
  void MLBaseMessengerExampleWrapper::setDiameter(double diameter)
  {
    _baseMessengerExample->setDiameter(diameter);
  }

  QList<double> MLBaseMessengerExampleWrapper::getPosition()
  {
    return _convertToQList(_baseMessengerExample->getPosition());
  }
  void MLBaseMessengerExampleWrapper::setPosition(QList<double> position)
  {
    _baseMessengerExample->setPosition(_convertToVector3(position));
  }

  QList<double> MLBaseMessengerExampleWrapper::getColor()
  {
    return _convertToQList(_baseMessengerExample->getColor());
  }
  void MLBaseMessengerExampleWrapper::setColor(QList<double> color)
  {
    _baseMessengerExample->setColor(_convertToVector3(color));
  }

  qint64 MLBaseMessengerExampleWrapper::getShapeType()
  {
    return _baseMessengerExample->getShapeType();
  }
  void MLBaseMessengerExampleWrapper::setShapeType(qint64 shapeType)
  {
    _baseMessengerExample->setShapeType(shapeType);
  }

Our example wrapper now offers access to all four attributes of the BaseMessengerExample.

Properties

Both Qt and Python can use properties to read and write class members, and of course we can use those in our wrapper... but this one come's with a price. Qt does not support arbitrary types in its properties, and so we must use QVariants (or QVariantList) instead.

mlBaseMessengerExampleWrapper.h:
Note that we changed the wrapper types of position and color.

  #include <QObject>
  #include "mlBaseMessengerExample.h"

  class MLBaseMessengerExampleWrapper : public QObject
  {
    Q_OBJECT;
    Q_PROPERTY(double diameter READ getDiameter WRITE setDiameter);
    Q_PROPERTY(QVariantList position READ getPosition WRITE setPosition);
    Q_PROPERTY(QVariantList color READ getColor WRITE setColor);
    Q_PROPERTY(qint64 shapeType READ getShapeType WRITE setShapeType);

  public:
    MLBaseMessengerExampleWrapper(ml::BaseMessengerExample* baseMessengerExample);

  public slots:
    double getDiameter();
    QVariantList getPosition();
    QVariantList getColor();
    qint64 getShapeType();

    void setDiameter(double diameter);
    void setPosition(QVariantList position);
    void setColor(QVariantList color);
    void setShapeType(qint64 shapeType);

  private:
    ml::BaseMessengerExample* _baseMessengerExample;
  };

mlBaseMessengerExampleWrapper.cpp:
Just replicate this for the color property. The other setters and getters are not changed. For ease of read, we use pseudo-codish for-loops.

  QVariantList MLBaseMessengerExampleWrapper::getPosition()
  {
    QVariantList vl;
    // pseudo-loop
    for( component in _baseMessengerExample->getPosition() )
    {
      // just insert values
      vl << component;
    }
    return vl;
  }
  void MLBaseMessengerExampleWrapper::setPosition(QVariantList position)
  {
    ml::Vector3 v;
    // pseudo-loop, imagine the idx loop variable...
    for( component in position )
    {
      // this is but one way to extract (types) values from a QVariant...
      v[idx] = component.value<double>();
    }
    _baseMessengerExample->setPosition(v);
  }

If you use properties, it's often a good idea to declare the respective setters and getters as private. And here's one more thing: nothing prevents you from adding methods that just trigger some action in the wrapped object, or that even do things the object cannot do.

Use your wrapper in scripting

The wrapper for the BaseMessengerExample is finished now, so we instantiate a BaseOwnerExample module to see how the code we implemented in the previous steps works out on the Python side. Get the wrapper object with

  obj = ctx.field("outputMessenger").object()
Note the message from the wrapper init method.

You can modify and examine the underlying object with the available properties.

  obj.position = [100, 50, -2]
  if obj.shapeType == 1:
    print 'Shape type is 1'
  obj.diameter = 4.0

Base objects do not emit field notifications when changed, so your modifications will not be propagated until you force this with

  ctx.field("outputMessenger").touch()

A word on object ownership and lifetime

We can access an existing object with our wrapper, but can we also create a new one from Python? Unfortunately, the answer is no. Although we can modify the object's members, it is still owned by the ML. Transfering ownership from Python to the ML is impractical, and so objects can only be created or destroyed on the C++ side. And to be honest: a wrapper needs something to wrap anyway.

By now, you've probably played around with the wrapper in a scripting console. You might be curious when the wrapper object will be destroyed. Well, it is destroyed when the scripting context is destroyed, and also when the wrapped object changes in any way. The question of ownership becomes even more interesting when we take a look at a common scenario when working with Base types.

Wrapping lists of Base objects

Many Base types comprise a type proper and a list type. When getting the wrapper for the list type, PythonQt does not create the wrappers for the objects in the list automatically. You have to implement two wrappers for these types, one for the list and one for the type proper. The list wrapper constructor then creates the remaining object wrappers (the element wrappers) itself. For the object ownership, it is crucial that you set the parent of each object wrapper to the list wrapper.

Example code:

  MLMyTypeListWrapper::MLMyTypeListWrapper(ml::MyTypeList *list)
  {
    _list = list;

    ml::MyTypeList::iterator list_i;
    for( list_i=_list->begin(); list_i!=_list->end(); ++list_i )
    {
      MLMyTypeWrapper* x = new MLMyTypeWrapper(&*list_i);
      // IMPORTANT!
      x->setParent(this);
      _wrapperList.push_back(x);
    }
  }

The list wrapper is now the owner of the element wrappers, and the desctructor of the list wrapper has to destroy them. In the same way, you can implement methods adding and removing elements to and from the list. When you create a new element, you create a wrapper for it, and when you remove it, you remove the wrapper, too.

Wrappers for modules

Typically, wrappers are used to work with Base objects, but wrappers for classes derived from Module can be written the same way, and the same limitations on types and ownership apply.

Creating the script reference

To create Doxygen documentation for the scripting reference, add

\script 
to Doxygen blocks that describe properties or slots, and add the wrapper name in the list below. To be created by the ToolRunner or MasterBuilder, add a line like
  os.environ["MLAB_FMEwork_Release"] + "/Sources/Wrappers/MLCurveWrappers"
with the correct package and directory to pathList in MeVisLab/Resources/Documentation/Sources/SDK/ScriptingReference/filter.py.

Common pitfalls

If your wrapper doesn't work as it should, have a look at the common reasons for this:

Object wrapper scripting reference

The following object wrappers (mostly for Base object wrapping) are currently available:


Generated on Sat Sep 3 18:38:00 2011 for MeVisLab Scripting Reference by  doxygen 1.5.8