1use std::time::{Duration, Instant};
2
3pub struct PlatformStackWalker {
5 config: StackWalkConfig,
7 stats: WalkStats,
9 platform_context: PlatformContext,
11}
12
13#[derive(Debug, Clone)]
15pub struct StackWalkConfig {
16 pub max_depth: usize,
18 pub skip_frames: usize,
20 pub fast_unwind: bool,
22 pub collect_frame_info: bool,
24 pub max_walk_time: Duration,
26}
27
28#[derive(Debug)]
30struct PlatformContext {
31 initialized: bool,
33 #[cfg(target_os = "linux")]
35 linux_context: LinuxContext,
36 #[cfg(target_os = "windows")]
37 windows_context: WindowsContext,
38 #[cfg(target_os = "macos")]
39 macos_context: MacOSContext,
40}
41
42#[cfg(target_os = "linux")]
44#[derive(Debug)]
45struct LinuxContext {
46 libunwind_available: bool,
48 dwarf_available: bool,
50}
51
52#[cfg(target_os = "windows")]
54#[derive(Debug)]
55struct WindowsContext {
56 capture_available: bool,
58 symbols_initialized: bool,
60}
61
62#[cfg(target_os = "macos")]
64#[derive(Debug)]
65struct MacOSContext {
66 backtrace_available: bool,
68 dsym_available: bool,
70}
71
72#[derive(Debug)]
74struct WalkStats {
75 total_walks: std::sync::atomic::AtomicUsize,
77 total_frames: std::sync::atomic::AtomicUsize,
79 total_walk_time: std::sync::atomic::AtomicU64,
81 failed_walks: std::sync::atomic::AtomicUsize,
83}
84
85#[derive(Debug, Clone)]
87pub struct WalkResult {
88 pub success: bool,
90 pub frames: Vec<StackFrame>,
92 pub walk_time: Duration,
94 pub error: Option<WalkError>,
96}
97
98#[derive(Debug, Clone)]
100pub struct StackFrame {
101 pub ip: usize,
103 pub fp: Option<usize>,
105 pub sp: Option<usize>,
107 pub module_base: Option<usize>,
109 pub module_offset: Option<usize>,
111 pub symbol_info: Option<FrameSymbolInfo>,
113}
114
115#[derive(Debug, Clone)]
117pub struct FrameSymbolInfo {
118 pub name: String,
120 pub demangled_name: Option<String>,
122 pub file_name: Option<String>,
124 pub line_number: Option<u32>,
126}
127
128#[derive(Debug, Clone, PartialEq)]
130pub enum WalkError {
131 UnsupportedPlatform,
133 UnwindUnavailable,
135 InsufficientPermissions,
137 CorruptedStack,
139 Timeout,
141 MemoryError,
143 Unknown(String),
145}
146
147impl PlatformStackWalker {
148 pub fn new() -> Self {
150 Self {
151 config: StackWalkConfig::default(),
152 stats: WalkStats::new(),
153 platform_context: PlatformContext::new(),
154 }
155 }
156
157 pub fn initialize(&mut self) -> Result<(), WalkError> {
159 #[cfg(target_os = "linux")]
160 {
161 self.initialize_linux()
162 }
163
164 #[cfg(target_os = "windows")]
165 {
166 self.initialize_windows()
167 }
168
169 #[cfg(target_os = "macos")]
170 {
171 self.initialize_macos()
172 }
173
174 #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
175 {
176 Err(WalkError::UnsupportedPlatform)
177 }
178 }
179
180 pub fn walk_current_thread(&mut self) -> WalkResult {
182 let start_time = Instant::now();
183
184 if !self.platform_context.initialized {
185 return WalkResult {
186 success: false,
187 frames: Vec::new(),
188 walk_time: start_time.elapsed(),
189 error: Some(WalkError::UnwindUnavailable),
190 };
191 }
192
193 let result = self.perform_stack_walk();
194 let walk_time = start_time.elapsed();
195
196 self.stats
198 .total_walks
199 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
200 if result.success {
201 self.stats
202 .total_frames
203 .fetch_add(result.frames.len(), std::sync::atomic::Ordering::Relaxed);
204 } else {
205 self.stats
206 .failed_walks
207 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
208 }
209 self.stats.total_walk_time.fetch_add(
210 walk_time.as_nanos() as u64,
211 std::sync::atomic::Ordering::Relaxed,
212 );
213
214 WalkResult {
215 success: result.success,
216 frames: result.frames,
217 walk_time,
218 error: result.error,
219 }
220 }
221
222 pub fn walk_thread(&mut self, thread_id: u32) -> WalkResult {
224 #[cfg(target_os = "linux")]
226 {
227 self.walk_linux_thread(thread_id)
228 }
229
230 #[cfg(target_os = "windows")]
231 {
232 self.walk_windows_thread(thread_id)
233 }
234
235 #[cfg(target_os = "macos")]
236 {
237 self.walk_macos_thread(thread_id)
238 }
239
240 #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
241 {
242 WalkResult {
243 success: false,
244 frames: Vec::new(),
245 walk_time: Duration::ZERO,
246 error: Some(WalkError::UnsupportedPlatform),
247 }
248 }
249 }
250
251 pub fn get_statistics(&self) -> WalkStatistics {
253 let total_walks = self
254 .stats
255 .total_walks
256 .load(std::sync::atomic::Ordering::Relaxed);
257 let total_frames = self
258 .stats
259 .total_frames
260 .load(std::sync::atomic::Ordering::Relaxed);
261 let total_time_ns = self
262 .stats
263 .total_walk_time
264 .load(std::sync::atomic::Ordering::Relaxed);
265 let failed_walks = self
266 .stats
267 .failed_walks
268 .load(std::sync::atomic::Ordering::Relaxed);
269
270 WalkStatistics {
271 total_walks,
272 successful_walks: total_walks.saturating_sub(failed_walks),
273 failed_walks,
274 total_frames_collected: total_frames,
275 average_frames_per_walk: if total_walks > 0 {
276 total_frames as f64 / total_walks as f64
277 } else {
278 0.0
279 },
280 average_walk_time: if total_walks > 0 {
281 Duration::from_nanos(total_time_ns / total_walks as u64)
282 } else {
283 Duration::ZERO
284 },
285 success_rate: if total_walks > 0 {
286 (total_walks - failed_walks) as f64 / total_walks as f64
287 } else {
288 0.0
289 },
290 }
291 }
292
293 pub fn update_config(&mut self, config: StackWalkConfig) {
295 self.config = config;
296 }
297
298 fn perform_stack_walk(&self) -> WalkResult {
299 let mut frames = Vec::with_capacity(self.config.max_depth);
300 let start_time = Instant::now();
301
302 #[cfg(target_os = "linux")]
303 let result = self.walk_linux_stack(&mut frames);
304
305 #[cfg(target_os = "windows")]
306 let result = self.walk_windows_stack(&mut frames);
307
308 #[cfg(target_os = "macos")]
309 let result = self.walk_macos_stack(&mut frames);
310
311 #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
312 let result = Err(WalkError::UnsupportedPlatform);
313
314 match result {
315 Ok(()) => WalkResult {
316 success: true,
317 frames,
318 walk_time: start_time.elapsed(),
319 error: None,
320 },
321 Err(error) => WalkResult {
322 success: false,
323 frames,
324 walk_time: start_time.elapsed(),
325 error: Some(error),
326 },
327 }
328 }
329
330 #[cfg(target_os = "linux")]
331 fn initialize_linux(&mut self) -> Result<(), WalkError> {
332 self.platform_context.linux_context.libunwind_available = true; self.platform_context.linux_context.dwarf_available = true; self.platform_context.initialized = true;
336 Ok(())
337 }
338
339 #[cfg(target_os = "windows")]
340 fn initialize_windows(&mut self) -> Result<(), WalkError> {
341 self.platform_context.windows_context.capture_available = true; self.platform_context.windows_context.symbols_initialized = true; self.platform_context.initialized = true;
345 Ok(())
346 }
347
348 #[cfg(target_os = "macos")]
349 fn initialize_macos(&mut self) -> Result<(), WalkError> {
350 self.platform_context.macos_context.backtrace_available = true; self.platform_context.macos_context.dsym_available = true; self.platform_context.initialized = true;
354 Ok(())
355 }
356
357 #[cfg(target_os = "linux")]
358 fn walk_linux_stack(&self, frames: &mut Vec<StackFrame>) -> Result<(), WalkError> {
359 #[cfg(feature = "backtrace")]
360 {
361 use backtrace::Backtrace;
362 let bt = Backtrace::new();
363
364 for (i, frame) in bt.frames().iter().enumerate() {
365 if frames.len() >= self.config.max_depth {
366 break;
367 }
368
369 if i < self.config.skip_frames {
370 continue;
371 }
372
373 let ip = frame.ip() as usize;
374
375 frames.push(StackFrame {
376 ip,
377 fp: None,
378 sp: None,
379 module_base: None,
380 module_offset: None,
381 symbol_info: frame.symbols().first().map(|sym| FrameSymbolInfo {
382 name: sym
383 .name()
384 .map(|n| format!("{:?}", n))
385 .unwrap_or_else(|| format!("unknown_symbol_{i}")),
386 demangled_name: sym.name().map(|n| format!("{:?}", n)),
387 file_name: sym.filename().map(|f| f.display().to_string()),
388 line_number: sym.lineno(),
389 }),
390 });
391 }
392
393 if !frames.is_empty() {
394 return Ok(());
395 }
396 }
397
398 #[cfg(not(feature = "backtrace"))]
399 {
400 self.walk_linux_stack_fallback(frames)?;
401 }
402
403 Ok(())
404 }
405
406 #[cfg(target_os = "linux")]
407 #[allow(dead_code)] fn walk_linux_stack_fallback(&self, frames: &mut Vec<StackFrame>) -> Result<(), WalkError> {
409 use libc::{backtrace, c_void};
410
411 struct FrameData {
412 frames: *mut Vec<StackFrame>,
413 max_depth: usize,
414 skip_frames: usize,
415 count: usize,
416 }
417
418 extern "C" fn callback(
419 data: *mut c_void,
420 ip: libc::uintptr_t,
421 _sp: libc::uintptr_t,
422 _fp: libc::uintptr_t,
423 ) -> libc::c_int {
424 let frame_data = match unsafe { (data as *mut FrameData).as_mut() } {
425 Some(fd) => fd,
426 None => return -1,
427 };
428
429 if frame_data.count < frame_data.skip_frames {
430 frame_data.count += 1;
431 return 0;
432 }
433
434 if frame_data.frames.is_null() {
435 return -1;
436 }
437
438 let frames = unsafe { &mut *frame_data.frames };
439 if frames.len() < frame_data.max_depth {
440 frames.push(StackFrame {
441 ip,
442 fp: None,
443 sp: None,
444 module_base: None,
445 module_offset: None,
446 symbol_info: None,
447 });
448 }
449
450 0
451 }
452
453 let mut frame_data = FrameData {
454 frames: frames as *mut Vec<StackFrame>,
455 max_depth: self.config.max_depth,
456 skip_frames: self.config.skip_frames,
457 count: 0,
458 };
459
460 unsafe {
461 let mut buffer: [*mut libc::c_void; 64] = [std::ptr::null_mut(); 64];
462 let result = backtrace(buffer.as_mut_ptr(), buffer.len() as libc::c_int);
463 if result > 0 {
464 for i in 0..result {
465 let ip = buffer[i as usize] as libc::uintptr_t;
466 let _ = callback(&mut frame_data as *mut FrameData as *mut c_void, ip, 0, 0);
467 }
468 }
469 }
470
471 Ok(())
472 }
473
474 #[cfg(target_os = "windows")]
475 #[cfg(target_os = "windows")]
476 fn walk_windows_stack(&self, _frames: &mut Vec<StackFrame>) -> Result<(), WalkError> {
477 #[cfg(feature = "backtrace")]
490 {
491 use backtrace::Backtrace;
492 let bt = Backtrace::new();
493
494 for (i, frame) in bt.frames().iter().enumerate() {
495 if _frames.len() >= self.config.max_depth {
496 break;
497 }
498
499 if i < self.config.skip_frames {
500 continue;
501 }
502
503 let ip = frame.ip() as usize;
504
505 _frames.push(StackFrame {
506 ip,
507 fp: None,
508 sp: None,
509 module_base: None,
510 module_offset: None,
511 symbol_info: frame.symbols().first().map(|sym| FrameSymbolInfo {
512 name: sym
513 .name()
514 .map(|n| format!("{:?}", n))
515 .unwrap_or_else(|| format!("unknown_symbol_{i}")),
516 demangled_name: sym.name().map(|n| format!("{:?}", n)),
517 file_name: sym.filename().map(|f| f.display().to_string()),
518 line_number: sym.lineno(),
519 }),
520 });
521 }
522
523 if !_frames.is_empty() {
524 return Ok(());
525 }
526 }
527
528 tracing::warn!("Using simulated stack walking on Windows. Consider enabling 'backtrace' feature for accurate results.");
530
531 tracing::warn!("Unable to capture stack frames on Windows. Backtrace feature may not be enabled or failed.");
533 Ok(())
534 }
535
536 #[cfg(target_os = "macos")]
537 fn walk_macos_stack(&self, frames: &mut Vec<StackFrame>) -> Result<(), WalkError> {
538 use libc::{backtrace, c_void};
539
540 const MAX_FRAMES: usize = 64;
541 let mut buffer: Vec<*mut c_void> = vec![std::ptr::null_mut(); MAX_FRAMES];
542
543 let num_frames: i32 = unsafe { backtrace(buffer.as_mut_ptr(), MAX_FRAMES as i32) };
544
545 if num_frames <= 0 {
546 return Ok(());
547 }
548
549 let num_frames_usize = num_frames as usize;
550
551 for (i, &frame) in buffer.iter().take(num_frames_usize).enumerate() {
552 if frames.len() >= self.config.max_depth {
553 break;
554 }
555
556 if i < self.config.skip_frames {
557 continue;
558 }
559
560 let ip = frame as usize;
561
562 frames.push(StackFrame {
563 ip,
564 fp: None,
565 sp: None,
566 module_base: None,
567 module_offset: None,
568 symbol_info: None,
569 });
570 }
571
572 Ok(())
573 }
574
575 #[cfg(target_os = "linux")]
576 fn walk_linux_thread(&self, _thread_id: u32) -> WalkResult {
577 WalkResult {
580 success: false,
581 frames: Vec::new(),
582 walk_time: Duration::ZERO,
583 error: Some(WalkError::Unknown(
584 "Thread-specific stack walking is not yet implemented on Linux. Would require ptrace or reading /proc/[pid]/task/[tid]/stack.".to_string(),
585 )),
586 }
587 }
588
589 #[cfg(target_os = "windows")]
590 fn walk_windows_thread(&self, _thread_id: u32) -> WalkResult {
591 WalkResult {
594 success: false,
595 frames: Vec::new(),
596 walk_time: Duration::ZERO,
597 error: Some(WalkError::Unknown(
598 "Thread-specific stack walking is not yet implemented on Windows. Would require OpenThread and StackWalk64 API integration.".to_string(),
599 )),
600 }
601 }
602
603 #[cfg(target_os = "macos")]
604 fn walk_macos_thread(&self, _thread_id: u32) -> WalkResult {
605 WalkResult {
608 success: false,
609 frames: Vec::new(),
610 walk_time: Duration::ZERO,
611 error: Some(WalkError::Unknown(
612 "Thread-specific stack walking is not yet implemented on macOS. Would require thread_get_state API integration.".to_string(),
613 )),
614 }
615 }
616}
617
618impl PlatformContext {
619 fn new() -> Self {
620 Self {
621 initialized: false,
622 #[cfg(target_os = "linux")]
623 linux_context: LinuxContext {
624 libunwind_available: false,
625 dwarf_available: false,
626 },
627 #[cfg(target_os = "windows")]
628 windows_context: WindowsContext {
629 capture_available: false,
630 symbols_initialized: false,
631 },
632 #[cfg(target_os = "macos")]
633 macos_context: MacOSContext {
634 backtrace_available: false,
635 dsym_available: false,
636 },
637 }
638 }
639}
640
641impl WalkStats {
642 fn new() -> Self {
643 Self {
644 total_walks: std::sync::atomic::AtomicUsize::new(0),
645 total_frames: std::sync::atomic::AtomicUsize::new(0),
646 total_walk_time: std::sync::atomic::AtomicU64::new(0),
647 failed_walks: std::sync::atomic::AtomicUsize::new(0),
648 }
649 }
650}
651
652#[derive(Debug, Clone)]
654pub struct WalkStatistics {
655 pub total_walks: usize,
657 pub successful_walks: usize,
659 pub failed_walks: usize,
661 pub total_frames_collected: usize,
663 pub average_frames_per_walk: f64,
665 pub average_walk_time: Duration,
667 pub success_rate: f64,
669}
670
671impl Default for StackWalkConfig {
672 fn default() -> Self {
673 Self {
674 max_depth: 32,
675 skip_frames: 2,
676 fast_unwind: true,
677 collect_frame_info: true,
678 max_walk_time: Duration::from_millis(10),
679 }
680 }
681}
682
683impl Default for PlatformStackWalker {
684 fn default() -> Self {
685 Self::new()
686 }
687}
688
689impl std::fmt::Display for WalkError {
690 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
691 match self {
692 WalkError::UnsupportedPlatform => write!(f, "Platform not supported for stack walking"),
693 WalkError::UnwindUnavailable => write!(f, "Stack unwinding library not available"),
694 WalkError::InsufficientPermissions => {
695 write!(f, "Insufficient permissions for stack walking")
696 }
697 WalkError::CorruptedStack => write!(f, "Stack appears to be corrupted"),
698 WalkError::Timeout => write!(f, "Stack walk timed out"),
699 WalkError::MemoryError => write!(f, "Memory access error during stack walk"),
700 WalkError::Unknown(msg) => write!(f, "Unknown error: {}", msg),
701 }
702 }
703}
704
705impl std::error::Error for WalkError {}
706
707#[cfg(test)]
708mod tests {
709 use super::*;
710
711 #[test]
712 fn test_platform_stack_walker_creation() {
713 let walker = PlatformStackWalker::new();
714 assert!(!walker.platform_context.initialized);
715
716 let stats = walker.get_statistics();
717 assert_eq!(stats.total_walks, 0);
718 assert_eq!(stats.success_rate, 0.0);
719 }
720
721 #[test]
722 fn test_stack_walk_config() {
723 let config = StackWalkConfig::default();
724 assert_eq!(config.max_depth, 32);
725 assert_eq!(config.skip_frames, 2);
726 assert!(config.fast_unwind);
727 assert!(config.collect_frame_info);
728 }
729
730 #[test]
731 fn test_initialization() {
732 let mut walker = PlatformStackWalker::new();
733 let result = walker.initialize();
734
735 #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
737 assert!(result.is_ok());
738
739 #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
740 assert_eq!(result, Err(WalkError::UnsupportedPlatform));
741 }
742
743 #[test]
744 fn test_current_thread_walk() {
745 let mut walker = PlatformStackWalker::new();
746 let _ = walker.initialize();
747
748 let result = walker.walk_current_thread();
749
750 #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
751 {
752 if walker.platform_context.initialized {
753 assert!(result.success);
754 #[cfg(any(target_os = "linux", target_os = "macos"))]
755 {
756 assert!(
757 !result.frames.is_empty(),
758 "Native backtrace should produce frames on Linux/macOS"
759 );
760 assert!(result.walk_time > Duration::ZERO);
761 }
762 }
763 }
764 }
765
766 #[cfg(target_os = "macos")]
767 #[test]
768 fn test_macos_native_backtrace() {
769 let mut walker = PlatformStackWalker::new();
770 let _ = walker.initialize();
771
772 let mut frames = Vec::new();
773 let result = walker.walk_macos_stack(&mut frames);
774
775 assert!(result.is_ok());
776 assert!(
777 !frames.is_empty(),
778 "macOS native backtrace should produce at least the test frame"
779 );
780
781 for frame in &frames {
782 assert!(
783 frame.ip > 0,
784 "Each frame should have a valid instruction pointer"
785 );
786 }
787 }
788
789 #[cfg(target_os = "macos")]
790 #[test]
791 fn test_macos_backtrace_respects_max_depth() {
792 let mut walker = PlatformStackWalker::new();
793 walker.config.max_depth = 3;
794
795 let mut frames = Vec::new();
796 let _ = walker.walk_macos_stack(&mut frames);
797
798 assert!(
799 frames.len() <= 3,
800 "Should respect max_depth limit, got {} frames",
801 frames.len()
802 );
803 }
804
805 #[cfg(target_os = "macos")]
806 #[test]
807 fn test_macos_backtrace_respects_skip_frames() {
808 let mut walker = PlatformStackWalker::new();
809 walker.config.skip_frames = 2;
810
811 let mut frames = Vec::new();
812 let _ = walker.walk_macos_stack(&mut frames);
813
814 if frames.len() > 2 {
815 assert!(
816 frames[0].ip != frames[1].ip || frames.len() < 3,
817 "First frames should be skipped"
818 );
819 }
820 }
821
822 #[test]
823 fn test_frame_information() {
824 let frame = StackFrame {
825 ip: 0x12345678,
826 fp: Some(0x7fff0000),
827 sp: Some(0x7fff0008),
828 module_base: Some(0x12340000),
829 module_offset: Some(0x5678),
830 symbol_info: Some(FrameSymbolInfo {
831 name: "test_function".to_string(),
832 demangled_name: Some("namespace::test_function".to_string()),
833 file_name: Some("test.rs".to_string()),
834 line_number: Some(42),
835 }),
836 };
837
838 assert_eq!(frame.ip, 0x12345678);
839 assert_eq!(frame.fp, Some(0x7fff0000));
840 assert!(frame.symbol_info.is_some());
841
842 let symbol = frame
843 .symbol_info
844 .as_ref()
845 .expect("Symbol info should exist");
846 assert_eq!(symbol.name, "test_function");
847 assert_eq!(symbol.line_number, Some(42));
848 }
849}