@@ -6,9 +6,9 @@ use remoteprocess::{Pid, ProcessMemory};
66use serde_derive:: Serialize ;
77
88use crate :: config:: { Config , LineNo } ;
9- use crate :: python_data_access:: { copy_bytes, copy_string} ;
9+ use crate :: python_data_access:: { copy_bytes, copy_string, extract_type_name } ;
1010use crate :: python_interpreters:: {
11- CodeObject , FrameObject , InterpreterState , ThreadState , TupleObject ,
11+ CodeObject , FrameObject , InterpreterState , Object , ThreadState , TupleObject , TypeObject ,
1212} ;
1313
1414/// Call stack for a single python thread
@@ -68,6 +68,8 @@ pub struct ProcessInfo {
6868 pub parent : Option < Box < ProcessInfo > > ,
6969}
7070
71+ const PY_TPFLAGS_TYPE_SUBCLASS : usize = 1 << 31 ;
72+
7173/// Given an InterpreterState, this function returns a vector of stack traces for each thread
7274pub fn get_stack_traces < I , P > (
7375 interpreter_address : usize ,
@@ -90,13 +92,20 @@ where
9092
9193 let lineno = config. map ( |c| c. lineno ) . unwrap_or ( LineNo :: NoLine ) ;
9294 let dump_locals = config. map ( |c| c. dump_locals ) . unwrap_or ( 0 ) ;
95+ let include_class_name = config. map ( |c| c. include_class_name ) . unwrap_or ( false ) ;
9396
9497 while !threads. is_null ( ) {
9598 let thread = process
9699 . copy_pointer ( threads)
97100 . context ( "Failed to copy PyThreadState" ) ?;
98101
99- let mut trace = get_stack_trace ( & thread, process, dump_locals > 0 , lineno) ?;
102+ let mut trace = get_stack_trace :: < I , <I as InterpreterState >:: ThreadState , P > (
103+ & thread,
104+ process,
105+ dump_locals > 0 ,
106+ lineno,
107+ include_class_name,
108+ ) ?;
100109 trace. owns_gil = trace. thread_id == gil_thread_id;
101110
102111 ret. push ( trace) ;
@@ -110,13 +119,15 @@ where
110119}
111120
112121/// Gets a stack trace for an individual thread
113- pub fn get_stack_trace < T , P > (
122+ pub fn get_stack_trace < I , T , P > (
114123 thread : & T ,
115124 process : & P ,
116125 copy_locals : bool ,
117126 lineno : LineNo ,
127+ include_class_name : bool ,
118128) -> Result < StackTrace , Error >
119129where
130+ I : InterpreterState ,
120131 T : ThreadState ,
121132 P : ProcessMemory ,
122133{
@@ -165,7 +176,7 @@ where
165176 continue ;
166177 }
167178 let filename = filename?;
168- let name = name?;
179+ let mut name = name?;
169180
170181 // skip <shim> entries in python 3.12+
171182 // Unset file/function name in py3.13 means this is a shim.
@@ -195,11 +206,27 @@ where
195206 } ;
196207
197208 let locals = if copy_locals {
198- Some ( get_locals ( & code, frame_ptr, & frame, process) ?)
209+ Some ( get_locals ( & code, frame_ptr, & frame, process, false ) ?)
199210 } else {
200211 None
201212 } ;
202213
214+ // cls or self are always the first argument in methods and classmethods (and first local)
215+ if include_class_name && code. argcount ( ) > 0 {
216+ let found_locals = match locals {
217+ Some ( ref found_locals) => found_locals,
218+ None => & get_locals ( & code, frame_ptr, & frame, process, true ) ?,
219+ } ;
220+ // Errors copying self/cls type name are silenced.
221+ if let Some ( class_name) = found_locals
222+ . get ( 0 )
223+ . and_then ( |first_arg| get_class_name_from_arg :: < I , P > ( process, first_arg) . ok ( ) )
224+ . flatten ( )
225+ {
226+ name = format ! ( "{}.{}" , class_name, name) ;
227+ }
228+ }
229+
203230 let is_entry = frame. is_entry ( ) ;
204231
205232 frames. push ( Frame {
@@ -273,6 +300,7 @@ fn get_locals<C: CodeObject, F: FrameObject, P: ProcessMemory>(
273300 frameptr : * const F ,
274301 frame : & F ,
275302 process : & P ,
303+ first_var_only : bool ,
276304) -> Result < Vec < LocalVariable > , Error > {
277305 let local_count = code. nlocals ( ) as usize ;
278306 let argcount = code. argcount ( ) as usize ;
@@ -283,7 +311,12 @@ fn get_locals<C: CodeObject, F: FrameObject, P: ProcessMemory>(
283311
284312 let mut ret = Vec :: new ( ) ;
285313
286- for i in 0 ..local_count {
314+ let vars_to_copy = if first_var_only {
315+ std:: cmp:: min ( local_count, 1 )
316+ } else {
317+ local_count
318+ } ;
319+ for i in 0 ..vars_to_copy {
287320 let nameptr: * const C :: StringObject =
288321 process. copy_struct ( varnames. address ( code. varnames ( ) as usize , i) ) ?;
289322 let name = copy_string ( nameptr, process) ?;
@@ -301,6 +334,49 @@ fn get_locals<C: CodeObject, F: FrameObject, P: ProcessMemory>(
301334 Ok ( ret)
302335}
303336
337+ /// Get class from a `self` or `cls` argument, as long as its type matches expectations.
338+ fn get_class_name_from_arg < I , P > (
339+ process : & P ,
340+ first_local : & LocalVariable ,
341+ ) -> Result < Option < String > , Error >
342+ where
343+ I : InterpreterState ,
344+ P : ProcessMemory ,
345+ {
346+ // If the first variable isn't an argument, there are no arguments, so the fn isn't a normal
347+ // method or a class method.
348+ if !first_local. arg {
349+ return Ok ( None ) ;
350+ }
351+
352+ let first_arg_name = & first_local. name ;
353+ if first_arg_name != "self" && first_arg_name != "cls" {
354+ return Ok ( None ) ;
355+ }
356+
357+ let value: I :: Object = process. copy_struct ( first_local. addr ) ?;
358+ let mut value_type = process. copy_pointer ( value. ob_type ( ) ) ?;
359+ let is_type = value_type. flags ( ) & PY_TPFLAGS_TYPE_SUBCLASS != 0 ;
360+
361+ // validate that the first argument is:
362+ // - an instance of something else than `type` if it is called "self"
363+ // - an instance of `type` if it is called "cls"
364+ match ( first_arg_name. as_str ( ) , is_type) {
365+ ( "self" , false ) => { }
366+ ( "cls" , true ) => {
367+ // Copy the remote argument struct, but this time as PyTypeObject, rather than
368+ // PyObject. We needed to read type flags from PyObject to know that this is a
369+ // PyTypeObject.
370+ value_type = process. copy_struct ( first_local. addr ) ?;
371+ }
372+ _ => {
373+ return Ok ( None ) ;
374+ }
375+ }
376+
377+ Ok ( Some ( extract_type_name ( process, & value_type) ?) )
378+ }
379+
304380pub fn get_gil_threadid < I : InterpreterState , P : ProcessMemory > (
305381 threadstate_address : usize ,
306382 process : & P ,
0 commit comments