Commit a36efe6d authored by Kirill Smelkov's avatar Kirill Smelkov

libpyxruntime: Teach PyFunc to report Python exception after the call

- Add PyError class that wraps Python exception;
- change PyFunc::operator() return from void -> to error, which might be
  either PyError or special error indicating that Python interpreter is
  stopped.
- Provide way to reraise that Python exception.

We will need to see which exception, if any, PyFunc call raised in
sync.PyWorkGroup, where this python-level exception will need to be
propagated to PyWorkGroup.wait() and reraised from there.
parent 73182038
......@@ -154,6 +154,8 @@ cdef nogil:
Timer new_timer_pyexc(double dt) except +topyexc:
return new_timer(dt)
Timer _new_timer_pyfunc_pyexc(double dt, PyObject *pyf) except +topyexc:
# NOTE C++ implicitly casts func<void()> <- func<error()>
# XXX error (= Py Exception) -> exit program with traceback (same as in go) ?
return after_func(dt, runtime.PyFunc(pyf))
cbool timer_stop_pyexc(Timer t) except +topyexc:
......
......@@ -23,6 +23,8 @@
// Library Libpyxruntime complements Libgolang and provides support for
// Python/Cython runtimes that can be used from nogil code.
//
// - `PyError` represents Python exception, that can be caught/reraised from
// nogil code, and is interoperated with libgolang `error`.
// - `PyFunc` represents Python function that can be called from nogil code.
#include <golang/libgolang.h>
......@@ -40,6 +42,46 @@ namespace golang {
namespace pyx {
namespace runtime {
// ErrPyStopped indicates that Python interpreter is stopped.
extern LIBPYXRUNTIME_API const error ErrPyStopped;
// PyError wraps Python exception into error.
// PyError can be used from nogil code.
typedef refptr<class _PyError> PyError;
class _PyError final : public _error, object {
// retrieved by PyErr_Fetch; each one is holding 1 reference to pointed object
PyObject *pyexc_type;
PyObject *pyexc_value;
PyObject *pyexc_tb;
// don't new - create only via runtime::PyErr_Fetch();
private:
_PyError();
~_PyError();
friend error PyErr_Fetch();
friend void PyErr_ReRaise(PyError pyerr);
public:
LIBPYXRUNTIME_API void incref();
LIBPYXRUNTIME_API void decref();
// error interface
std::string Error();
private:
_PyError(const _PyError&); // don't copy
_PyError(_PyError&&); // don't move
};
// PyErr_Fetch fetches and clears current Python error.
// It can be called from nogil code.
// It returns either PyError, ErrPyStopped or nil if there is no error.
LIBPYXRUNTIME_API error PyErr_Fetch();
// PyErr_ReRaise reraises Python error with original traceback.
// It can be called from nogil code.
LIBPYXRUNTIME_API void PyErr_ReRaise(PyError pyerr);
// PyFunc represents python function that can be called.
// PyFunc can be used from nogil code.
// PyFunc is safe to use wrt race to python interpreter shutdown.
......@@ -59,7 +101,7 @@ public:
// call.
// returned error is either PyError or ErrPyStopped.
LIBPYXRUNTIME_API void operator() () const;
LIBPYXRUNTIME_API error operator() () const;
};
// libpyxruntime must be initialized before use via _init.
......
......@@ -20,13 +20,29 @@
"""Package pyx/runtime.pyx complements golang.pyx and provides support for
Python/Cython runtimes that can be used from nogil code.
- `PyError` represents Python exception, that can be caught/reraised from
nogil code, and is interoperated with libgolang `error`.
- `PyFunc` represents Python function that can be called from nogil code.
"""
from golang cimport error, _error, refptr, gobject
from cpython cimport PyObject
from libcpp.string cimport string
cdef extern from "golang/pyx/runtime.h" namespace "golang::pyx::runtime" nogil:
const error ErrPyStopped
cppclass _PyError (_error, gobject):
string Error()
cppclass PyError (refptr[_PyError]):
# PyError.X = PyError->X in C++
string Error "_ptr()->Error" ()
error PyErr_Fetch()
void PyErr_ReRaise(PyError pyerr)
cppclass PyFunc:
__init__(PyObject *pyf)
void operator() ()
error operator() ()
......@@ -24,7 +24,9 @@
#include "golang/pyx/runtime.h"
#include "golang/sync.h"
#include "golang/errors.h"
using namespace golang;
using std::string;
#include <utility>
using std::tuple;
......@@ -83,6 +85,88 @@ static tuple<PyGILState_STATE, bool> pygil_ensure() {
return make_tuple(gstate, true);
}
// errors
const error ErrPyStopped = errors::New("Python interpreter is stopped");
error PyErr_Fetch() {
PyObject *pyexc_type, *pyexc_value, *pyexc_tb;
PyGILState_STATE gstate;
bool ok;
tie(gstate, ok) = pygil_ensure();
if (!ok) {
return ErrPyStopped;
}
::PyErr_Fetch(&pyexc_type, &pyexc_value, &pyexc_tb);
// we now own references to fetched pyexc_*
PyGILState_Release(gstate);
// no error
if (pyexc_type == NULL && pyexc_value == NULL && pyexc_tb == NULL)
return NULL;
// -> _PyError
_PyError* _e = new _PyError();
_e->pyexc_type = pyexc_type;
_e->pyexc_value = pyexc_value;
_e->pyexc_tb = pyexc_tb;
return adoptref(static_cast<_error*>(_e));
}
void PyErr_ReRaise(PyError pyerr) {
PyGILState_STATE gstate;
bool ok;
tie(gstate, ok) = pygil_ensure();
if (!ok) {
return; // python interpreter is stopped
}
// PyErr_Restore takes 1 reference to restored objects.
// We want to keep pyerr itself alive and valid.
Py_XINCREF(pyerr->pyexc_type);
Py_XINCREF(pyerr->pyexc_value);
Py_XINCREF(pyerr->pyexc_tb);
::PyErr_Restore(pyerr->pyexc_type, pyerr->pyexc_value, pyerr->pyexc_tb);
PyGILState_Release(gstate);
}
_PyError::_PyError() {}
_PyError::~_PyError() {
PyGILState_STATE gstate;
bool ok;
tie(gstate, ok) = pygil_ensure();
PyObject *pyexc_type = this->pyexc_type;
PyObject *pyexc_value = this->pyexc_value;
PyObject *pyexc_tb = this->pyexc_tb;
this->pyexc_type = NULL;
this->pyexc_value = NULL;
this->pyexc_tb = NULL;
if (!ok) {
return;
}
Py_XDECREF(pyexc_type);
Py_XDECREF(pyexc_value);
Py_XDECREF(pyexc_tb);
PyGILState_Release(gstate);
}
void _PyError::incref() {
object::incref();
}
void _PyError::decref() {
if (__decref())
delete this;
}
string _PyError::Error() {
return "<PyError>"; // TODO consider putting exception details into error string
}
// PyFunc
......@@ -123,7 +207,7 @@ PyFunc::~PyFunc() {
PyGILState_Release(gstate);
}
void PyFunc::operator() () const {
error PyFunc::operator() () const {
PyGILState_STATE gstate;
bool ok;
......@@ -135,21 +219,18 @@ void PyFunc::operator() () const {
// _golang.pyx::__goviac and pygil_ensure for details.
tie(gstate, ok) = pygil_ensure();
if (!ok) {
return;
return ErrPyStopped;
}
ok = true;
error err;
PyObject *ret = PyObject_CallFunction(pyf, NULL);
if (ret == NULL && !pyexited) {
PyErr_PrintEx(0);
ok = false;
if (ret == NULL) {
err = PyErr_Fetch();
}
Py_XDECREF(ret);
PyGILState_Release(gstate);
// XXX exception -> exit program with traceback (same as in go) ?
//if (!ok)
// panic("pycall failed");
return err;
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment