MeVisLab Scripting Reference
Object Wrappers

Overview

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
MLAB_PACKAGES += MeVisLab_IDE

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. Although Qt supports arbitrary types in its properties, PythonQt only supports a useful subset. In case of QList<double>, we have to use QVariant (or QVariantList) as the property type 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? For simple ml::Base derived objects, the answer is no. For objects derived from ml::RefCountedBase, it becomes possible to create an instance and the wrapper for it using Python only. Such RefCountedBase derived objects can be created using MLAB.createMLBaseObject("Classname", [arg1, arg2, ...]). Examples for ml::RefCountedBase derived classes are WEM, CSOList and RemoteCallInterface.

In case of a RefCountedBase derived class, you need to derive your wrapper from the MLRefCountedBaseWrapper class instead of QObject, which handles ref-counting of the base object for you. The following code shows how to do this:

An example RefCountedBase derived class:

class RemoteCallInterface : public RefCountedBase
{
public:
static ml::RefCountedBase* create(const QVariantList& args) {
return new RemoteCallInterface();
}
...
};

The wrapper for it:

#include "Fields/mlRefCountedBaseWrapper.h"
class MLRemoteCallInterfaceWrapper : public MLRefCountedBaseWrapper
{
Q_OBJECT
public:
MLRemoteCallInterfaceWrapper(ml::RemoteCallInterface* object):MLRefCountedBaseWrapper(object)
{ ... };
Script wrapper for RemoteCallInterface Offers to call methods on remote objects and to register local...
Definition: mlRemoteCallInterfaceWrapper.h:46

Registration in the init code of your library:

MLAB_REGISTER_OBJECT_WRAPPER_AND_CONSTRUCTOR("RemoteCallInterface", ml::RemoteCallInterface, MLRemoteCallInterfaceWrapper, ml::RemoteCallInterface::create);

Returning other wrappers/QObjects

It is possible to use/return QObject derived types from your wrapper's slots, but you need to register these types using:

MLAB_REGISTER_SCRIPTABLE_QOBJECT_CLASS(YourQObjectDerivedClass);

When you return such an object, make sure that it either has a QObject as its parent (so the ownership is handled by the parent), or that you mark the object as disposable before you return it:

MLABScriptContextFactory::markObjectDisposable(yourObject);

Marking the object as disposable will allow PythonQt to garbage collect it when it goes out of scope. When you return such objects, make sure that they do NOT contain a reference to the base object which can be destroyed any time. If you need such a reference, make sure that you clear that reference when the base wrapper is destroyed, which effectively means that you need to track the returned QObjects in your wrapper.

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 

(remove the space between the backslash and 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:

  • You forgot the .def file. Without it, MeVisLab doesn't know about the wrapper at all.
  • "Reload Updated Shares Libraries" will typically crash when you reload the wrapper lib while you're already using it somewhere.
  • Not all types can be converted automatically by PythonQt. For example, a method accepting a QStringList will happily take any Python list with numbers or strings in it, but a QList<float> will throw errors when you pass it a Python list with strings. If getting or setting values does not work as you intended, think again about what the involved types are, or just use a QVariantList and move the type checks inside the wrapper.
  • PythonQt does not support all types that Q_PROPERTY does, if you encounter a problem, try using QVariant or QVariantList/Map instead.

Object wrapper scripting reference

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