Attempt to implement sscanf in Rust, failing when passing &str as argument

228 Views Asked by At

Problem:

Im new to Rust, and im trying to implement a macro which simulates sscanf from C. So far it works with any numeric types, but not with strings, as i am already trying to parse a string.

macro_rules! splitter {
    ( $string:expr, $sep:expr) => {
        let mut iter:Vec<&str> = $string.split($sep).collect();
        iter
    }
}

macro_rules! scan_to_types {    
    ($buffer:expr,$sep:expr,[$($y:ty),+],$($x:expr),+) => {
        let res = splitter!($buffer,$sep);
        let mut i = 0;
        $(
            $x = res[i].parse::<$y>().unwrap_or_default();
            i+=1;
        )*        
    };
}

fn main() {
   let mut a :u8;   let mut b :i32;   let mut c :i16;   let mut d :f32;
   let buffer = "00:98;76,39.6";
   let sep = [':',';',','];
   scan_to_types!(buffer,sep,[u8,i32,i16,f32],a,b,c,d);  // this will work
   println!("{} {} {} {}",a,b,c,d);
}

This obviously wont work, because at compile time, it will try to parse a string slice to str:

let a :u8;   let b :i32;   let c :i16;   let d :f32;   let e :&str;
let buffer = "02:98;abc,39.6";
let sep = [':',';',','];
scan_to_types!(buffer,sep,[u8,i32,&str,f32],a,b,e,d);
println!("{} {} {} {}",a,b,e,d);
$x = res[i].parse::<$y>().unwrap_or_default();
   |        ^^^^^ the trait `FromStr` is not implemented for `&str`

What i have tried

I have tried to compare types using TypeId, and a if else condition inside of the macro to skip the parsing, but the same situation happens, because it wont expand to a valid code:

macro_rules! scan_to_types {    
    ($buffer:expr,$sep:expr,[$($y:ty),+],$($x:expr),+) => {
        let res = splitter!($buffer,$sep);
        let mut i = 0;
        $(
            if TypeId::of::<$y>() == TypeId::of::<&str>(){
                $x = res[i];
            }else{                
                $x = res[i].parse::<$y>().unwrap_or_default();
            }
            i+=1;
        )*        
    };
}

Is there a way to set conditions or skip a repetition inside of a macro ? Or instead, is there a better aproach to build sscanf using macros ? I have already made functions which parse those strings, but i couldnt pass types as arguments, or make them generic.

1

There are 1 best solutions below

3
On BEST ANSWER

Note before the answer: you probably don't want to emulate sscanf() in Rust. There are many very capable parsers in Rust, so you should probably use one of them.

Simple answer: the simplest way to address your problem is to replace the use of &str with String, which makes your macro compile and run. If your code is not performance-critical, that is probably all you need. If you care about performance and about avoiding allocation, read on.

A downside of String is that under the hood it copies the string data from the string you're scanning into a freshly allocated owned string. Your original approach of using an &str should have allowed for your &str to directly point into the data that was scanned, without any copying. Ideally we'd like to write something like this:

trait MyFromStr {
    fn my_from_str(s: &str) -> Self;
}

// when called on a type that impls `FromStr`, use `parse()`
impl<T: FromStr + Default> MyFromStr for T {
    fn my_from_str(s: &str) -> T {
        s.parse().unwrap_or_default()
    }
}

// when called on &str, just return it without copying
impl MyFromStr for &str {
    fn my_from_str(s: &str) -> &str {
        s
    }
}

Unfortunately that doesn't compile, complaining of a "conflicting implementation of trait MyFromStr for &str", even though there is no conflict between the two implementations, as &str doesn't implement FromStr. But the way Rust currently works, a blanket implementation of a trait precludes manual implementations of the same trait, even on types not covered by the blanket impl.

In the future this might be resolved by specialization. Specialization is not yet part of stable Rust, and might not come to stable Rust for years, so we have to turn to another solution. Since you're already using a macro, we can just let the compiler "specialize" for us by creating two separate traits which share the name of the method. (This is similar to the autoref-based specialization invented by David Tolnay, but even simpler because it doesn't require autoref resolution to work, as we have the types provided explicitly.)

We create separate traits for parsed and unparsed values, and implement them as needed:

trait ParseFromStr {
    fn my_from_str(s: &str) -> Self;
}

impl<T: FromStr + Default> ParseFromStr for T {
    fn my_from_str(s: &str) -> T {
        s.parse().unwrap_or_default()
    }
}

pub trait StrFromStr {
    fn my_from_str(s: &str) -> &str;
}

impl StrFromStr for &str {
    fn my_from_str(s: &str) -> &str {
        s
    }
}

Then in the macro we just call <$y>::my_from_str() and let the compiler generate the correct code. Since macros are untyped, this is one of the rare cases where a duck-typing-style approach works in Rust. This is because we never need to provide a single "trait bound" that would disambiguate which my_from_str() we want. (Such a trait bound would require specialization.)

macro_rules! scan_to_types {
    ($buffer:expr,$sep:expr,[$($y:ty),+],$($x:expr),+) => {
        #[allow(unused_assignments)]
        {
            let res = splitter!($buffer,$sep);
            let mut i = 0;
            $(
                $x = <$y>::my_from_str(&res[i]);
                i+=1;
            )*
        }
    };
}

Complete example in the playground.