Skip to content

Commit

Permalink
feat: Add PyExceptionContext for passing Python exceptions through …
Browse files Browse the repository at this point in the history
…C++ exceptions. (#87)
  • Loading branch information
LinZhihao-723 authored Nov 11, 2024
1 parent 81ecf85 commit 8d2f075
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 15 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ set(CLP_FFI_PY_LIB_IR_SOURCES
${CLP_FFI_PY_LIB_SRC_DIR}/modules/ir_native.cpp
${CLP_FFI_PY_LIB_SRC_DIR}/Py_utils.cpp
${CLP_FFI_PY_LIB_SRC_DIR}/Py_utils.hpp
${CLP_FFI_PY_LIB_SRC_DIR}/PyExceptionContext.hpp
${CLP_FFI_PY_LIB_SRC_DIR}/PyObjectCast.hpp
${CLP_FFI_PY_LIB_SRC_DIR}/PyObjectUtils.hpp
${CLP_FFI_PY_LIB_SRC_DIR}/Python.hpp
Expand Down
14 changes: 12 additions & 2 deletions src/clp_ffi_py/ExceptionFFI.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@
#define CLP_FFI_PY_EXCEPTION_FFI

#include <string>
#include <utility>

#include <clp/ErrorCode.hpp>
#include <clp/TraceableException.hpp>

#include <clp_ffi_py/PyExceptionContext.hpp>

namespace clp_ffi_py {
/**
* A class that represents a traceable exception during the native code execution. Note: for
* exceptions of CPython execution, please use CPython interface to set the exception instead.
* A class that represents a traceable exception during the native code execution. It captures any
* Python exceptions set, allowing the handler at the catch site to either restore or discard the
* exception as needed.
*/
class ExceptionFFI : public clp::TraceableException {
public:
Expand All @@ -23,8 +28,13 @@ class ExceptionFFI : public clp::TraceableException {

[[nodiscard]] auto what() const noexcept -> char const* override { return m_message.c_str(); }

[[nodiscard]] auto get_py_exception_context() -> PyExceptionContext& {
return m_py_exception_context;
}

private:
std::string m_message;
PyExceptionContext m_py_exception_context;
};
} // namespace clp_ffi_py

Expand Down
70 changes: 70 additions & 0 deletions src/clp_ffi_py/PyExceptionContext.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#ifndef CLP_FFI_PY_PYEXCEPTIONCONTEXT_HPP
#define CLP_FFI_PY_PYEXCEPTIONCONTEXT_HPP

#include <clp_ffi_py/Python.hpp> // Must always be included before any other header files

namespace clp_ffi_py {
/**
* Class to get/set Python exception context, designed to capture the current exception state upon
* instantiation and provide a restore API that allows users to reinstate the exception context when
* needed. Doc: https://docs.python.org/3/c-api/exceptions.html#c.PyErr_Fetch
*/
class PyExceptionContext {
public:
// Constructor
/**
* Constructs the context by fetching the current raised exception (if any).
*/
PyExceptionContext() { PyErr_Fetch(&m_type, &m_value, &m_traceback); }

// Delete copy/move constructors and assignments
PyExceptionContext(PyExceptionContext const&) = delete;
PyExceptionContext(PyExceptionContext&&) = delete;
auto operator=(PyExceptionContext const&) -> PyExceptionContext& = delete;
auto operator=(PyExceptionContext&&) -> PyExceptionContext& = delete;

// Destructor
~PyExceptionContext() {
Py_XDECREF(m_type);
Py_XDECREF(m_value);
Py_XDECREF(m_traceback);
}

// Methods
/**
* @return Whether the context stores an exception.
*/
[[nodiscard]] auto has_exception() const noexcept -> bool { return nullptr != m_value; }

/**
* Restores the exception from the context.
* NOTE:
* - This method will clear the existing exception if one is set.
* - The stored context will be cleared after restoration.
* - If there's no exception in the stored context, the error indicator will be cleared.
* - This method should be called strictly once, otherwise the error indicator will be cleared.
* @return Whether an exception has been set by restoring the context.
*/
[[maybe_unused]] auto restore() noexcept -> bool {
auto const exception_has_been_set{has_exception()};
PyErr_Restore(m_type, m_value, m_traceback);
m_type = nullptr;
m_value = nullptr;
m_traceback = nullptr;
return exception_has_been_set;
}

[[nodiscard]] auto get_type() const -> PyObject* { return m_type; }

[[nodiscard]] auto get_value() const -> PyObject* { return m_value; }

[[nodiscard]] auto get_traceback() const -> PyObject* { return m_traceback; }

private:
PyObject* m_type{nullptr};
PyObject* m_value{nullptr};
PyObject* m_traceback{nullptr};
};
} // namespace clp_ffi_py

#endif // CLP_FFI_PY_PYEXCEPTIONCONTEXT_HPP
9 changes: 2 additions & 7 deletions src/clp_ffi_py/ir/native/PyMetadata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -237,13 +237,8 @@ auto PyMetadata::init(nlohmann::json const& metadata, bool is_four_byte_encoding
try {
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
m_metadata = new Metadata(metadata, is_four_byte_encoding);
} catch (ExceptionFFI const& ex) {
PyErr_Format(
PyExc_RuntimeError,
"Failed to initialize metadata from deserialized JSON format preamble. "
"Error message: %s",
ex.what()
);
} catch (clp_ffi_py::ExceptionFFI& ex) {
handle_traceable_exception(ex);
m_metadata = nullptr;
return false;
}
Expand Down
8 changes: 2 additions & 6 deletions src/clp_ffi_py/ir/native/PyQuery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -620,12 +620,8 @@ auto PyQuery::init(
wildcard_queries,
search_time_termination_margin
);
} catch (ExceptionFFI const& ex) {
PyErr_Format(
PyExc_RuntimeError,
"Failed to initialize Query object. Error message: %s",
ex.what()
);
} catch (clp_ffi_py::ExceptionFFI& ex) {
handle_traceable_exception(ex);
m_query = nullptr;
return false;
}
Expand Down
22 changes: 22 additions & 0 deletions src/clp_ffi_py/utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

#include <iostream>

#include <clp/TraceableException.hpp>

#include <clp_ffi_py/ExceptionFFI.hpp>
#include <clp_ffi_py/PyObjectCast.hpp>

namespace clp_ffi_py {
Expand Down Expand Up @@ -59,4 +62,23 @@ auto get_py_bool(bool is_true) -> PyObject* {
}
Py_RETURN_FALSE;
}

auto handle_traceable_exception(clp::TraceableException& exception) noexcept -> void {
if (auto* py_ffi_exception{dynamic_cast<ExceptionFFI*>(&exception)}) {
auto& exception_context{py_ffi_exception->get_py_exception_context()};
if (exception_context.has_exception()) {
exception_context.restore();
return;
}
}

PyErr_Format(
PyExc_RuntimeError,
"%s:%d: ErrorCode: %d; Message: %s",
exception.get_filename(),
exception.get_line_number(),
static_cast<int>(exception.get_error_code()),
exception.what()
);
}
} // namespace clp_ffi_py
7 changes: 7 additions & 0 deletions src/clp_ffi_py/utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <vector>

#include <clp/ffi/encoding_methods.hpp>
#include <clp/TraceableException.hpp>

namespace clp_ffi_py {
/**
Expand Down Expand Up @@ -60,6 +61,12 @@ auto get_py_bool(bool is_true) -> PyObject*;
template <typename int_type>
auto parse_py_int(PyObject* py_int, int_type& val) -> bool;

/**
* Handles a `clp::TraceableException` by setting a Python exception accordingly.
* @param exception
*/
auto handle_traceable_exception(clp::TraceableException& exception) noexcept -> void;

/**
* A template that always evaluates as false.
*/
Expand Down

0 comments on commit 8d2f075

Please sign in to comment.