boost::python::call and PyErr_Print in C++ destructors throw unexpected exceptions and cause running `async` python functions to incorrectly return `None`
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 25/100
- Issue type
- Bug
- Clarity
- Needs clarification
- Activity status
- Stale
- Domain
- backend-api-design
Research direction
Reproduce the behavior using the native.cpp and main.py examples, following the provided build script and running python main.py. Compare the five async tests, including the manual-destruction cases, and trace the destructor calls, boost::python calls, and PyErr_Print handling. Done means all tests return their numeric values without unexpected exceptions, interpreter corruption, or missing output.
Written by the indexing model from the issue text.
Description
Summary
While inside a Python async function, if a C++ object's destructor calls any Python function via boost::python::call or call_method, an exception is raised (and Python code is not successfully called) if destruction is performed after Python's return.
Even worse, calls to PyErr_Print in such destructors corrupt the Python interpreter, causing the current async function to return None regardless of its expected behavior.
The outcome is that certain boost::python calls in C++ destructors fail unexpectedly, and cause arbitrary async python functions to return None when they cannot logically do so.
This is very surprising and bad.
Steps to Reproduce
- Save the native code at the bottom of this issue as
native.cpp. - Save the Python file as
main.py. - Build the native library. I used this build script in Python:
ffrom distutils.core import setup
from distutils.extension import Extension
native_ext = Extension('native', sources=['native.cpp'], libraries=['boost_python39-mt'])
setup(name='repro', version='0', ext_modules=[native_ext])
- Run
python main.py - Observe that the return value from
++++ Test 1: autodestructisNone, despite the fact that there is no way for that function to return None. - Observe that the None return is preceeded by a stacktrace with a
StopIterationexception raised from a call todoprint, despite the fact that there is no way for that function to raise StopIteration` as it contains no iterators. - Observe that the return value from
++++ Test 4: autodestruct with printis similarlyNone, impossibly. - Observe that the stacktrace emitted for test 4 is different from the trace in test 1 (and conceals the real interpreter invariant violation:
SystemError: _PyEval_EvalFrameDefault returned a result with an error set, despite the fact that the error still occurs). - Observe that the code in the destructor-called function for test 4 is never called ("called from native code" is never printed).
Expected behavior
- All tests in
main.pyobserve a numeric return value from the test functions. - In python
asyncfunctions,boost::python::callorcall_methodcalls that are performed in C++ destructors should behave equivalently to equivalent pure-python calls contained in__del__methods. Specifically, they should not raise errors, and should especially not corrupt interpreter state such that return values from unrelated functions are changed.
Specific issues
There are three specific sub-issues here, ordered from most to least severe:
- Calls to
PyErr_Printin the wrong place can cause unrelated code to return incorrect values. - Calls from C++ to Python in C++ destructors can incorrectly raise exceptions, potentially interrupting cleanup logic.
- The "real" Python error (
SystemError: _PyEval_EvalFrameDefault returned a result with an error set) is not emitted or otherwise observable from destructor-called Python functions in most cases, making diagnosis/error googling difficult.
Removing calls to PyErr_Print prevents return-value corruption but issues 2 and 3 remain so long as Python code is invoked from C++ in a destructor.
Code to reproduce issue:
Native code:
#include <boost/python.hpp>
#include <iostream>
#include <string>
class Container
{
PyObject* _inner;
public:
Container(PyObject* inner) {
_inner = inner;
Py_XINCREF(inner);
}
virtual ~Container() {
std::cout << "Destroying native object" << std::endl;
try {
boost::python::call<void>(_inner);
} catch (boost::python::error_already_set e) {
std::cout << "Native destruction error: ";
PyErr_Print();
}
Py_XDECREF(_inner);
}
};
BOOST_PYTHON_MODULE(native)
{
using namespace boost::python;
class_< Container >("Container", init<PyObject*>());
}
Python:
import asyncio
import native
class Container:
def __init__(self, inner):
self._inner = inner
def __del__(self):
self._inner()
def empty():
pass
def doprint():
print("Called from native code")
async def test_autodestruct_control_group(num):
obj = Container(empty)
print("Returning", num)
return num
async def test_autodestruct(num):
obj = native.Container(empty)
print("Returning", num)
return num
async def test_autodestruct_print(num):
obj = native.Container(doprint)
print("Returning", num)
return num
async def test_manual_destruct(num):
obj = native.Container(empty)
del obj
print("Returning", num)
return num
async def test_manual_destruct_finally(num):
obj = native.Container(empty)
try:
print("Returning", num)
return num
finally:
del obj
async def main():
print("++++ Starting tests")
print("++++ Test 0: control group (pure python)", await test_autodestruct_control_group(0))
print("++++ Test 1: autodestruct",await test_autodestruct(1))
print("++++ Test 2: manual destruct", await test_manual_destruct(2))
print("++++ Test 3: manual destruct in finally", await test_manual_destruct_finally(3))
print("++++ Test 4: autodestruct with print", await test_autodestruct_print(4))
if __name__ == '__main__':
asyncio.run(main())
Example (issue exhibiting) output:
∴ python main.py
++++ Starting tests
Returning 0
++++ Test 0: control group (pure python) 0
Returning 1
Destroying native object
Native destruction errors: StopIteration: 1
The above exception was the direct cause of the following exception:
SystemError: _PyEval_EvalFrameDefault returned a result with an error set
++++ Test 1: autodestruct None
Destroying native object
Returning 2
++++ Test 2: manual destruct 2
Returning 3
Destroying native object
++++ Test 3: manual destruct in finally 3
Returning 4
Destroying native object
Native destruction errors: Traceback (most recent call last):
File "/Users/zac.bentley/Desktop/Projects/pycpp_repro/main.py", line 18, in doprint
print("Called from native code")
StopIteration: 4
++++ Test 4: autodestruct with print
Testing environment:
OS: MacOS 11.5.2
Architecture: x86_64
Python interpreter: Python 3.9.6 via Homebrew
Boost version: 1.76.0 via Homebrew
Boost-python version: 1.76.0 via Homebrew (boost-python3)
Compiler:
clang++ --version
Apple clang version 12.0.5 (clang-1205.0.22.9)
Target: x86_64-apple-darwin20.6.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin
- Dominant language
- C++
- Stars
- 537
- Forks
- 223
- Avg merge
- 11h 22m
- Merged PRs (30d)
- 2
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from boostorg/python
-
Difficulty 1/5 Under an hour Newbie friendliness 84/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
-
BoostDetectToolset-1.90.0.cmake file not found in an include() call in boost_python-config.cmake Open
Difficulty 3/5 1-2 days Newbie friendliness 48/100
-
Difficulty 4/5 3-5 days Newbie friendliness 25/100
Similar issues
-
AuTest Bug Tests
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
apache/trafficserver#13714 ·
-
bug build
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
facebookincubator/velox#19143 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
tenstorrent/tt-metal#57393 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
objectionary/eo-graphs#74 ·