Interfacing C++ and Python (Part 1)

Published April 17, 2011
Advertisement
I've recently decided it was time to gain a bit more flexibility in my c++ codebase by adding support for a scripting interface. My initial use case was to enable my homegrown gui system (in c++) to offload callbacks and general customization to a scripting layer, rather than having to hard code a user interface in C++. I chose Python as my scripting language, due to my years of experience with this language.

With the GUI/addon system use case in mind, I need my C++ code to call into a Python script/module and I also need my Python scripts to interact with C++ objects. Here's a simple example that I've got in mind for this process:


# import the pyd that interfaces python with my c++ library
from az_gui import *

...

def onCancelButton(self, event):
'''
Event handler for cancel button.
'''
window = self.getWindow()
window.close()

return True



I've decided to split this topic into several postings. This first post covers the ground work necessary for calling into Python scripts. The second posting will review a PythonCallback wrapper class that is designed to encapsulate the Python C API code for calling function callbacks in python modules. After that I'll have a post or two on my Boost.Python experiences that expose my C++ libraries to Python (e.g., allowing for Python code to call the C++ class, Window as shown in the example above).


C++ calls into Python scripts:

Went with the Python C API for communication in this direction, since it was a manageable process. I'm resistant to adding 3rdParty dependencies to my project, so this seemed like the best I could do. On a side note, my original goal was to write my own Python C API wrappers to also exposing my c++ classes to Python scripts, but that panned out to be an enormous undertaking that I'm not ready to tackle yet. (this discussion is saved for another post).

There were a few catches along the way that made the implementation less straightfoward. For starters, a standard install of Python does not have a debug library. As such, some macro magic needs to be performed to include Python.h header file.


//Note
// Since Python may define some pre-processor definitions which affect
// the standard headers on some systems, you must include Python.h before
// any standard headers are included.

#ifdef _DEBUG
#undef _DEBUG
#include "Python.h"
#define _DEBUG
#else
#include "Python.h"
#endif


It is my preference to keep this Python.h ugliness inside the cpp files, so my next trick involved forward declaring PyObject:


// forward declare PyObject
// as suggested on the python mailing list
// http://mail.python.org/pipermail/python-dev/2003-August/037601.html
#ifndef PyObject_HEAD
struct _object;
typedef _object PyObject;
#endif


Pulling it all together:
To manage the Python C API, I created a PythonManager class to encapsulate the Python interpreter session used within my codebase.

PythonManager.h

...

class PythonManager
{
public:
PythonManager();
virtual ~PythonManager();

void initialize();
void uninitialize();

// add directories to the python interpreter's sys.path
// to allow for Python scripts to locate script directories.
bool addSysPath(const std::string& relativePath);
protected:
};


PythonManger.cpp:

...

PythonManager::PythonManager()
{
}

PythonManager::~PythonManager()
{
uninitialize();
}

bool PythonManager::addSysPath(const std::string& relativePath)
{
std::ostringstream ss;

ss << "import os\n";
ss << "import sys\n";
ss << "olderr = sys.stderr\n";
ss << "oldout = sys.stdout\n";
ss << "sys.stdout = open('pythonOut.txt','w')\n";
ss << "print 'cwd is %s' % os.getcwd()\n";
ss << "print 'adding relative path to sys.path: %s' % '" << relativePath.c_str() << "'\n";
ss << "sys.path.append(os.path.join(os.getcwd(), '" << relativePath << "'))\n";
ss << "sys.stderr = olderr\n";
ss << "sys.stdout = oldout\n";

int retval = PyRun_SimpleString(ss.str().c_str());

if (retval != 0)
{
PyErr_Print();
}

return retval == 0;

}

void PythonManager::initialize()
{
Py_Initialize();
}

void PythonManager::uninitialize()
{
Py_Finalize();
}



Finally, this PythonManager is called from outside my "main loop":


PythonManager* python = new PythonManager();
python->initialize();
python->addSysPath("../scripts");


Check back for follow on summaries on the remaining implementation details (PythonCallback wrapper class, and Boost Python).
0 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement