Side step borrow checker to access vars as options?

I realize this will be unsafe and that I am fighting an up hill battle against my buddy the bc, but if I only get options when accessing the value using Rc or something like that it may be less unsafe if possible at all. With that said here we go

fn main() {
    // Some kind of container/wrapper to hold ref to vars when in scope
    // and hold None otherwise
    let mut a = Holder::new();
    {
        let mut x = 5; // some local var
        _a.set_ref(&x); // _a is now available 
        x += 1;
        // _a's ref to x should now be 6
        _a.drop_ref(); // now _a's ref is None
    }
}

what I am trying to do is create a hashmap like structure of variable names and values using an attribute macro to allow step debugging. I don't need a mut ref, I only want to be able to print. If this is at all possible using a pointer or Rc or UnsafeCell that would be great.

Thanks in advance for any and all input

Is raw supposed to be x and _a -> a?

What you have described is UB because you have an aliasing unique reference (&mut T) when you call +=

1 Like

I appreciate the desire, but "uphill battle" is an understatement!

If I understand you correctly (which I'm not sure about), then what you're trying to do is getting (readable) references to arbitrary (mutable) values in scope.
This clashes directly with the core principle of rust that references are readable XOR writable. This is not just a "nice feature" of Rust, it is THE fundamental assumption that the compiler makes that makes the Rust Ownership Model even possible.
Violating it is the very worst form of Undefined Behaviour in the scariest sense of the word.

If you are trying to implement a simplified "step debugger", why not use an actual stepping debugger?
Both GDB and LLDB work decently with Rust. LLDB even has a VSCode extension.

If those are too much overkill, maybe a few dbg!() macros are enough? (dbg!() is the Rust-Deluxe version of Println-debugging)

If that also doesn't isn't sufficient, your last option is to refactor 90% your program and replace all variables with Arc<T>. This cannot be done without also changing practically every place in your program where you use the monitored variables.
This replacement is probably more work than following a gdb-tutorial from scratch, which brings us back to "use an actual debugger".

4 Likes

You should use Cell or AtomicU* wrappers for this. You might use raw pointers if you insist this is safe, but you can't use & for this, because & is not a pointer. Cheating borrows will end up giving invalid aliasing information to LLVM and it may miscompile your program.

5 Likes

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.

4 Likes

Thank you so much for all the help, it's really encouraging to get feedback (even when I'm doing something kooky). I will continue to stumble onward. Again thanks!