2

I am executing the following Python function from C:

Python:

def runLoop(self):
    try:
        while True:
            print("working")
            time.sleep(1)  # Delay for 1 second.
            self.incrementcounter()  
    except:
        print("caught exception")

C:

//Here we register traceFunction to give us a hook into the python 
PyEval_SetProfile((Py_tracefunc)traceFunction, NULL);

//Abort interrupt reset to false
m_interruptRequest = false;

//Call the runLoop method
const char *szName = "runLoop";

PyObject_CallMethod(m_pyObject, szName, nullptr);

In the middle of this loop, I would like to inject an exception from C to abort the loop, but also handle the exception in the try/except in Python. As mentioned in the comments I am registering a profiling function in C called tracer be able to inject an exception from C. The following code below injects an exception and kills the program because it is not caught in the try/except.

//Tracer function callback hook into Python execution. Here we handle pause, resume and abort requests.

void PythonRunner::tracer(void)
{
    ...
    //Abort requested
    else if (true == interruptRequested())
    {
        PyErr_SetString(PyExc_Exception, "ABORT");


    }
}

Does anyone know how I can throw an exception from C to be handled in Python? Thanks

Update: With the help of @zwol and this article stackoverflow.com/questions/1420957/stopping-embedded-python I found a solution. The key was to add a pending call to python e.g

    int PythonRunner::tracer(PyObject *, _frame *, int, PyObject *)
    {
        ...
        //Abort requested
        else if (true == _instance->interruptRequested())
        {
            Py_AddPendingCall(&PythonRunner::raiseException, NULL);
        }
    return 0;
   }

int PythonRunner::raiseException(void *)
{
    PyErr_SetString(PyExc_KeyboardInterrupt, "Abort");
    return -1;
}
Michael Japzon
  • 201
  • 1
  • 2
  • 10

1 Answers1

2

In general, to throw an exception using the CPython C API, you call one of the PyErr_Set* functions and then you return a special value from your API function. For functions that normally return a Python object, that special value is NULL; for other functions you have to check the documentation, which may only exist in the header files.

In this case, quoting pystate.h (from CPython 3.7):

/* Py_tracefunc return -1 when raising an exception, or 0 for success. */
typedef int (*Py_tracefunc)(PyObject *, struct _frame *, int, PyObject *);

That means you must declare PythonRunner::tracer with a matching signature, like so:

class PythonRunner {
    // ...
public:
    static int tracer(PyObject *, struct _frame *, int, PyObject *);
}

And then you can return −1 to signal an exception, or 0 to continue normally:

int PythonRunner::tracer(PyObject *, struct _frame *, int, PyObject *)
{
    ...
    // Abort requested?
    else if (interruptRequested())
    {
        PyErr_SetString(PyExc_Exception, "ABORT");
        return -1;
    }
    // Otherwise proceed normally.
    return 0;
}

Also, your call to PyEval_SetProfile will then no longer need a cast.

PyEval_SetProfile(PythonRunner::tracer, NULL);

I mention this because casting function pointers is almost always a bug in and of itself, and in this case, recognizing that could have clued you into what the problem was.

(For historical reasons, the Python "Extending and embedding" documentation's example code contains a bunch of unnecessary casts and functions with incorrect signatures. Whenever you copy code out of that document, remove all of the casts from it, and then carefully inspect the compiler's complaints, adjust type signatures, and put back only the casts that are unavoidable.)

zwol
  • 135,547
  • 38
  • 252
  • 361
  • Thanks for the quick response. I edited my code base on your comments but unfortunately I'm still getting similar behavior as before. I'd like to see the exception caught in the Python code i.e "caught exception" to be printed. With a -1 return (PyErr_SetString(PyExc_Exception, "ABORT"); return -1;), the loop is aborted but exception is not caught. – Michael Japzon Mar 11 '19 at 15:39
  • With the information you've given us, I don't have any other ideas as to what might be wrong. I might be able to help you further if you can provide a _complete_ example program that I can use to observe the problem for myself. – zwol Mar 11 '19 at 15:46
  • I have a QT example here: https://github.com/mbjapzon/pythonAbortException Hopefully you can glean what you need from it...it's a little tough since you need to have Python and QT dependencies. Thanks again for your time. – Michael Japzon Mar 11 '19 at 15:56
  • You will have to modify the .pro file to point to your version of Python. I'm using a 64 bit WinPython, but any 64 bit Python should suffice. – Michael Japzon Mar 11 '19 at 16:00
  • Unfortunately I don't know enough about Qt to be able to tell if the problem is a bad interaction between Qt and Python. Can you try to write a reduced test program that avoids using Qt? – zwol Mar 11 '19 at 16:58
  • Great news, I found a fix referencing this post: https://stackoverflow.com/questions/1420957/stopping-embedded-python – Michael Japzon Mar 11 '19 at 17:35
  • The key is to add a pending call and then raise the exception as you recommended.... else if (true == _instance->interruptRequested()) { Py_AddPendingCall(&PythonRunner::raiseException, NULL); } int PythonRunner::raiseException(void *) { PyErr_SetString(PyExc_KeyboardInterrupt, "Abort"); return -1; } – Michael Japzon Mar 11 '19 at 17:36