boost::python::call and PyErr_Print in C++ destructors throw unexpected exceptions and cause running `async` python functions to incorrectly return `None`
還沒有人認領這個 Issue。
評估
- 難度
- 5/5
- 預估耗時
- 一週以上
- 新手友好度
- 25/100
- Issue 類型
- 缺陷
- 描述清晰度
- 需要釐清
- 活躍度
- 停滯
研究方向
使用 native.cpp 和 main.py 範例重現該行為,遵循提供的建置腳本並執行 python main.py。比較五個非同步測試,包括手動銷毀的情況,並追蹤解構函式呼叫、boost::python 呼叫以及 PyErr_Print 的處理。完成的標準是所有測試都回傳其數值,且沒有非預期例外、直譯器損壞或輸出遺失。
由索引模型根據 Issue 內容生成。
描述
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
- 主要語言
- C++
- 星號
- 537
- 分支
- 223
- 平均合併
- 11 小時 22 分鐘
- 30 天內合併 PR
- 2
貢獻指南
這個儲存庫沒有索引到貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
boostorg/python 的其他 Issue
-
難度 1/5 1 小時以內 新手友好度 84/100
-
難度 2/5 1-3 小時 新手友好度 68/100
-
難度 2/5 1-3 小時 新手友好度 64/100
-
BoostDetectToolset-1.90.0.cmake file not found in an include() call in boost_python-config.cmake 未關閉
難度 3/5 1-2 天 新手友好度 48/100
-
難度 4/5 3-5 天 新手友好度 25/100
相似的 Issue
-
難度 2/5 1-3 小時 新手友好度 75/100
flutter-webrtc/flutter-webrtc#2206 ·
-
難度 2/5 1-3 小時 新手友好度 70/100
google-ai-edge/LiteRT-LM#3739 ·
-
Component: GLib
難度 2/5 1-3 小時 新手友好度 70/100
-
Mute ydb/tests/functional/dstool/test_canonical_requests.py.Test.test_group_take_snapshot in main 未關閉ai_reviewed
難度 2/5 1-3 小時 新手友好度 70/100
ydb-platform/ydb#53974 · 3 則留言 ·
-
難度 2/5 1-3 小時 新手友好度 70/100
google/libultrahdr#485 ·