You cannot keep the code "as is".
Your proc-macro would need to transform
let mut x = 5;
x += 1; // &mut _ access
println!("{}", x); // &_ access
into
unsafe {
let x_raw = UnsafeCell::new(5);
macro_rules! x {() => (*x.get())}
x!() += 1;
println!("{}", x!());
}
since this is the only way you can go and inspect x_raw in between the accesses without breaking aliasing guarantees and cause UB.
This, in and on itself, is already a huge effort (note: for non mut bindings you would not need to do that, since there is no non-aliasing guarantee);
Now, assuming you manage to automate this transformation, you can achieve what you want with something along these lines:
#![feature(specialization, raw)]
use ::std::{*,
cell::{
Cell,
UnsafeCell,
},
ptr::NonNull,
};
trait AsDynDebug {
fn as_dyn_debug (self: &'_ Self) -> &'_ dyn fmt::Debug;
}
impl<T> AsDynDebug for T {
default
fn as_dyn_debug (self: &'_ Self) -> &'_ dyn fmt::Debug
{
struct DefaultDbg;
impl fmt::Debug for DefaultDbg {
fn fmt (
self: &'_ Self,
stream: &'_ mut fmt::Formatter<'_>,
) -> fmt::Result
{
write!(stream, "<Missing Debug impl>")
}
}
&DefaultDbg
}
}
impl<T> AsDynDebug for T
where
T : fmt::Debug,
{
fn as_dyn_debug (self: &'_ Self) -> &'_ dyn fmt::Debug
{
self
}
}
struct Holder (
Cell<Option<(&'static str, NonNull<UnsafeCell<dyn AsDynDebug>>)>>,
);
impl Holder {
fn new () -> Self
{
Self(
Cell::new(None),
)
}
fn set_ref<T : AsDynDebug> (
self: &'_ Self,
name: &'static str,
p: &'_ UnsafeCell<T>,
)
{
self.0.set(Some((name, unsafe {
// cast &UnsafeCell<T> into &UnsafeCell<dyn ...>
let mut raw_trait_object: raw::TraitObject = mem::transmute(
ptr::null::<T>() as *const dyn AsDynDebug
);
raw_trait_object.data = p as *const _ as *mut ();
let p: &UnsafeCell<dyn AsDynDebug> = mem::transmute(
raw_trait_object
);
p.into() // decay into a raw non_null pointer
})));
}
fn drop_ref (self: &'_ Self)
{
self.0.set(None)
}
}
thread_local! {
static HOLDER: Holder = Holder::new();
}
macro_rules! bp {() => (
HOLDER.with(|slf| unsafe {
if let Some((name, p)) = slf.0.get() {
let p = &*(p.as_ref().get());
eprintln!("Debug: {} = {:?}", name, p.as_dyn_debug());
} else {
eprintln!("Debug: No state");
}
});
)}
fn main ()
{
bp!();
{
let raw_x = UnsafeCell::new(5);
HOLDER.with(|slf| slf.set_ref("x", &raw_x));
bp!();
unsafe { *(raw_x.get()) += 1; }
bp!();
HOLDER.with(Holder::drop_ref);
}
bp!();
}
So yes, one can go and circumvent Rust checks such as lifetimes with unsafe and raw pointers, but there are other guarantees, such as aliasing, that require extreme care to do properly.
As others have stated above, this "inject reflection code" approach may not be the easiest way to set up a debugger.