SWIG vs Boost.Python

pkdawson

07-09-2006 05:41:01

This doesn't necessarily have anything to do with PyOgre, but I thought it might be a good place to ask anyway.

My current design is a small C++ 'core' with all the game logic, UI stuff, etc. in Python. I don't have much experience with this, so I started by using SWIG. It works very well for simple situations. Then I decided to compare it with Boost.Python. I didn't get very far before running into a problem with getting virtual functions to work properly. With SWIG this all works out automagically by specifiying (directors="1"). With Boost, it seems I have to manually write 'wrapper' classes, which is fairly tedious and absurd.

So what's the best option for writing a C++ extension to Python, which will use Ogre, OIS, various other libraries, and possibly GOOF in the future? Is Boost.Python usable if I do it with Pyste? How about Py++?

OvermindDL1

07-09-2006 16:09:26

I use boost::python, but that is because I like having control. Boost::python is still rather low level and does not have a generator (have you tried writing swig without the generator, it is hell compared to boost::python). pyplusplus is one such generator for boost::python (its not py++, but pyplusplus). Pyste is ancient and near useless, don't touch it. But don't compare something low level like b::p with swig, if you compare generators, like pyplusplus and swig, then do so. :)

I've wrapped a good chunk of Ogre manually using boost::python, it is not anywhere near as hard as any other low level way, very simple actually. Here is some copied boost::python binding code from one of my projects:

