boost::python::call and PyErr_Print in C++ destructors throw unexpected exceptions and cause running `async` python functions to incorrectly return `None`
Personne n'a encore pris cette issue.
Évaluation
- Difficulté
- 5/5
- Temps estimé
- Plus d'une semaine
- Accessibilité débutants
- 25/100
- Type d'issue
- Bug
- Clarté
- À clarifier
- Activité
- À l'abandon
- Domaine
- backend-api-design
Piste de recherche
Reproduisez le comportement à l’aide des exemples native.cpp et main.py, en suivant le script de build fourni et en exécutant python main.py. Comparez les cinq tests asynchrones, y compris les cas de destruction manuelle, et tracez les appels aux destructeurs, les appels à boost::python et la gestion de PyErr_Print. La tâche est terminée lorsque tous les tests renvoient leurs valeurs numériques sans exceptions inattendues, corruption de l’interpréteur ni sortie manquante.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
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
- Langage dominant
- C++
- Étoiles
- 537
- Forks
- 223
- Merge moyen
- 11 h 22 min
- PR mergées (30 j)
- 2
Guide de contribution
Aucun guide de contribution indexé pour ce dépôt
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de boostorg/python
-
numpy::from_data calls vector::front and crashes (or has UB) when called with a scalar object Ouverte
Difficulté 1/5 Moins d'une heure Accessibilité débutants 84/100
-
Difficulté 2/5 1-3 heures Accessibilité débutants 68/100
-
Difficulté 2/5 1-3 heures Accessibilité débutants 64/100
-
BoostDetectToolset-1.90.0.cmake file not found in an include() call in boost_python-config.cmake Ouverte
Difficulté 3/5 1-2 jours Accessibilité débutants 48/100
-
Difficulté 4/5 3-5 jours Accessibilité débutants 25/100
Toutes les issues de boostorg/python
Issues similaires
-
Difficulté 2/5 1-3 heures Accessibilité débutants 76/100
objectionary/eo-graphs#74 ·
-
Difficulté 1/5 Moins d'une heure Accessibilité débutants 95/100
-
enhancement
Difficulté 1/5 Moins d'une heure Accessibilité débutants 88/100
QuantStack/git2cpp#187 ·
-
Difficulté 2/5 1-3 heures Accessibilité débutants 86/100
-
Difficulté 2/5 1-3 heures Accessibilité débutants 72/100