views:

109

answers:

1

Hi. I am struggling a bit with the Python C API. I am calling a python method to do some game AI at about 60hz. It works most of the time but every second or so the call to PyEval_CallObject results in a NULL return value. If I correctly detect the error and continue looping, all is well for the next second or so, whereupon the error occurs again.

I suspect I am doing something wrong with ref counting but I can't figure out what it is:

int script_do_ai(struct game_data_t* gd)
{

    PyObject *pAiModule, *pResult;

    float result=0.0;
    pResult = NULL;

    pAiModule = PyImport_Import(PyString_FromString("ai_script"));

Yeah, I'm importing the the module every iteration. Is that necessary? If I store pAiModule as a global, I get a hard crash after about a second.

    pResult = PyEval_CallObject(PyObject_GetAttrString(pAiModule, "do_ai"),
                               Py_BuildValue("f", gd->important_float))  
    if (pResult != NULL)
    {       
        PyArg_Parse(pResult, "f", &result);
        Py_DECREF(pResult);
        ConquerEnemies(result);  //you get the idea
    }
    else  //this happens every 75 or so iterations thru the loop
    {
       if (PyErr_ExceptionMatches(PyExc_SomeException))  //? not sure what to do here
       {

I haven't been able to find out how to extract the exception yet, either...without testing for every exception

       }
    }

Am I even close to doing this right? Like I said, it mostly works but I'd really like to understand why I am getting an error.

Thank you in advance for any help.

+3  A: 

You can call PyImport_Import() as often as you like, but you'll just keep getting the same module object back. Python caches imports. Also, instead of creating a new Python string and leaking the reference (and thus the object), you should just use PyImport_ImportModule(), which takes a const char *.

PyImport_Import*() return a new reference, though, you should call Py_DECREF() on it when you're done. Storing the module in a global should not be a problem, as long as you own a reference to it (which you do, here.)

In your call to PyEval_CallObject() you aren't checking the result of Py_BuildValue() for errors, and you're also not calling Py_DECREF() when you're done with it, so you're leaking that object as well.

In order to convert a Python float to a C double, you should probably just call PyFloat_AsDouble() instead of mucking about with PyArg_Parse() (and keep in mind to test for exceptions)

Down to the actual error handling: PyErr_ExceptionMatches() is only useful when you actually want to test if the exception matches something. If you want to know if an exception occurred, or get the actual exception object, PyErr_Occurred() is what you should call. It returns the current exception type (not the actual exception object) as a borrowed reference, or NULL if none is set. If you want to just print a traceback to stderr, PyErr_Print() and PyErr_Clear() are what you want to use. For more fine-grained inspection of the actual error in your code, PyErr_Fetch() gets you the current exception object and the traceback associated with it (it gets you the same information as sys.exc_info() in Python code.) All things considered you rarely want to get that deeply into the exception handling in C code.

Thomas Wouters
+1 thanks...I figured if I inlined the call to Py_BuildValue it would take care of the leaky reference but perhaps not.I'll see what PyErr_Fetch gives me on pyErr_Occurred
Nick Monkman
Alas, C is not garbage collected :) PyErr_Print() is quite convenient for printing exceptions, especially when debugging cases like this.
Thomas Wouters
Yeah too much time in .NET-land for me.Okay weird. I added "sys.stdout = open('CONOUT$', 'wt')" to my python script (to output to console...I'm in Win32) and everything started magically working. Bizarre.
Nick Monkman
FYI...I fixed the bug. Turns out the loop I was running was not sync'd to 60hz...it was running the cpu at 100%. I threw in a 10ms non-busy wait and all appears to be well. It at least explains why adding console output 'fixed' the problem before...it was a timing issue. Couldn't have solved it without the above tips though...thanks!
Nick Monkman