Skip to main content

rustpython_vm/protocol/
callable.rs

1use crate::{
2    builtins::{PyBoundMethod, PyFunction},
3    function::{FuncArgs, IntoFuncArgs},
4    types::{GenericMethod, VectorCallFunc},
5    {PyObject, PyObjectRef, PyResult, VirtualMachine},
6};
7
8impl PyObject {
9    #[inline]
10    #[must_use]
11    pub fn to_callable(&self) -> Option<PyCallable<'_>> {
12        PyCallable::new(self)
13    }
14
15    #[inline]
16    #[must_use]
17    pub fn is_callable(&self) -> bool {
18        self.to_callable().is_some()
19    }
20
21    /// PyObject_Call*Arg* series
22    #[inline]
23    pub fn call(&self, args: impl IntoFuncArgs, vm: &VirtualMachine) -> PyResult {
24        let args = args.into_args(vm);
25        self.call_with_args(args, vm)
26    }
27
28    /// PyObject_Call
29    pub fn call_with_args(&self, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
30        let Some(callable) = self.to_callable() else {
31            return Err(vm.new_type_error(format!(
32                "'{}' object is not callable",
33                self.class().slot_name()
34            )));
35        };
36        vm_trace!("Invoke: {:?} {:?}", callable, args);
37        callable.invoke(args, vm)
38    }
39
40    /// Vectorcall: call with owned positional args + optional kwnames.
41    /// Falls back to FuncArgs-based call if no vectorcall slot.
42    #[inline]
43    pub fn vectorcall(
44        &self,
45        args: Vec<PyObjectRef>,
46        nargs: usize,
47        kwnames: Option<&[PyObjectRef]>,
48        vm: &VirtualMachine,
49    ) -> PyResult {
50        let Some(callable) = self.to_callable() else {
51            return Err(vm.new_type_error(format!(
52                "'{}' object is not callable",
53                self.class().slot_name()
54            )));
55        };
56        callable.invoke_vectorcall(args, nargs, kwnames, vm)
57    }
58}
59
60#[derive(Debug)]
61pub struct PyCallable<'a> {
62    pub obj: &'a PyObject,
63    pub call: GenericMethod,
64    pub vectorcall: Option<VectorCallFunc>,
65}
66
67impl<'a> PyCallable<'a> {
68    pub fn new(obj: &'a PyObject) -> Option<Self> {
69        let slots = obj.class().slots();
70        let call = slots.call.load()?;
71        let vectorcall = slots.vectorcall.load();
72        Some(PyCallable {
73            obj,
74            call,
75            vectorcall,
76        })
77    }
78
79    pub fn invoke(&self, args: impl IntoFuncArgs, vm: &VirtualMachine) -> PyResult {
80        let args = args.into_args(vm);
81        if !vm.use_tracing.get() {
82            return (self.call)(self.obj, args, vm);
83        }
84        // Python functions get 'call'/'return' events from with_frame().
85        // Bound methods delegate to the inner callable, which fires its own events.
86        // All other callables (built-in functions, etc.) get 'c_call'/'c_return'/'c_exception'.
87        let is_python_callable = self.obj.downcast_ref::<PyFunction>().is_some()
88            || self.obj.downcast_ref::<PyBoundMethod>().is_some();
89        if is_python_callable {
90            (self.call)(self.obj, args, vm)
91        } else {
92            let callable = self.obj.to_owned();
93            vm.trace_event(TraceEvent::CCall, Some(callable.clone()))?;
94            let result = (self.call)(self.obj, args, vm);
95            if result.is_ok() {
96                vm.trace_event(TraceEvent::CReturn, Some(callable))?;
97            } else {
98                let _ = vm.trace_event(TraceEvent::CException, Some(callable));
99            }
100            result
101        }
102    }
103
104    /// Vectorcall dispatch: use vectorcall slot if available, else fall back to FuncArgs.
105    #[inline]
106    pub fn invoke_vectorcall(
107        &self,
108        args: Vec<PyObjectRef>,
109        nargs: usize,
110        kwnames: Option<&[PyObjectRef]>,
111        vm: &VirtualMachine,
112    ) -> PyResult {
113        if let Some(vc) = self.vectorcall {
114            if !vm.use_tracing.get() {
115                return vc(self.obj, args, nargs, kwnames, vm);
116            }
117            let is_python_callable = self.obj.downcast_ref::<PyFunction>().is_some()
118                || self.obj.downcast_ref::<PyBoundMethod>().is_some();
119            if is_python_callable {
120                vc(self.obj, args, nargs, kwnames, vm)
121            } else {
122                let callable = self.obj.to_owned();
123                vm.trace_event(TraceEvent::CCall, Some(callable.clone()))?;
124                let result = vc(self.obj, args, nargs, kwnames, vm);
125                if result.is_ok() {
126                    vm.trace_event(TraceEvent::CReturn, Some(callable))?;
127                } else {
128                    let _ = vm.trace_event(TraceEvent::CException, Some(callable));
129                }
130                result
131            }
132        } else {
133            // Fallback: convert owned Vec to FuncArgs (move, no clone)
134            let func_args = FuncArgs::from_vectorcall_owned(args, nargs, kwnames);
135            self.invoke(func_args, vm)
136        }
137    }
138}
139
140/// Trace events for sys.settrace and sys.setprofile.
141#[derive(Clone, Copy, Eq, PartialEq)]
142pub(crate) enum TraceEvent {
143    Call,
144    Return,
145    Exception,
146    Line,
147    Opcode,
148    CCall,
149    CReturn,
150    CException,
151}
152
153impl TraceEvent {
154    /// Whether sys.settrace receives this event.
155    #[must_use]
156    const fn is_trace_event(self) -> bool {
157        matches!(
158            self,
159            Self::Call | Self::Return | Self::Exception | Self::Line | Self::Opcode
160        )
161    }
162
163    /// Whether sys.setprofile receives this event.
164    /// In legacy_tracing.c, profile callbacks are only registered for
165    /// PY_RETURN, PY_UNWIND, C_CALL, C_RETURN, C_RAISE.
166    #[must_use]
167    const fn is_profile_event(self) -> bool {
168        matches!(
169            self,
170            Self::Call | Self::Return | Self::CCall | Self::CReturn | Self::CException
171        )
172    }
173
174    /// Whether this event is dispatched only when f_trace_opcodes is set.
175    #[must_use]
176    pub(crate) const fn is_opcode_event(self) -> bool {
177        matches!(self, Self::Opcode)
178    }
179
180    /// Default `what_event` for this legacy event.
181    #[must_use]
182    const fn default_what(self) -> crate::stdlib::sys::monitoring::MonitoringEvent {
183        use crate::stdlib::sys::monitoring::MonitoringEvent as Ev;
184        match self {
185            Self::Call => Ev::PyStart,
186            Self::Return => Ev::PyReturn,
187            Self::Exception => Ev::Raise,
188            Self::Line => Ev::Line,
189            Self::Opcode => Ev::Instruction,
190            Self::CCall => Ev::Call,
191            Self::CReturn => Ev::CReturn,
192            Self::CException => Ev::CRaise,
193        }
194    }
195}
196
197impl core::fmt::Display for TraceEvent {
198    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
199        match self {
200            Self::Call => write!(f, "call"),
201            Self::Return => write!(f, "return"),
202            Self::Exception => write!(f, "exception"),
203            Self::Line => write!(f, "line"),
204            Self::Opcode => write!(f, "opcode"),
205            Self::CCall => write!(f, "c_call"),
206            Self::CReturn => write!(f, "c_return"),
207            Self::CException => write!(f, "c_exception"),
208        }
209    }
210}
211
212impl VirtualMachine {
213    /// Call registered trace function.
214    ///
215    /// Returns the trace function's return value:
216    /// - `Some(obj)` if the trace function returned a non-None value
217    /// - `None` if it returned Python None or no trace function was active
218    ///
219    /// In CPython's trace protocol:
220    /// - For 'call' events: the return value determines the per-frame `f_trace`
221    /// - For 'line'/'return' events: the return value can update `f_trace`
222    #[inline]
223    pub(crate) fn trace_event(
224        &self,
225        event: TraceEvent,
226        arg: Option<PyObjectRef>,
227    ) -> PyResult<Option<PyObjectRef>> {
228        self.trace_event_what(event, event.default_what(), arg)
229    }
230
231    /// Like [`Self::trace_event`], but records `what` as `tstate->what_event`
232    /// for the duration of the callback (so `f_lineno` assignment can tell
233    /// a 'line' event from a 'call'/'return'/'exception').
234    #[inline]
235    pub(crate) fn trace_event_what(
236        &self,
237        event: TraceEvent,
238        what: crate::stdlib::sys::monitoring::MonitoringEvent,
239        arg: Option<PyObjectRef>,
240    ) -> PyResult<Option<PyObjectRef>> {
241        if self.use_tracing.get() && !self.tracing_is_suppressed() {
242            let old = self.what_event.replace(Some(what));
243            let result = self._trace_event_inner(event, arg);
244            self.what_event.set(old);
245            result
246        } else {
247            Ok(None)
248        }
249    }
250    fn _trace_event_inner(
251        &self,
252        event: TraceEvent,
253        arg: Option<PyObjectRef>,
254    ) -> PyResult<Option<PyObjectRef>> {
255        let trace_func = self.trace_func.borrow().to_owned();
256        let profile_func = self.profile_func.borrow().to_owned();
257        if self.is_none(&trace_func) && self.is_none(&profile_func) {
258            return Ok(None);
259        }
260
261        let is_trace_event = event.is_trace_event();
262        let is_profile_event = event.is_profile_event();
263        let is_opcode_event = event.is_opcode_event();
264
265        let Some(frame_ref) = crate::frame::current_thread_frame_materialize(self) else {
266            return Ok(None);
267        };
268
269        // Opcode events are only dispatched when f_trace_opcodes is set.
270        if is_opcode_event
271            && !frame_ref
272                .iframe()
273                .cold_opt()
274                .is_some_and(|c| *c.trace_opcodes.lock())
275        {
276            return Ok(None);
277        }
278
279        // trace_trampoline: CALL uses the global callback; every other
280        // event uses the per-frame f_trace (and is a no-op if that is unset).
281        let callback = if event == TraceEvent::Call {
282            if self.is_none(&trace_func) {
283                None
284            } else {
285                Some(trace_func)
286            }
287        } else {
288            frame_ref
289                .iframe()
290                .cold_opt()
291                .and_then(|c| c.trace.lock().clone())
292        };
293
294        let frame: PyObjectRef = frame_ref.to_owned().into();
295        let event_str: PyObjectRef = self.ctx.new_str(event.to_string()).into();
296        let args = vec![frame, event_str, arg.unwrap_or_else(|| self.ctx.none())];
297
298        let mut trace_result = None;
299
300        // temporarily disable tracing, during the call to the
301        // tracing function itself.
302        if is_trace_event && let Some(callback) = callback {
303            self.use_tracing.set(false);
304            self.enter_tracing();
305            let res = callback.call(args.clone(), self);
306            self.leave_tracing();
307            self.use_tracing.set(true);
308            match res {
309                Ok(result) => {
310                    if !self.is_none(&result) {
311                        *frame_ref.iframe().cold().trace.lock() = Some(result.clone());
312                        trace_result = Some(result);
313                    }
314                }
315                Err(e) => {
316                    // trace_trampoline: disable the global tracer and clear
317                    // this frame's f_trace, then propagate.
318                    *self.trace_func.borrow_mut() = self.ctx.none();
319                    *frame_ref.iframe().cold().trace.lock() = None;
320                    let profile_is_none = self.is_none(&self.profile_func.borrow());
321                    self.use_tracing.set(!profile_is_none);
322                    return Err(e);
323                }
324            }
325        }
326
327        if is_profile_event && !self.is_none(&profile_func) {
328            self.use_tracing.set(false);
329            self.enter_tracing();
330            let res = profile_func.call(args, self);
331            self.leave_tracing();
332            self.use_tracing.set(true);
333            if res.is_err() {
334                *self.profile_func.borrow_mut() = self.ctx.none();
335            }
336        }
337        Ok(trace_result)
338    }
339}