1

I'm trying to use Python to open a dialog to accept input into my C++ application.

Here is a very minimal representation of what I am trying to do:

#include <iostream>
#include <Python.h>

int main()
{
    /* Begin Python Ititialization - only needs to be done once. */
    PyObject *ip_module_name = NULL;
    PyObject *ip_module = NULL;
    PyObject *ip_module_contents = NULL;
    PyObject *ip_module_getip_func = NULL;

    Py_Initialize();
    PyEval_InitThreads();

    ip_module_name     = PyString_FromString( "get_ip" );
    ip_module          = PyImport_Import( ip_module_name );
    ip_module_contents = PyModule_GetDict( ip_module );
    ip_module_getip_func = PyDict_GetItemString( ip_module_contents, "get_ip_address" );
    /* End Initialization */

    PyGILState_STATE state = PyGILState_Ensure();
    PyObject *result = PyObject_CallObject( ip_module_getip_func, NULL );

    if( result == Py_None )
        printf( "None\n" );
    else
        printf( "%s\n", PyString_AsString( result ) );

    PyGILState_Release( state );

    /* This is called when the progam exits. */
    Py_Finalize();
}

However, when I call the function with PyObject_CallObject, the app segfaults. I'm guessing that it's because I'm using the Tk library. I've tried linking my app against _tkinter.lib, tk85.lib, tcl85.lib, tkstub85.lib, tclstub85.lib and none of that helps. I'm pretty stumped...

Here's the script:

import Tkinter as tk
from tkSimpleDialog import askstring
from tkMessageBox import showerror

def get_ip_address():

    root = tk.Tk()
    root.withdraw()

    ip = askstring( 'Server Address', 'Enter IP:' )

    if ip is None:
        return None

    ip = ip.strip()

    if ip is '':
        showerror( 'Error', 'Please enter a valid IP address' )
        return get_ip_address()

    if len(ip.split(".")) is not 4:
        showerror( 'Error', 'Please enter a valid IP address' )
        return get_ip_address()

    for octlet in ip.split("."):
        x = 0

        if octlet.isdigit():
            x = int(octlet)
        else:
            showerror( 'Error', 'Please enter a valid IP address' )
            return get_ip_address()

        if not ( x < 256 and x >= 0 ):
            showerror( 'Error', 'Please enter a valid IP address' )
            return get_ip_address()

    return ip

Edit: added my threading setup

Neil
  • 466
  • 1
  • 6
  • 15
  • Does it segfault immediately, without doing anything? Does your C code use threads? – user4815162342 Apr 25 '13 at 06:29
  • It segfaults right at root = tk.Tk(). If I remove the windowing code and set ip to something like "127.0.0.1", it works as intended. – Neil Apr 25 '13 at 06:30
  • I use threads, is there something I have to do before calling my function in addition to setting up the tread stuff (PyEval_InitThreads(), etc after PyInitialize())? – Neil Apr 25 '13 at 06:32
  • `Launcher::getIP` needs to call `PyGILState_Ensure` before calling any Python/C functions or macros, and `PyGILState_Release` before returning to C++. See [the documentation](http://docs.python.org/2/c-api/init.html#non-python-created-threads) for details. – user4815162342 Apr 25 '13 at 07:00
  • Okay, that makes sense, but when I add it, the program just hangs when it reaches that function :/ – Neil Apr 25 '13 at 07:10
  • Presumably someone else is not releasing the GIL. Your thread initialization code looks suspicious — what's wrong with just calling `PyEval_InitThreads()` right after `Py_Initialize`? After that, the GIL will be acquired, and Python will automatically release it when the time comes. You only need to worry about the GIL when entering Python, i.e. when called from non-Python C code. That code needs to to acquire the GIL with `PyGILState_Ensure` and release it with `PyGILState_Release`. – user4815162342 Apr 25 '13 at 08:52
  • Okay, I did all that, and I put a cut-down example in the question post rather than snippets. It still didn't work. It seems to be failing on the assignment of result. If I remove 'result =', the code runs, but the function doesn't run... it just skips the call (I was choosing not to assign the output to a variable for testing when I saw this) – Neil Apr 25 '13 at 14:15
  • found out that PyObject_CallObject is returning NULL (C NULL aka 0, not Python None). Looks like the script fails when running "root = tk.Tk()" – Neil Apr 25 '13 at 17:07
  • Whenever something returns `NULL`, at least call `PyErr_Print()` to see what went wrong. The exception will make it clear **why** the failure occurred. This is how I debugged it. – user4815162342 Apr 25 '13 at 17:14

1 Answers1

3

Add PySys_SetArgv(argc, argv) (along with int argc, char **argv parameters to main), and your code will work.

tk.Tk() accesses sys.argv, which doesn't exist unless PySys_SetArgv has been called. This causes an exception which gets propagated out of get_ip and reported to Python/C by PyObject_CallObject returning NULL. The NULL gets stored to result and passed to PyString_AsString, which is the immediate cause of the observed crash.

Several remarks on the code:

  • It took effort to debug this because the code does no error checking whatsoever, it blindly presses forward until it crashes due to passing NULL pointers around. The least one can do is write something like:

    if (!ip_module_name) {
        PyErr_Print();
        exit(1);
    }
    // and so on for every PyObject* that you get from a Python API call
    

    In real code you wouldn't exit(), but do some cleanup and return NULL (or raise a C++-level exception, or whatever is appropriate).

  • There is no need to call PyGILState_Ensure in the thread that you already know holds the GIL. As the documentation of PyEval_InitThreads states, it initializes the GIL and acquires it. You only need to re-acquire the GIL when calling into Python from a C callback that comes from, say, the toolkit event loop that has nothing to do with Python.

  • New references received from Python need to be Py_DECREF'ed once no longer needed. Reference counting might be omitted from the minimal example for brevity, but it should always be minded.

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • 1
    @nbsdx No problem. Please note the additional remarks on code style — the bug would have been much easier to catch with good coding practices. In any case, good luck hacking Python/C! – user4815162342 Apr 25 '13 at 17:12
  • Yeah, I was in a hurry, and hadn't worked with Python or the Python/C bindings before. I agree 100% with the code style comments. I should be doing error checking, I was just in a hurry haha. Thanks again :) – Neil Apr 25 '13 at 17:22