Monday, September 1, 2008

Integrating SDL with Python

Last time we embedded Lua interpreter into a simple OpenGL application and today I'm going to try the same with

I will use the same SDL skeleton, so we can concentrate only on the Python part.

Python skeleton

Embedding Python interpreter is a little more complicated than embedding Lua, so we will actually make it in three distinct steps.

1. Minimal Python
#include <Python.h>

int main(void)
{
        Py_Initialize();
        FILE *f = fopen("skeleton.py", "r");
        PyRun_SimpleFile(f, "skeleton.py");
        fclose(f);
        Py_Finalize();
}
g++ minimal.cpp -o python_minimal -lpython25

For compiling on Linux just replace -lpython25 with -lpython2.5. The library is named differently

Compile this program with Mingw on Windows and you will experience a crash. Compiling with Microsoft compiler or compiling on Linux works correctly. What's going on? After some searching, I found the answer in Blender source, which pointed me to the main page of Python embedding documentation, where it explicitly says, that the file descriptors passed to PyRun_*File function, have to be created by the same runtime libraries. Since I'm using Mingw, I had to avoid all the PyRun_*File functions and pass the Python script to the interpreter as a string.

2. Minimal Python as a String
#include <Python.h>

int main(int argc, char*argv[])
{
 Py_Initialize();

 FILE *f = fopen("skeleton.py", "r");
 fseek(f, 0, SEEK_END);
 size_t length = ftell(f);
 fseek(f, 0, SEEK_SET);
 char *data = (char *)malloc(length + 1);
 fread(data, 1, length, f);
 data[length] = '\0';
 fclose(f);

 PyRun_SimpleString(data);
 Py_Finalize();

 return 0;
}

All this code does is opening a file, reading the contents and passing it to Python interpreter, nothing really spectacular. You can place any Python code into skeleton.py such as:

print 'Hello from Python'
3. Calling C Functions from Python

To be able to call a C function from Python you have to do a little more than in case of Lua. You have to create a new module for a set of functions with Py_InitModule. In subsequent Python code you have to import from this module.

#include <Python.h>

// our function, which we call from Python
PyObject * redraw(PyObject *self, PyObject *args)
{
 double theta;
 PyArg_ParseTuple(args, "d", &theta);
 printf("%g\n", theta);

 return Py_BuildValue("");
}

// list of methods, which will put into module Foo
static PyMethodDef methods[] =
{
 { "redraw", redraw, METH_VARARGS, "updates the screen" },
 {NULL}
};

int main(int argc, char*argv[])
{
 Py_Initialize();

 FILE *f = fopen("mixed.py", "r");
 fseek(f, 0, SEEK_END);
 size_t length = ftell(f);
 fseek(f, 0, SEEK_SET);
 char *data = (char *)malloc(length+1);
 fread(data, 1, length, f);
 data[length] = '\0';
 fclose(f);

 // init our module
 PyObject *module = Py_InitModule("Foo", methods);
 PyRun_SimpleString(data);
 Py_Finalize();

 return 0;
}

When run in conjunction with this little Python script, it will print out all the rotations of the triangle, which will we display in SDL version later.

from Foo import *
i = 1
while (i < 2000):
 redraw(i)
 i += 0.5

Skeletons mixed

And now we need just to merge in the SDL code together and we're done

#include <Python.h>
#include <SDL/SDL.h>
#include <windows.h>
#include <gl/gl.h>

PyObject * redraw(PyObject *self, PyObject *args)
{
 double theta;
 PyArg_ParseTuple(args, "d", &theta);

 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 glLoadIdentity();
 glTranslatef(0.0f,0.0f,0.0f);
 glRotatef(theta, 0.0f, 0.0f, 1.0f);
 glBegin(GL_TRIANGLES);
 glColor3f(1.0f, 0.0f, 0.0f);
 glVertex2f(0.0f, 1.0f);
 glColor3f(0.0f, 1.0f, 0.0f);
 glVertex2f(0.87f, -0.5f);
 glColor3f(0.0f, 0.0f, 1.0f);
 glVertex2f(-0.87f, -0.5f);
 glEnd();

 SDL_GL_SwapBuffers();
 return Py_BuildValue("");
}

static PyMethodDef methods[] =
{
 { "redraw", redraw, METH_VARARGS, "updates the screen" },
 {NULL}
};

int main(int argc, char*argv[])
{
 atexit(SDL_Quit);
 if( SDL_Init(SDL_INIT_VIDEO) < 0 ) {
  fprintf(stderr,"Couldn't initialize SDL: %s\n", SDL_GetError());
  exit(1);
 }

 SDL_Surface *screen = SDL_SetVideoMode(400, 400, 32, SDL_DOUBLEBUF | SDL_HWSURFACE | SDL_OPENGL);

 glViewport(0, 0, 400, 400);
 glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
 glClearDepth(1.0);
 glDepthFunc(GL_LESS);
 glEnable(GL_DEPTH_TEST);
 glShadeModel(GL_SMOOTH);
 glMatrixMode(GL_PROJECTION);
 glMatrixMode(GL_MODELVIEW);

 Py_Initialize();

 FILE *f = fopen("mixed.py", "r");
 fseek(f, 0, SEEK_END);
 size_t length = ftell(f);
 fseek(f, 0, SEEK_SET);
 char *data = (char *)malloc(length+1);
 fread(data, 1, length, f);
 data[length] = '\0';
 fclose(f);

 PyObject *module = Py_InitModule("Foo", methods);
 PyRun_SimpleString(data);
 Py_Finalize();

 return 0;
}

The result, we're getting, is of course the same as last time

Colorful triangle drawn by OpenGL called from Python

In this case as well I couldn't find any statistically significant speed difference between native C and interpreted Python.

All sources and binaries can be downloaded here 4 MB

Very interesting side note: when implementing this example I found out an extremely awesome thing. New versions of Mingw allow linking directly to DLLs. You don't need no .a, .la or .lib files. You can just direct the compiler/linker to the directory with the DLL you're referencing and it will automagically resolve the symbols from within the DLL - simply AWESOME!!!

0 comments: