Skip to content

Commit 72a67d6

Browse files
committed
Implement sys.call_tracing, sys._current_exceptions
- Add sys.call_tracing as func(*args) dispatch - Add sys._current_exceptions with per-thread exception tracking via thread_exceptions registry synced on push/pop/set_exception - Fix f_back to use previous_frame pointer and cross-thread lookup - Add module="sys" to float_info struct sequence - Fix sys.exit to unpack tuple args - Set default stdio_errors to surrogateescape - Remove noisy __del__ warning in object core
1 parent ca00699 commit 72a67d6

File tree

7 files changed

+167
-28
lines changed

7 files changed

+167
-28
lines changed

Lib/test/test_sys.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,6 @@ def test_getwindowsversion(self):
431431
# still has 5 elements
432432
maj, min, buildno, plat, csd = sys.getwindowsversion()
433433

434-
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module 'sys' has no attribute 'call_tracing'
435434
def test_call_tracing(self):
436435
self.assertRaises(TypeError, sys.call_tracing, type, 2)
437436

@@ -497,7 +496,6 @@ def test_getframemodulename(self):
497496
self.assertIsNone(sys._getframemodulename(i))
498497

499498
# sys._current_frames() is a CPython-only gimmick.
500-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: didn't find f123() on thread's call stack
501499
@threading_helper.reap_threads
502500
@threading_helper.requires_working_threading()
503501
def test_current_frames(self):
@@ -565,7 +563,6 @@ def g456():
565563
leave_g.set()
566564
t.join()
567565

568-
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module 'sys' has no attribute '_current_exceptions'
569566
@threading_helper.reap_threads
570567
@threading_helper.requires_working_threading()
571568
def test_current_exceptions(self):
@@ -1188,7 +1185,6 @@ def __del__(self):
11881185
rc, stdout, stderr = assert_python_ok('-c', code)
11891186
self.assertEqual(stdout.rstrip(), b'True')
11901187

