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: ...@@ -154,6 +154,8 @@ cdef nogil:
Timer new_timer_pyexc(double dt) except +topyexc: Timer new_timer_pyexc(double dt) except +topyexc:
return new_timer(dt) return new_timer(dt)
Timer _new_timer_pyfunc_pyexc(double dt, PyObject *pyf) except +topyexc: 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)) return after_func(dt, runtime.PyFunc(pyf))
cbool timer_stop_pyexc(Timer t) except +topyexc: cbool timer_stop_pyexc(Timer t) except +topyexc:
......
...@@ -23,6 +23,8 @@ ...@@ -23,6 +23,8 @@
// Library Libpyxruntime complements Libgolang and provides support for // Library Libpyxruntime complements Libgolang and provides support for
// Python/Cython runtimes that can be used from nogil code. // 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. // - `PyFunc` represents Python function that can be called from nogil code.
#include <golang/libgolang.h> #include <golang/libgolang.h>
...@@ -40,6 +42,46 @@ namespace golang { ...@@ -40,6 +42,46 @@ namespace golang {
namespace pyx { namespace pyx {
namespace runtime { 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 represents python function that can be called.
// PyFunc can be used from nogil code. // PyFunc can be used from nogil code.
// PyFunc is safe to use wrt race to python interpreter shutdown. // PyFunc is safe to use wrt race to python interpreter shutdown.
...@@ -59,7 +101,7 @@ public: ...@@ -59,7 +101,7 @@ public:
// call. // call.
// returned error is either PyError or ErrPyStopped. // 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. // libpyxruntime must be initialized before use via _init.
......
...@@ -20,13 +20,29 @@ ...@@ -20,13 +20,29 @@
"""Package pyx/runtime.pyx complements golang.pyx and provides support for """Package pyx/runtime.pyx complements golang.pyx and provides support for
Python/Cython runtimes that can be used from nogil code. 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. - `PyFunc` represents Python function that can be called from nogil code.
""" """
from golang cimport error, _error, refptr, gobject
from cpython cimport PyObject from cpython cimport PyObject
from libcpp.string cimport string
cdef extern from "golang/pyx/runtime.h" namespace "golang::pyx::runtime" nogil: 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: cppclass PyFunc:
__init__(PyObject *pyf) __init__(PyObject *pyf)
void operator() () error operator() ()
...@@ -24,7 +24,9 @@ ...@@ -24,7 +24,9 @@
#include "golang/pyx/runtime.h" #include "golang/pyx/runtime.h"
#include "golang/sync.h" #include "golang/sync.h"
#include "golang/errors.h"
using namespace golang; using namespace golang;
using std::string;
#include <utility> #include <utility>
using std::tuple; using std::tuple;
...@@ -83,6 +85,88 @@ static tuple<PyGILState_STATE, bool> pygil_ensure() { ...@@ -83,6 +85,88 @@ static tuple<PyGILState_STATE, bool> pygil_ensure() {
return make_tuple(gstate, true); 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 // PyFunc
...@@ -123,7 +207,7 @@ PyFunc::~PyFunc() { ...@@ -123,7 +207,7 @@ PyFunc::~PyFunc() {
PyGILState_Release(gstate); PyGILState_Release(gstate);
} }
void PyFunc::operator() () const { error PyFunc::operator() () const {
PyGILState_STATE gstate; PyGILState_STATE gstate;
bool ok; bool ok;
...@@ -135,21 +219,18 @@ void PyFunc::operator() () const { ...@@ -135,21 +219,18 @@ void PyFunc::operator() () const {
// _golang.pyx::__goviac and pygil_ensure for details. // _golang.pyx::__goviac and pygil_ensure for details.
tie(gstate, ok) = pygil_ensure(); tie(gstate, ok) = pygil_ensure();
if (!ok) { if (!ok) {
return; return ErrPyStopped;
} }
ok = true; error err;
PyObject *ret = PyObject_CallFunction(pyf, NULL); PyObject *ret = PyObject_CallFunction(pyf, NULL);
if (ret == NULL && !pyexited) { if (ret == NULL) {
PyErr_PrintEx(0); err = PyErr_Fetch();
ok = false;
} }
Py_XDECREF(ret); Py_XDECREF(ret);
PyGILState_Release(gstate); PyGILState_Release(gstate);
// XXX exception -> exit program with traceback (same as in go) ? return err;
//if (!ok)
// panic("pycall failed");
} }
......
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