use std::time::{Duration, Instant};
pub struct PlatformStackWalker {
config: StackWalkConfig,
stats: WalkStats,
platform_context: PlatformContext,
}
#[derive(Debug, Clone)]
pub struct StackWalkConfig {
pub max_depth: usize,
pub skip_frames: usize,
pub fast_unwind: bool,
pub collect_frame_info: bool,
pub max_walk_time: Duration,
}
#[derive(Debug)]
struct PlatformContext {
initialized: bool,
#[cfg(target_os = "linux")]
linux_context: LinuxContext,
#[cfg(target_os = "windows")]
windows_context: WindowsContext,
#[cfg(target_os = "macos")]
macos_context: MacOSContext,
}
#[cfg(target_os = "linux")]
#[derive(Debug)]
struct LinuxContext {
libunwind_available: bool,
dwarf_available: bool,
}
#[cfg(target_os = "windows")]
#[derive(Debug)]
struct WindowsContext {
capture_available: bool,
symbols_initialized: bool,
}
#[cfg(target_os = "macos")]
#[derive(Debug)]
struct MacOSContext {
backtrace_available: bool,
dsym_available: bool,
}
#[derive(Debug)]
struct WalkStats {
total_walks: std::sync::atomic::AtomicUsize,
total_frames: std::sync::atomic::AtomicUsize,
total_walk_time: std::sync::atomic::AtomicU64,
failed_walks: std::sync::atomic::AtomicUsize,
}
#[derive(Debug, Clone)]
pub struct WalkResult {
pub success: bool,
pub frames: Vec<StackFrame>,
pub walk_time: Duration,
pub error: Option<WalkError>,
}
#[derive(Debug, Clone)]
pub struct StackFrame {
pub ip: usize,
pub fp: Option<usize>,
pub sp: Option<usize>,
pub module_base: Option<usize>,
pub module_offset: Option<usize>,
pub symbol_info: Option<FrameSymbolInfo>,
}
#[derive(Debug, Clone)]
pub struct FrameSymbolInfo {
pub name: String,
pub demangled_name: Option<String>,
pub file_name: Option<String>,
pub line_number: Option<u32>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum WalkError {
UnsupportedPlatform,
UnwindUnavailable,
InsufficientPermissions,
CorruptedStack,
Timeout,
MemoryError,
Unknown(String),
}
impl PlatformStackWalker {
pub fn new() -> Self {
Self {
config: StackWalkConfig::default(),
stats: WalkStats::new(),
platform_context: PlatformContext::new(),
}
}
pub fn initialize(&mut self) -> Result<(), WalkError> {
#[cfg(target_os = "linux")]
{
self.initialize_linux()
}
#[cfg(target_os = "windows")]
{
self.initialize_windows()
}
#[cfg(target_os = "macos")]
{
self.initialize_macos()
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{
Err(WalkError::UnsupportedPlatform)
}
}
pub fn walk_current_thread(&mut self) -> WalkResult {
let start_time = Instant::now();
if !self.platform_context.initialized {
return WalkResult {
success: false,
frames: Vec::new(),
walk_time: start_time.elapsed(),
error: Some(WalkError::UnwindUnavailable),
};
}
let result = self.perform_stack_walk();
let walk_time = start_time.elapsed();
self.stats
.total_walks
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if result.success {
self.stats
.total_frames
.fetch_add(result.frames.len(), std::sync::atomic::Ordering::Relaxed);
} else {
self.stats
.failed_walks
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
self.stats.total_walk_time.fetch_add(
walk_time.as_nanos() as u64,
std::sync::atomic::Ordering::Relaxed,
);
WalkResult {
success: result.success,
frames: result.frames,
walk_time,
error: result.error,
}
}
pub fn walk_thread(&mut self, thread_id: u32) -> WalkResult {
#[cfg(target_os = "linux")]
{
self.walk_linux_thread(thread_id)
}
#[cfg(target_os = "windows")]
{
self.walk_windows_thread(thread_id)
}
#[cfg(target_os = "macos")]
{
self.walk_macos_thread(thread_id)
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{
WalkResult {
success: false,
frames: Vec::new(),
walk_time: Duration::ZERO,
error: Some(WalkError::UnsupportedPlatform),
}
}
}
pub fn get_statistics(&self) -> WalkStatistics {
let total_walks = self
.stats
.total_walks
.load(std::sync::atomic::Ordering::Relaxed);
let total_frames = self
.stats
.total_frames
.load(std::sync::atomic::Ordering::Relaxed);
let total_time_ns = self
.stats
.total_walk_time
.load(std::sync::atomic::Ordering::Relaxed);
let failed_walks = self
.stats
.failed_walks
.load(std::sync::atomic::Ordering::Relaxed);
WalkStatistics {
total_walks,
successful_walks: total_walks.saturating_sub(failed_walks),
failed_walks,
total_frames_collected: total_frames,
average_frames_per_walk: if total_walks > 0 {
total_frames as f64 / total_walks as f64
} else {
0.0
},
average_walk_time: if total_walks > 0 {
Duration::from_nanos(total_time_ns / total_walks as u64)
} else {
Duration::ZERO
},
success_rate: if total_walks > 0 {
(total_walks - failed_walks) as f64 / total_walks as f64
} else {
0.0
},
}
}
pub fn update_config(&mut self, config: StackWalkConfig) {
self.config = config;
}
fn perform_stack_walk(&self) -> WalkResult {
let mut frames = Vec::with_capacity(self.config.max_depth);
let start_time = Instant::now();
#[cfg(target_os = "linux")]
let result = self.walk_linux_stack(&mut frames);
#[cfg(target_os = "windows")]
let result = self.walk_windows_stack(&mut frames);
#[cfg(target_os = "macos")]
let result = self.walk_macos_stack(&mut frames);
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
let result = Err(WalkError::UnsupportedPlatform);
match result {
Ok(()) => WalkResult {
success: true,
frames,
walk_time: start_time.elapsed(),
error: None,
},
Err(error) => WalkResult {
success: false,
frames,
walk_time: start_time.elapsed(),
error: Some(error),
},
}
}
#[cfg(target_os = "linux")]
fn initialize_linux(&mut self) -> Result<(), WalkError> {
self.platform_context.linux_context.libunwind_available = true; self.platform_context.linux_context.dwarf_available = true; self.platform_context.initialized = true;
Ok(())
}
#[cfg(target_os = "windows")]
fn initialize_windows(&mut self) -> Result<(), WalkError> {
self.platform_context.windows_context.capture_available = true; self.platform_context.windows_context.symbols_initialized = true; self.platform_context.initialized = true;
Ok(())
}
#[cfg(target_os = "macos")]
fn initialize_macos(&mut self) -> Result<(), WalkError> {
self.platform_context.macos_context.backtrace_available = true; self.platform_context.macos_context.dsym_available = true; self.platform_context.initialized = true;
Ok(())
}
#[cfg(target_os = "linux")]
fn walk_linux_stack(&self, frames: &mut Vec<StackFrame>) -> Result<(), WalkError> {
#[cfg(feature = "backtrace")]
{
use backtrace::Backtrace;
let bt = Backtrace::new();
for (i, frame) in bt.frames().iter().enumerate() {
if frames.len() >= self.config.max_depth {
break;
}
if i < self.config.skip_frames {
continue;
}
let ip = frame.ip() as usize;
frames.push(StackFrame {
ip,
fp: None,
sp: None,
module_base: None,
module_offset: None,
symbol_info: frame.symbols().first().map(|sym| FrameSymbolInfo {
name: sym
.name()
.map(|n| format!("{:?}", n))
.unwrap_or_else(|| format!("unknown_symbol_{i}")),
demangled_name: sym.name().map(|n| format!("{:?}", n)),
file_name: sym.filename().map(|f| f.display().to_string()),
line_number: sym.lineno(),
}),
});
}
if !frames.is_empty() {
return Ok(());
}
}
#[cfg(not(feature = "backtrace"))]
{
self.walk_linux_stack_fallback(frames)?;
}
Ok(())
}
#[cfg(target_os = "linux")]
#[allow(dead_code)] fn walk_linux_stack_fallback(&self, frames: &mut Vec<StackFrame>) -> Result<(), WalkError> {
use libc::{backtrace, c_void};
struct FrameData {
frames: *mut Vec<StackFrame>,
max_depth: usize,
skip_frames: usize,
count: usize,
}
extern "C" fn callback(
data: *mut c_void,
ip: libc::uintptr_t,
_sp: libc::uintptr_t,
_fp: libc::uintptr_t,
) -> libc::c_int {
let frame_data = match unsafe { (data as *mut FrameData).as_mut() } {
Some(fd) => fd,
None => return -1,
};
if frame_data.count < frame_data.skip_frames {
frame_data.count += 1;
return 0;
}
if frame_data.frames.is_null() {
return -1;
}
let frames = unsafe { &mut *frame_data.frames };
if frames.len() < frame_data.max_depth {
frames.push(StackFrame {
ip,
fp: None,
sp: None,
module_base: None,
module_offset: None,
symbol_info: None,
});
}
0
}
let mut frame_data = FrameData {
frames: frames as *mut Vec<StackFrame>,
max_depth: self.config.max_depth,
skip_frames: self.config.skip_frames,
count: 0,
};
unsafe {
let mut buffer: [*mut libc::c_void; 64] = [std::ptr::null_mut(); 64];
let result = backtrace(buffer.as_mut_ptr(), buffer.len() as libc::c_int);
if result > 0 {
for i in 0..result {
let ip = buffer[i as usize] as libc::uintptr_t;
let _ = callback(&mut frame_data as *mut FrameData as *mut c_void, ip, 0, 0);
}
}
}
Ok(())
}
#[cfg(target_os = "windows")]
#[cfg(target_os = "windows")]
fn walk_windows_stack(&self, _frames: &mut Vec<StackFrame>) -> Result<(), WalkError> {
#[cfg(feature = "backtrace")]
{
use backtrace::Backtrace;
let bt = Backtrace::new();
for (i, frame) in bt.frames().iter().enumerate() {
if _frames.len() >= self.config.max_depth {
break;
}
if i < self.config.skip_frames {
continue;
}
let ip = frame.ip() as usize;
_frames.push(StackFrame {
ip,
fp: None,
sp: None,
module_base: None,
module_offset: None,
symbol_info: frame.symbols().first().map(|sym| FrameSymbolInfo {
name: sym
.name()
.map(|n| format!("{:?}", n))
.unwrap_or_else(|| format!("unknown_symbol_{i}")),
demangled_name: sym.name().map(|n| format!("{:?}", n)),
file_name: sym.filename().map(|f| f.display().to_string()),
line_number: sym.lineno(),
}),
});
}
if !_frames.is_empty() {
return Ok(());
}
}
tracing::warn!("Using simulated stack walking on Windows. Consider enabling 'backtrace' feature for accurate results.");
tracing::warn!("Unable to capture stack frames on Windows. Backtrace feature may not be enabled or failed.");
Ok(())
}
#[cfg(target_os = "macos")]
fn walk_macos_stack(&self, frames: &mut Vec<StackFrame>) -> Result<(), WalkError> {
use libc::{backtrace, c_void};
const MAX_FRAMES: usize = 64;
let mut buffer: Vec<*mut c_void> = vec![std::ptr::null_mut(); MAX_FRAMES];
let num_frames: i32 = unsafe { backtrace(buffer.as_mut_ptr(), MAX_FRAMES as i32) };
if num_frames <= 0 {
return Ok(());
}
let num_frames_usize = num_frames as usize;
for (i, &frame) in buffer.iter().take(num_frames_usize).enumerate() {
if frames.len() >= self.config.max_depth {
break;
}
if i < self.config.skip_frames {
continue;
}
let ip = frame as usize;
frames.push(StackFrame {
ip,
fp: None,
sp: None,
module_base: None,
module_offset: None,
symbol_info: None,
});
}
Ok(())
}
#[cfg(target_os = "linux")]
fn walk_linux_thread(&self, _thread_id: u32) -> WalkResult {
WalkResult {
success: false,
frames: Vec::new(),
walk_time: Duration::ZERO,
error: Some(WalkError::Unknown(
"Thread-specific stack walking is not yet implemented on Linux. Would require ptrace or reading /proc/[pid]/task/[tid]/stack.".to_string(),
)),
}
}
#[cfg(target_os = "windows")]
fn walk_windows_thread(&self, _thread_id: u32) -> WalkResult {
WalkResult {
success: false,
frames: Vec::new(),
walk_time: Duration::ZERO,
error: Some(WalkError::Unknown(
"Thread-specific stack walking is not yet implemented on Windows. Would require OpenThread and StackWalk64 API integration.".to_string(),
)),
}
}
#[cfg(target_os = "macos")]
fn walk_macos_thread(&self, _thread_id: u32) -> WalkResult {
WalkResult {
success: false,
frames: Vec::new(),
walk_time: Duration::ZERO,
error: Some(WalkError::Unknown(
"Thread-specific stack walking is not yet implemented on macOS. Would require thread_get_state API integration.".to_string(),
)),
}
}
}
impl PlatformContext {
fn new() -> Self {
Self {
initialized: false,
#[cfg(target_os = "linux")]
linux_context: LinuxContext {
libunwind_available: false,
dwarf_available: false,
},
#[cfg(target_os = "windows")]
windows_context: WindowsContext {
capture_available: false,
symbols_initialized: false,
},
#[cfg(target_os = "macos")]
macos_context: MacOSContext {
backtrace_available: false,
dsym_available: false,
},
}
}
}
impl WalkStats {
fn new() -> Self {
Self {
total_walks: std::sync::atomic::AtomicUsize::new(0),
total_frames: std::sync::atomic::AtomicUsize::new(0),
total_walk_time: std::sync::atomic::AtomicU64::new(0),
failed_walks: std::sync::atomic::AtomicUsize::new(0),
}
}
}
#[derive(Debug, Clone)]
pub struct WalkStatistics {
pub total_walks: usize,
pub successful_walks: usize,
pub failed_walks: usize,
pub total_frames_collected: usize,
pub average_frames_per_walk: f64,
pub average_walk_time: Duration,
pub success_rate: f64,
}
impl Default for StackWalkConfig {
fn default() -> Self {
Self {
max_depth: 32,
skip_frames: 2,
fast_unwind: true,
collect_frame_info: true,
max_walk_time: Duration::from_millis(10),
}
}
}
impl Default for PlatformStackWalker {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for WalkError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WalkError::UnsupportedPlatform => write!(f, "Platform not supported for stack walking"),
WalkError::UnwindUnavailable => write!(f, "Stack unwinding library not available"),
WalkError::InsufficientPermissions => {
write!(f, "Insufficient permissions for stack walking")
}
WalkError::CorruptedStack => write!(f, "Stack appears to be corrupted"),
WalkError::Timeout => write!(f, "Stack walk timed out"),
WalkError::MemoryError => write!(f, "Memory access error during stack walk"),
WalkError::Unknown(msg) => write!(f, "Unknown error: {}", msg),
}
}
}
impl std::error::Error for WalkError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_platform_stack_walker_creation() {
let walker = PlatformStackWalker::new();
assert!(!walker.platform_context.initialized);
let stats = walker.get_statistics();
assert_eq!(stats.total_walks, 0);
assert_eq!(stats.success_rate, 0.0);
}
#[test]
fn test_stack_walk_config() {
let config = StackWalkConfig::default();
assert_eq!(config.max_depth, 32);
assert_eq!(config.skip_frames, 2);
assert!(config.fast_unwind);
assert!(config.collect_frame_info);
}
#[test]
fn test_initialization() {
let mut walker = PlatformStackWalker::new();
let result = walker.initialize();
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
assert!(result.is_ok());
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
assert_eq!(result, Err(WalkError::UnsupportedPlatform));
}
#[test]
fn test_current_thread_walk() {
let mut walker = PlatformStackWalker::new();
let _ = walker.initialize();
let result = walker.walk_current_thread();
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
{
if walker.platform_context.initialized {
assert!(result.success);
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
assert!(
!result.frames.is_empty(),
"Native backtrace should produce frames on Linux/macOS"
);
assert!(result.walk_time > Duration::ZERO);
}
}
}
}
#[cfg(target_os = "macos")]
#[test]
fn test_macos_native_backtrace() {
let mut walker = PlatformStackWalker::new();
let _ = walker.initialize();
let mut frames = Vec::new();
let result = walker.walk_macos_stack(&mut frames);
assert!(result.is_ok());
assert!(
!frames.is_empty(),
"macOS native backtrace should produce at least the test frame"
);
for frame in &frames {
assert!(
frame.ip > 0,
"Each frame should have a valid instruction pointer"
);
}
}
#[cfg(target_os = "macos")]
#[test]
fn test_macos_backtrace_respects_max_depth() {
let mut walker = PlatformStackWalker::new();
walker.config.max_depth = 3;
let mut frames = Vec::new();
let _ = walker.walk_macos_stack(&mut frames);
assert!(
frames.len() <= 3,
"Should respect max_depth limit, got {} frames",
frames.len()
);
}
#[cfg(target_os = "macos")]
#[test]
fn test_macos_backtrace_respects_skip_frames() {
let mut walker = PlatformStackWalker::new();
walker.config.skip_frames = 2;
let mut frames = Vec::new();
let _ = walker.walk_macos_stack(&mut frames);
if frames.len() > 2 {
assert!(
frames[0].ip != frames[1].ip || frames.len() < 3,
"First frames should be skipped"
);
}
}
#[test]
fn test_frame_information() {
let frame = StackFrame {
ip: 0x12345678,
fp: Some(0x7fff0000),
sp: Some(0x7fff0008),
module_base: Some(0x12340000),
module_offset: Some(0x5678),
symbol_info: Some(FrameSymbolInfo {
name: "test_function".to_string(),
demangled_name: Some("namespace::test_function".to_string()),
file_name: Some("test.rs".to_string()),
line_number: Some(42),
}),
};
assert_eq!(frame.ip, 0x12345678);
assert_eq!(frame.fp, Some(0x7fff0000));
assert!(frame.symbol_info.is_some());
let symbol = frame
.symbol_info
.as_ref()
.expect("Symbol info should exist");
assert_eq!(symbol.name, "test_function");
assert_eq!(symbol.line_number, Some(42));
}
}