1191-
@unittest.expectedFailure # TODO: RUSTPYTHON; IndexError: list index out of range
11921188
def test_issue20602(self):
11931189
# sys.flags and sys.float_info were wiped during shutdown.
11941190
code = """if 1:

crates/vm/src/builtins/frame.rs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use super::{PyCode, PyDictRef, PyIntRef, PyStrRef};
66
use crate::{
7-
AsObject, Context, Py, PyObjectRef, PyRef, PyResult, VirtualMachine,
7+
Context, Py, PyObjectRef, PyRef, PyResult, VirtualMachine,
88
class::PyClassImpl,
99
frame::{Frame, FrameOwner, FrameRef},
1010
function::PySetterValue,
@@ -192,16 +192,42 @@ impl Py<Frame> {
192192

193193
#[pygetset]
194194
pub fn f_back(&self, vm: &VirtualMachine) -> Option<PyRef<Frame>> {
195-
// TODO: actually store f_back inside Frame struct
195+
let previous = self.previous_frame();
196+
if previous.is_null() {
197+
return None;
198+
}
196199

197-
// get the frame in the frame stack that appears before this one.
198-
// won't work if this frame isn't in the frame stack, hence the todo above
199-
vm.frames
200+
if let Some(frame) = vm
201+
.frames
200202
.borrow()
201203
.iter()
202-
.rev()
203-
.skip_while(|p| !p.is(self.as_object()))
204-
.nth(1)
204+
.find(|f| {
205+
let ptr: *const Frame = &****f;
206+
core::ptr::eq(ptr, previous)
207+
})
205208
.cloned()
209+
{
210+
return Some(frame);
211+
}
212+
213+
#[cfg(feature = "threading")]
214+
{
215+
let registry = vm.state.thread_frames.lock();
216+
for slot in registry.values() {
217+
if let Some(frame) = slot
218+
.lock()
219+
.iter()
220+
.find(|f| {
221+
let ptr: *const Frame = &****f;
222+
core::ptr::eq(ptr, previous)
223+
})
224+
.cloned()
225+
{
226+
return Some(frame);
227+
}
228+
}
229+
}
230+
231+
None
206232
}
207233
}

crates/vm/src/object/core.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,10 +1078,7 @@ impl PyObject {
10781078
Some(true) => Ok(()),
10791079
// we've been resurrected by __del__
10801080
Some(false) => Err(()),
1081-
None => {
1082-
warn!("couldn't run __del__ method for object");
1083-
Ok(())
1084-
}
1081+
None => Ok(()),
10851082
}
10861083
}
10871084

crates/vm/src/stdlib/sys.rs

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ mod sys {
3333
use crate::{
3434
AsObject, PyObject, PyObjectRef, PyPayload, PyRef, PyRefExact, PyResult,
3535
builtins::{
36-
PyBaseExceptionRef, PyDictRef, PyFrozenSet, PyNamespace, PyStr, PyStrRef, PyTupleRef,
37-
PyTypeRef,
36+
PyBaseExceptionRef, PyDictRef, PyFrozenSet, PyNamespace, PyStr, PyStrRef, PyTuple,
37+
PyTupleRef, PyTypeRef,
3838
},
3939
common::{
4040
ascii,
@@ -789,8 +789,22 @@ mod sys {
789789

790790
#[pyfunction]
791791
fn exit(code: OptionalArg<PyObjectRef>, vm: &VirtualMachine) -> PyResult {
792-
let code = code.unwrap_or_none(vm);
793-
Err(vm.new_exception(vm.ctx.exceptions.system_exit.to_owned(), vec![code]))
792+
let status = code.unwrap_or_none(vm);
793+
let args = if let Some(status_tuple) = status.downcast_ref::<PyTuple>() {
794+
status_tuple.as_slice().to_vec()
795+
} else {
796+
vec![status]
797+
};
798+
let exc = vm.invoke_exception(vm.ctx.exceptions.system_exit.to_owned(), args)?;
799+
Err(exc)
800+
}
801+
802+
#[pyfunction]
803+
fn call_tracing(func: PyObjectRef, args: PyTupleRef, vm: &VirtualMachine) -> PyResult {
804+
// CPython temporarily enables tracing state around this call.
805+
// RustPython does not currently model the full C-level tracing toggles,
806+
// but call semantics (func(*args)) are matched.
807+
func.call(PosArgs::new(args.as_slice().to_vec()), vm)
794808
}
795809

796810
#[pyfunction]
@@ -1024,6 +1038,33 @@ mod sys {
10241038
Ok(dict)
10251039
}
10261040

1041+
/// Return a dictionary mapping each thread's identifier to its currently
1042+
/// active exception, or None if no exception is active.
1043+
#[cfg(feature = "threading")]
1044+
#[pyfunction]
1045+
fn _current_exceptions(vm: &VirtualMachine) -> PyResult<PyDictRef> {
1046+
use crate::AsObject;
1047+
use crate::vm::thread::get_all_current_exceptions;
1048+
1049+
let dict = vm.ctx.new_dict();
1050+
for (thread_id, exc) in get_all_current_exceptions(vm) {
1051+
let key = vm.ctx.new_int(thread_id);
1052+
let value = exc.map_or_else(|| vm.ctx.none(), |e| e.into());
1053+
dict.set_item(key.as_object(), value, vm)?;
1054+
}
1055+
1056+
Ok(dict)
1057+
}
1058+
1059+
#[cfg(not(feature = "threading"))]
1060+
#[pyfunction]
1061+
fn _current_exceptions(vm: &VirtualMachine) -> PyResult<PyDictRef> {
1062+
let dict = vm.ctx.new_dict();
1063+
let key = vm.ctx.new_int(0);
1064+
dict.set_item(key.as_object(), vm.topmost_exception().to_pyobject(vm), vm)?;
1065+
Ok(dict)
1066+
}
1067+
10271068
/// Stub for non-threading builds - returns empty dict
10281069
#[cfg(not(feature = "threading"))]
10291070
#[pyfunction]
@@ -1619,7 +1660,7 @@ mod sys {
16191660
};
16201661
}
16211662

1622-
#[pystruct_sequence(name = "float_info", data = "FloatInfoData", no_attr)]
1663+
#[pystruct_sequence(name = "float_info", module = "sys", data = "FloatInfoData", no_attr)]
16231664
pub(super) struct PyFloatInfo;
16241665

16251666
#[pyclass(with(PyStructSequence))]

crates/vm/src/vm/interpreter.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ where
121121
#[cfg(feature = "threading")]
122122
thread_frames: parking_lot::Mutex::new(std::collections::HashMap::new()),
123123
#[cfg(feature = "threading")]
124+
thread_exceptions: parking_lot::Mutex::new(std::collections::HashMap::new()),
125+
#[cfg(feature = "threading")]
124126
thread_handles: parking_lot::Mutex::new(Vec::new()),
125127
#[cfg(feature = "threading")]
126128
shutdown_handles: parking_lot::Mutex::new(Vec::new()),

crates/vm/src/vm/mod.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ pub struct PyGlobalState {
132132
/// Registry of all threads' current frames for sys._current_frames()
133133
#[cfg(feature = "threading")]
134134
pub thread_frames: parking_lot::Mutex<HashMap<u64, stdlib::thread::CurrentFrameSlot>>,
135+
/// Registry of all threads' exception slots for sys._current_exceptions()
136+
#[cfg(feature = "threading")]
137+
pub thread_exceptions: parking_lot::Mutex<HashMap<u64, thread::CurrentExceptionSlot>>,
135138
/// Registry of all ThreadHandles for fork cleanup
136139
#[cfg(feature = "threading")]
137140
pub thread_handles: parking_lot::Mutex<Vec<stdlib::thread::HandleEntry>>,
@@ -380,7 +383,12 @@ impl VirtualMachine {
380383
let errors = if fd == 2 {
381384
Some("backslashreplace")
382385
} else {
383-
self.state.config.settings.stdio_errors.as_deref()
386+
self.state
387+
.config
388+
.settings
389+
.stdio_errors
390+
.as_deref()
391+
.or(Some("surrogateescape"))
384392
};
385393

386394
let stdio = self.call_method(
@@ -1333,13 +1341,19 @@ impl VirtualMachine {
13331341
let mut excs = self.exceptions.borrow_mut();
13341342
let prev = core::mem::take(&mut *excs);
13351343
excs.prev = Some(Box::new(prev));
1336-
excs.exc = exc
1344+
excs.exc = exc;
1345+
drop(excs);
1346+
#[cfg(feature = "threading")]
1347+
thread::sync_thread_exception(self.topmost_exception());
13371348
}
13381349

13391350
pub(crate) fn pop_exception(&self) -> Option<PyBaseExceptionRef> {
13401351
let mut excs = self.exceptions.borrow_mut();
13411352
let cur = core::mem::take(&mut *excs);
13421353
*excs = *cur.prev.expect("pop_exception() without nested exc stack");
1354+
drop(excs);
1355+
#[cfg(feature = "threading")]
1356+
thread::sync_thread_exception(self.topmost_exception());
13431357
cur.exc
13441358
}
13451359

@@ -1351,6 +1365,8 @@ impl VirtualMachine {
13511365
// don't be holding the RefCell guard while __del__ is called
13521366
let prev = core::mem::replace(&mut self.exceptions.borrow_mut().exc, exc);
13531367
drop(prev);
1368+
#[cfg(feature = "threading")]
1369+
thread::sync_thread_exception(self.topmost_exception());
13541370
}
13551371

13561372
pub(crate) fn contextualize_exception(&self, exception: &Py<PyBaseException>) {

crates/vm/src/vm/thread.rs

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::frame::Frame;
22
#[cfg(feature = "threading")]
3-
use crate::frame::FrameRef;
3+
use crate::{builtins::PyBaseExceptionRef, frame::FrameRef};
44
use crate::{AsObject, PyObject, VirtualMachine};
55
#[cfg(feature = "threading")]
66
use alloc::sync::Arc;
@@ -18,6 +18,11 @@ use std::thread_local;
1818
#[cfg(feature = "threading")]
1919
pub type CurrentFrameSlot = Arc<parking_lot::Mutex<Vec<FrameRef>>>;
2020

21+
/// Type for current exception slot - shared between threads for sys._current_exceptions()
22+
/// Each thread updates its own slot via thread-local reference (no global lock on hot path).
23+
#[cfg(feature = "threading")]
24+
pub type CurrentExceptionSlot = Arc<crate::common::lock::PyMutex<Option<PyBaseExceptionRef>>>;
25+
2126
thread_local! {
2227
pub(super) static VM_STACK: RefCell<Vec<NonNull<VirtualMachine>>> = Vec::with_capacity(1).into();
2328

@@ -27,6 +32,10 @@ thread_local! {
2732
#[cfg(feature = "threading")]
2833
static CURRENT_FRAME_SLOT: RefCell<Option<CurrentFrameSlot>> = const { RefCell::new(None) };
2934

35+
/// Current thread's exception slot for sys._current_exceptions()
36+
#[cfg(feature = "threading")]
37+
static CURRENT_EXCEPTION_SLOT: RefCell<Option<CurrentExceptionSlot>> = const { RefCell::new(None) };
38+
3039
/// Current top frame for signal-safe traceback walking.
3140
/// Mirrors `PyThreadState.current_frame`. Read by faulthandler's signal
3241
/// handler to dump tracebacks without accessing RefCell or locks.
@@ -65,12 +74,20 @@ fn init_frame_slot_if_needed(vm: &VirtualMachine) {
6574
CURRENT_FRAME_SLOT.with(|slot| {
6675
if slot.borrow().is_none() {
6776
let thread_id = crate::stdlib::thread::get_ident();
68-
let new_slot = Arc::new(parking_lot::Mutex::new(Vec::new()));
77+
let new_frame_slot = Arc::new(parking_lot::Mutex::new(Vec::new()));
78+
let new_exc_slot = Arc::new(crate::common::lock::PyMutex::new(None));
6979
vm.state
7080
.thread_frames
7181
.lock()
72-
.insert(thread_id, new_slot.clone());
73-
*slot.borrow_mut() = Some(new_slot);
82+
.insert(thread_id, new_frame_slot.clone());
83+
vm.state
84+
.thread_exceptions
85+
.lock()
86+
.insert(thread_id, new_exc_slot.clone());
87+
*slot.borrow_mut() = Some(new_frame_slot);
88+
CURRENT_EXCEPTION_SLOT.with(|es| {
89+
*es.borrow_mut() = Some(new_exc_slot);
90+
});
7491
}
7592
});
7693
}
@@ -109,14 +126,43 @@ pub fn get_current_frame() -> *const Frame {
109126
CURRENT_FRAME.with(|c| c.load(Ordering::Relaxed) as *const Frame)
110127
}
111128

129+
/// Update the current thread's exception slot via thread-local (no global lock).
130+
/// Called from push_exception/pop_exception/set_exception.
131+
#[cfg(feature = "threading")]
132+
pub fn sync_thread_exception(exc: Option<PyBaseExceptionRef>) {
133+
CURRENT_EXCEPTION_SLOT.with(|slot| {
134+
if let Some(s) = slot.borrow().as_ref() {
135+
*s.lock() = exc;
136+
}
137+
});
138+
}
139+
140+
/// Collect all threads' current exceptions for sys._current_exceptions().
141+
/// Only acquires the global registry lock briefly to clone slot references,
142+
/// then reads each slot individually.
143+
#[cfg(feature = "threading")]
144+
pub fn get_all_current_exceptions(
145+
vm: &VirtualMachine,
146+
) -> Vec<(u64, Option<PyBaseExceptionRef>)> {
147+
let registry = vm.state.thread_exceptions.lock();
148+
registry
149+
.iter()
150+
.map(|(id, slot)| (*id, slot.lock().clone()))
151+
.collect()
152+
}
153+
112154
/// Cleanup frame tracking for the current thread. Called at thread exit.
113155
#[cfg(feature = "threading")]
114156
pub fn cleanup_current_thread_frames(vm: &VirtualMachine) {
115157
let thread_id = crate::stdlib::thread::get_ident();
116158
vm.state.thread_frames.lock().remove(&thread_id);
159+
vm.state.thread_exceptions.lock().remove(&thread_id);
117160
CURRENT_FRAME_SLOT.with(|s| {
118161
*s.borrow_mut() = None;
119162
});
163+
CURRENT_EXCEPTION_SLOT.with(|s| {
164+
*s.borrow_mut() = None;
165+
});
120166
}
121167

122168
/// Reinitialize frame slot after fork. Called in child process.
@@ -144,10 +190,25 @@ pub fn reinit_frame_slot_after_fork(vm: &VirtualMachine) {
144190
registry.insert(current_ident, new_slot.clone());
145191
drop(registry);
146192

147-
// Update thread-local to point to the new slot
193+
let new_exc_slot = Arc::new(crate::common::lock::PyMutex::new(vm.topmost_exception()));
194+
let mut exc_registry = match vm.state.thread_exceptions.try_lock() {
195+
Some(guard) => guard,
196+
None => {
197+
unsafe { vm.state.thread_exceptions.force_unlock() };
198+
vm.state.thread_exceptions.lock()
199+
}
200+
};
201+
exc_registry.clear();
202+
exc_registry.insert(current_ident, new_exc_slot.clone());
203+
drop(exc_registry);
204+
205+
// Update thread-local to point to the new slots
148206
CURRENT_FRAME_SLOT.with(|s| {
149207
*s.borrow_mut() = Some(new_slot);
150208
});
209+
CURRENT_EXCEPTION_SLOT.with(|s| {
210+
*s.borrow_mut() = Some(new_exc_slot);
211+
});
151212
}
152213

153214
pub fn with_vm<F, R>(obj: &PyObject, f: F) -> Option<R>

0 commit comments

Comments
 (0)