void initOgrePart1(void)
{
using namespace boost::python;

class_<Ogre::FrameEvent>("Ogre_FrameEvent")
.def_readonly("timeSinceLastEvent", &Ogre::FrameEvent::timeSinceLastEvent, "Elapsed time in seconds since the last event. This gives you time between frame start & frame end, and between frame end and next frame start.\nThis may not be the elapsed time but the average elapsed time between recently fired events.")
.def_readonly("timeSinceLastFrame", &Ogre::FrameEvent::timeSinceLastFrame, "Elapsed time in seconds since the last event of the same type, i.e. time for a complete frame.\nThis may not be the elapsed time but the average elapsed time between recently fired events of the same type.")
;

class_<Ogre::Radian>("Radian") // TODO: write-me
;

class_<Ogre::Degree>("Degree") // TODO: write-me
;

class_<Ogre::Quaternion>("Quaternion")
.def(init<Ogre::Real, Ogre::Real, Ogre::Real, Ogre::Real>())
.def(init<const Ogre::Quaternion&>())
.def(init<const Ogre::Matrix3&>())
.def(init<const Ogre::Radian&, const Ogre::Vector3&>())
.def(init<const Ogre::Vector3&, const Ogre::Vector3&, const Ogre::Vector3&>())
.def(self + self)
.def(self - self)
.def(self * Ogre::Real())
.def(Ogre::Real() * self)
.def(self * self)
// .def(str(self))
.def("FromRotationMatrix", &Ogre::Quaternion::FromRotationMatrix)
.def("ToRotationMatrix", &Ogre::Quaternion::ToRotationMatrix)
.def("FromAngleAxis", &Ogre::Quaternion::FromAngleAxis)
.def("ToAngleAxis", &ToAngleAxis1)
.def("ToAngleAxis", &ToAngleAxis2)
.def("FromAxes", &FromAxes1)
.def("ToAxes", &ToAxes1)
.def("xAxis", &Ogre::Quaternion::xAxis)
.def("yAxis", &Ogre::Quaternion::yAxis)
.def("zAxis", &Ogre::Quaternion::zAxis)
.def("Dot", &Ogre::Quaternion::Dot)
.def("Norm", &Ogre::Quaternion::Norm)
.def("normalise", &Ogre::Quaternion::normalise)
.def("Inverse", &Ogre::Quaternion::Inverse)
.def("UnitInverse", &Ogre::Quaternion::UnitInverse)
.def("Exp", &Ogre::Quaternion::Exp)
.def("Log", &Ogre::Quaternion::Log)
.def("getRoll", &Ogre::Quaternion::getRoll)
.def("getPitch", &Ogre::Quaternion::getPitch)
.def("getYaw", &Ogre::Quaternion::getYaw)
.def("equals", &Ogre::Quaternion::equals)
.def("Slerp", &Ogre::Quaternion::Slerp)
.def("SlerpExtraSpins", &Ogre::Quaternion::SlerpExtraSpins)
.def("Intermediate", &Ogre::Quaternion::Intermediate)
.def("Squad", &Ogre::Quaternion::Squad)
.def("nlerp", &Ogre::Quaternion::nlerp)
.add_static_property("ms_fEpsilon", make_getter(Ogre::Quaternion::ms_fEpsilon))
.add_static_property("ZERO", make_getter(Ogre::Quaternion::ZERO))
.add_static_property("IDENTITY", make_getter(Ogre::Quaternion::IDENTITY))
.def_readwrite("w", &Ogre::Quaternion::w)
.def_readwrite("x", &Ogre::Quaternion::x)
.def_readwrite("y", &Ogre::Quaternion::y)
.def_readwrite("z", &Ogre::Quaternion::z)
;

// TODO: Should probably wrap vector3 to allow things like a triple tuple for creation and such, hmm, actually, maybe make a converter...
// Yes, not putting an init here since it is also default constructable
class_<Ogre::Vector3>("Vector3", "Standard 3-dimensional vector.\n@remarks\n A direction in 3D space represented as distances along the 3\n orthoganal axes (x, y, z). Note that positions, directions and\n scaling factors can be represented by a vector, depending on how\n you interpret the values.")
.def(init<const Ogre::Real, const Ogre::Real, const Ogre::Real>())
.def(init<const Ogre::Real>())
.def(init<const Ogre::Vector3&>())
.def(self + self)
.def(self + Ogre::Real())
.def(Ogre::Real() + self)
.def(self - self)
.def(self - Ogre::Real())
.def(Ogre::Real() - self)
.def(self * Ogre::Real())
.def(Ogre::Real() * self)
.def(self * self)
.def(self / Ogre::Real())
// .def(Ogre::Real() / self) // NOTE: would need a converter for the real, oddly enough...
.def(self / self)
.def(-self)
.def(self += Ogre::Real())
.def(self += self)
.def(self -= Ogre::Real())
.def(self -= self)
.def(self *= Ogre::Real())
.def(self *= self)
.def(self /= Ogre::Real())
.def(self /= self)
.def(self < self)
.def(self > self)
// .def(str(self))
.def("length", &Ogre::Vector3::length, "Returns the length (magnitude) of the vector.\n@warning\n This operation requires a square root and is expensive in\n terms of CPU operations. If you don't need to know the exact\n length (e.g. for just comparing lengths) use squaredLength()\n instead.")
.def("squaredLength", &Ogre::Vector3::squaredLength, "Returns the square of the length(magnitude) of the vector.\n@remarks\n This method is for efficiency - calculating the actual\n length of a vector requires a square root, which is expensive\n in terms of the operations required. This method returns the\n square of the length of the vector, i.e. the same as the\n length but before the square root is taken. Use this if you\n want to find the longest / shortest vector without incurring\n the square root.")
.def("dotProduct", &Ogre::Vector3::dotProduct, "Calculates the dot (scalar) product of this vector with another.\n@remarks\n The dot product can be used to calculate the angle between 2\n vectors. If both are unit vectors, the dot product is the\n cosine of the angle; otherwise the dot product must be\n divided by the product of the lengths of both vectors to get\n the cosine of the angle. This result can further be used to\n calculate the distance of a point from a plane.\n@param\n vec Vector with which to calculate the dot product (together\n with this one).\n@returns\n A float representing the dot product value.")
.def("normalise", &Ogre::Vector3::normalise, "Normalises the vector.\n@remarks\n This method normalises the vector such that it's\n length / magnitude is 1. The result is called a unit vector.\n@note\n This function will not crash for zero-sized vectors, but there\n will be no changes made to their components.\n@returns\n The previous length of the vector.")
.def("crossProduct", &Ogre::Vector3::crossProduct, "Calculates the cross-product of 2 vectors, i.e. the vector that\nlies perpendicular to them both.\n@remarks\n The cross-product is normally used to calculate the normal\n vector of a plane, by calculating the cross-product of 2\n non-equivalent vectors which lie on the plane (e.g. 2 edges\n of a triangle).\n@param\n vec Vector which, together with this one, will be used to\n calculate the cross-product.\n@returns\n A vector which is the result of the cross-product. This\n vector will <b>NOT</b> be normalised, to maximise efficiency\n - call Vector3::normalise on the result if you wish this to\n be done. As for which side the resultant vector will be on, the\n returned vector will be on the side from which the arc from 'self'\n to rkVector is anticlockwise, e.g. UNIT_Y.crossProduct(UNIT_Z)\n = UNIT_X, whilst UNIT_Z.crossProduct(UNIT_Y) = -UNIT_X.\n This is because OGRE uses a right-handed coordinate system.\n@par\n For a clearer explanation, look a the left and the bottom edges\n of your monitor's screen. Assume that the first vector is the\n left edge and the second vector is the bottom edge, both of\n them starting from the lower-left corner of the screen. The\n resulting vector is going to be perpendicular to both of them\n and will go <i>inside</i> the screen, towards the cathode tube\n (assuming you're using a CRT monitor, of course).")
.def("midPoint", &Ogre::Vector3::midPoint, "Returns a vector at a point half way between this and the passed\nin vector.")
.def("makeFloor", &Ogre::Vector3::makeFloor, "Sets this vector's components to the minimum of its own and the\nones of the passed in vector.\n@remarks\n 'Minimum' in this case means the combination of the lowest\n value of x, y and z from both vectors. Lowest is taken just\n numerically, not magnitude, so -1 < 0.")
.def("makeCeil", &Ogre::Vector3::makeCeil, "Sets this vector's components to the maximum of its own and the\nones of the passed in vector.\n@remarks\n 'Maximum' in this case means the combination of the highest\n value of x, y and z from both vectors. Highest is taken just\n numerically, not magnitude, so 1 > -3.")
.def("perpendicular", &Ogre::Vector3::perpendicular, "Generates a vector perpendicular to this vector (eg an 'up' vector).\n@remarks\n This method will return a vector which is perpendicular to this\n vector. There are an infinite number of possibilities but this\n method will guarantee to generate one of them. If you need more\n control you should use the Quaternion class.")
.def("randomDeviant", &randomDeviant1, "Generates a new random vector which deviates from this vector by a\ngiven angle in a random direction.\n@remarks\n This method assumes that the random number generator has already\n been seeded appropriately.\n@param\n angle The angle at which to deviate\n@param\n up Any vector perpendicular to this one (which could generated\n by cross-product of this vector and any other non-colinear\n vector). If you choose not to provide this the function will\n derive one on it's own, however if you provide one yourself the\n function will be faster (this allows you to reuse up vectors if\n you call this method more than once)\n@returns\n A random vector which deviates from this vector by angle. This\n vector will not be normalised, normalise it if you wish\n afterwards.")
.def("randomDeviant", &Ogre::Vector3::randomDeviant, "Generates a new random vector which deviates from this vector by a\ngiven angle in a random direction.\n@remarks\n This method assumes that the random number generator has already\n been seeded appropriately.\n@param\n angle The angle at which to deviate\n@param\n up Any vector perpendicular to this one (which could generated\n by cross-product of this vector and any other non-colinear\n vector). If you choose not to provide this the function will\n derive one on it's own, however if you provide one yourself the\n function will be faster (this allows you to reuse up vectors if\n you call this method more than once)\n@returns\n A random vector which deviates from this vector by angle. This\n vector will not be normalised, normalise it if you wish\n afterwards.")
.def("getRotationTo", &getRotationTo1, "Gets the shortest arc quaternion to rotate this vector to the destination\n vector.\n@remarks\n If you call this with a dest vector that is close to the inverse\n of this vector, we will rotate 180 degrees around the 'fallbackAxis'\n (if specified, or a generated axis if not) since in this case\n ANY axis of rotation is valid.")
.def("getRotationTo", &Ogre::Vector3::getRotationTo, "Gets the shortest arc quaternion to rotate this vector to the destination\n vector.\n@remarks\n If you call this with a dest vector that is close to the inverse\n of this vector, we will rotate 180 degrees around the 'fallbackAxis'\n (if specified, or a generated axis if not) since in this case\n ANY axis of rotation is valid.")
.def("isZeroLength", &Ogre::Vector3::isZeroLength, "Returns true if this vector is zero length.")
.def("normalisedCopy", &Ogre::Vector3::normalisedCopy, "As normalise, except that this vector is unaffected and the\nnormalised vector is returned as a copy.")
.def("reflect", &Ogre::Vector3::reflect, "Calculates a reflection vector to the plane with the given normal.\n@remarks\n NB assumes 'self' is pointing AWAY FROM the plane, invert if it is not.")
.def("positionEquals", &positionEquals1, "Returns whether this vector is within a positional tolerance\nof another vector.@param rhs The vector to compare with\n@param tolerance The amount that each element of the vector may vary by\n and still be considered equal")
.def("positionEquals", &Ogre::Vector3::positionEquals, "Returns whether this vector is within a positional tolerance\nof another vector.@param rhs The vector to compare with\n@param tolerance The amount that each element of the vector may vary by\n and still be considered equal")
.def("directionEquals", &Ogre::Vector3::directionEquals, "Returns whether this vector is within a directional tolerance\nof another vector.\n@param rhs The vector to compare with\n@param tolerance The maximum angle by which the vectors may vary and\n still be considered equal")
.def_readwrite("x", &Ogre::Vector3::x)
.def_readwrite("y", &Ogre::Vector3::y)
.def_readwrite("z", &Ogre::Vector3::z)
.add_static_property("ZERO", make_getter(Ogre::Vector3::ZERO))
.add_static_property("UNIT_X", make_getter(Ogre::Vector3::UNIT_X))
.add_static_property("UNIT_Y", make_getter(Ogre::Vector3::UNIT_Y))
.add_static_property("UNIT_Z", make_getter(Ogre::Vector3::UNIT_Z))
.add_static_property("NEGATIVE_UNIT_X", make_getter(Ogre::Vector3::NEGATIVE_UNIT_X))
.add_static_property("NEGATIVE_UNIT_Y", make_getter(Ogre::Vector3::NEGATIVE_UNIT_Y))
.add_static_property("NEGATIVE_UNIT_Z", make_getter(Ogre::Vector3::NEGATIVE_UNIT_Z))
.add_static_property("UNIT_SCALE", make_getter(Ogre::Vector3::UNIT_SCALE))
;


And a ton more, I have the documentation on some, not on others, etc... etc... It averages one line of code per bound function, and if I don't deal with the documentation then it is quite a short line at that. Only time I really need to write wrapper functions is if there are non-patterned overloads. And of course, need to write a very simple wrapper class if I intend to subclass. The wrapper class also just averages two lines of code per method, and those two lines can just be copy/pasted replacing a single word in each one with the name of the function, as such:

// if it is a non-abstract method:
if(py::override o = this->get_override("thisMethodName"))
o();

// if it *is* an abstract method (*has* to be defined then):
this->get_override("thisMethodName")();

pkdawson

07-09-2006 19:57:43

pyplusplus is one such generator for boost::python (its not py++, but pyplusplus).

Nope, they changed it as of 0.8.1.

The wrapper class also just averages two lines of code per method, and those two lines can just be copy/pasted replacing a single word in each one with the name of the function

Exactly!! It's tedious and absurd to expect from a human, because it's exactly the kind of thing a computer should do for you (like SWIG does). Being able to derive a subclass in Python, override a virtual method, and have it called properly from the C++ code is the entire point for me. I don't want to have to manually write wrapper classes for everything. It's needless clutter. I was initially attracted to Boost.Python because it didn't require the extra build step and special configuration files that SWIG does, but I guess that's unavoidable for me. I'm still wrestling with GCC-XML, so I haven't really tried Py++ yet.

OvermindDL1

07-09-2006 22:53:09

In my project I have the python wrapper class as part of the base class anywhere so it is actually far more simple for me, not to mention that I can keep things non-virtual on the C++ if I only want it from inside python to be subclassed so there is a slight speed boost too.

pkdawson

08-09-2006 18:50:06

Okay, that's a very interesting idea. So you just add those lines at the beginning of the method?

So if I do something like...
#define PYTHON_VIRTUAL(x) if (py::override o = this->get_override(x)) return o();

class Foo
{
virtual int bar()
{
PYTHON_VIRTUAL("bar");
return 87;
}
}


That's a lot more reasonable. I'd go a step further and use the __FUNCTION__ macro too, but that would give the string "Foo::bar" rather than "bar".

pkdawson

08-09-2006 19:46:50

I'm sold. I like how I can understand exactly what Boost is doing and adjust what I need to.

Just one problem. I'm trying to wrap a couple simple classes from OIS. Easy enough, except for this one:
class _OISExport MouseEvent : public EventArg
{
public:
MouseEvent( Object *obj, unsigned int ts, const MouseState &ms )
: EventArg(obj,ts), state(ms) {}
virtual ~MouseEvent() {}

//! The state of the mouse - including buttons and axes
const MouseState &state;
};


I'm not sure how to handle state, since it's a reference. I tried this:
class_<OIS::MouseEvent>("MouseEvent", no_init)
.def_readonly("state", &OIS::MouseEvent::state);


Which results in a compiler crash:
1>.\main.cpp(33) : error C2634: '&OIS::MouseEvent::OIS::MouseEvent::state' : pointer to reference member is illegal
1> C:\OIS_SDK\includes\OISMouse.h(86) : see declaration of 'OIS::MouseEvent::state'
1>.\main.cpp(33) : fatal error C1001: An internal error has occurred in the compiler.


Removing the & makes the compiler think I'm trying to use a static variable. I can work around this problem, but a solution would be nice.

Thanks for all your help!

OvermindDL1

12-09-2006 02:27:20

Sorry, been busy the past few days. I don't have the code in front of me (I have a nice quick sheet slash cpp file of odd things like that), but as I recall just write a quick getter function and do it like this:

const MouseState &MouseEvent_stateGetter(MouseEvent *event) { return event->state; }

class_<MouseEvent>("MouseEvent", no_init) // no_init because when should this be created outside of OIS?
.add_property("state", &MouseEvent_stateGetter)
;

There might (read: probably is a) be a better way, that is just off the top of my head.
If you have GTalk, I'm OvermindDL1 at gmail and I can help in real-time.