Conditional print formatting, debugging using attribute macro

hello all.
I am working on a proof of concept "step wise debugging" attribute macro dbgify for fun, just trying to get some basic functionality so far. Today I ran into a interesting problem, I would love some feed back on my solution to this and thoughts on how dumb this macro idea is??

First a bit of context. I have a trait that uses specialization to determine if the type impl's Debug. If so, the type is cast from "T" to "T: Debug" and printed accordingly. I'm using syn in some hacky ways to mimic a cousin of reflection, so the types can be dynamic, which is hard to show in the example.
This is a general example playground, this dbgify/dbg-collect link is the file where its used if any one is interested

// trait checks if Debug trait is present (same for Display)
trait Dbg {
    fn is_debug(&self) -> bool;
}
impl<T> Dbg for T {
    default fn is_debug(&self) -> bool {
        false
    }
}
impl<T: Debug> Dbg for T {
    fn is_debug(&self) -> bool {
        true
    }
}

fn cast_dbg<T, D: Debug + 'static>(t: &T) -> &D {
    unsafe { std::mem::transmute(t) }
}
fn dbg_if<T: Debug + 'static>(s: &dyn Any) {
    if let Some(t) = s.downcast_ref::<T>() {
        println!("Debug {:?}", t);
    } else {
        println!("Not Debug");
    }
}

fn main() {
    let v = vec![0,1];
    if Dbg::is_debug(&v) {
        // these types would be added dynamically by syn's quote! macro
        let d = cast_dbg::<Vec<usize>, Vec<usize>>(&v);
        print_if::<Vec<usize>>(d);
    }
}

Thanks in advanced, any feed back is appreciated good or constructively bad.

Hello @DevinR528 and welcome to this forum.

I am sorry to tell you that cast_dbg() is (wildly) unsound:

To fix it you need to:

  1. mark cast_dbg as unsafe, since there are inputs with which one can trigger UB;

  2. ensure that your proc_macro_attribute uses it correctly (it is fine for a proc-macro to generate code with an unsafe block in it, as long as the generated code is sound. It is important, however, that the documentation of you proc-macro do inform the users about it).

The second point is harder to spot, since proc-macros cannot be tested within the playground :sweat_smile:


On the other hand, dbg_if is fine, so you should keep exploring that road :slight_smile:

Here is an idea for you to chew on: define the Dbg trait as follows:

trait Dbg {
    fn try_as_debug (self: &'_ Self) -> Option<&'_ dyn Debug>;
}
impl<T> Dbg for T {
    default
    fn try_as_debug (self: &'_ Self) -> Option<&'_ dyn Debug>
    {
        None
    }
}
impl<T : Debug> Dbg for T {
    fn try_as_debug (self: &'_ Self) -> Option<&'_ dyn Debug>
    {
        Some(self as &dyn Debug)
    }
}

fn dbg_if (s: &'_ dyn Dbg)
{
    if let Some(t) = s.try_as_debug() {
        println!("Debug {:?}", t);
    } else {
        println!("Not Debug");
    }
}
  • This has the advantage of not requiring to guess the type parameter to feed to dbg_if; the dyn Dbg trait object type knows how to do it on its own;

  • This has the "drawback" of requiring dyn Dbg instead of dyn Any, although any sized type can be coerced to dyn Dbg (wherever Any was used, Dbg can now be used instead; just add a Any super trait bound on Dbg: trait Dbg : Any { ... })

  • Playground

3 Likes

Thanks for the input! :grinning: it made my day to have a helpful and not demeaning answer :laughing:
That type signature is so elegant I tried to clean mine up for an hour but I'm not sure if i can. I think now that there is a bit more context I can maybe get away with showing the actual code?

pub struct Debugable<T>(T);
impl<T> Debugable<T> {
    unsafe fn cast<D: std::fmt::Debug + 'static>(t: &T) -> &D {
        std::mem::transmute(t)
    }
    pub fn print<D: std::fmt::Debug + 'static>(d: T) {
        // do term formating/color stuff
        println!("{:?}", unsafe { Self::cast::<D>(&d) })
    }
}
// this type signature is terrible, but I need to accept any T so the macro expanded code
// will "never" crash because of type errors before starting the debugging 
pub fn show_fmt<T, D: std::fmt::Debug + 'static, P: std::fmt::Display + 'static>(t: &T) {
    // is_display and is_debug return enum
    match (DisplayCheck::is_display(t), DebugCheck::is_debug(t)) {
        (Output::Display, Output::Debug) => Debugable::print::<D>(t),
        (Output::Display, Output::Not) => Displayable::print::<P>(t),
        (Output::Not, Output::Debug) => Debugable::print::<D>(t),
        (Output::Not, Output::Not) => {
            println!("'{}' does not impl Debug or Display", stringify!(t))
        }
        err => {
            println!("{:?}", err);
            panic!("show fmt failed BUG")
        }
    }
}

...

//then i have to call it like so again not great, (the # is because its in a quote! block)
show_fmt::<_, #var_type, #var_type>(&#capt_clone)

My only question then, is that a reasonably safe way to use unsafe, I check if Debug or Display is implemented for that type before calling the unsafe bit?