Compare literal numerical values in macros or differentiate between literal strings and numbers

I'd like to create a macro that encodes some simple-ish structures into a serialization format like CBOR, at compile time instead of runtime. This saves doing things like vector allocations and some other inefficiencies by automatically calculating size at macro-evaluation time. But it looks like I can't make it generate different code for string literals and integer literals? Or make it generate different code for larger integers? Is there any good way around this? e.g. I wish we had some kind of if!($expr isa INTEGER_LITERAL && $expr < 24){ /* skip the type byte */ }

Also it seems const fn's can't do this either because they can't return differently sized arrays depending on input.

#![feature(trace_macros)]
use std::io::{self, Write};

//Convert an array of u8's to a static array in CBOR format.
//We don't handle numbers > 256 or array elements > 23 at this time.
//Is it possible to have a macro generate one thing if a constant
//literal number is within some bounds? I don't think so.

//Also, we'd love to handle strings and numbers differently. Is that possible?
//Again, I don't think so, not by itself or even using const fn's.
macro_rules! cbor {
    (@array $count:expr ; $size:expr ; [$($elems:expr,)*] $next:expr, $($rest:tt)*) => {
        //more elements
        {
            io::stdout().lock().write_all(b"next element\n")?;
            cbor!(@array ($count + 1) ; ($size + 2) ; [$($elems,)* 24, $next,] $($rest)*)
        }
    };
    (@array $count:expr ; $size:expr ; [$($elems:expr,)*] $next:expr) => {
        //last element
        {
            println!("array done with {} elements, {} bytes", $count + 1, $size + 2);
            let a: [u8; ($size + 2)] = [($count + 1) | 0b100_00000, $($elems,)* 24, $next];
            a
        }
    };
    ([ $($tt:tt)+ ]) => {
        //array start
        {
            io::stdout().lock().write_all(b"starting to process the array\n")?;
            cbor!(@array 0 ; 1 ; [] $($tt)+)
        }
    }
}


fn main() -> io::Result<()> {
	trace_macros!(true);
	
	let encoded_array = cbor!([1, 2, 103]);
	println!("{:?}", encoded_array);
	
    Ok(())
}

(Playground)

Output:

starting to process the array
next element
next element
array done with 3 elements, 7 bytes
[131, 24, 1, 24, 2, 24, 103]

Why not #[derive(Serialize)] + serde_cbor?

Because then you use to_vec/to_vec_packed which do a bunch of allocation and writing at runtime, or you use the writer variant with your own mut slice of an array, but then every time you do that, you have to figure out the length yourself. Also it's written to do all the serialization at runtime; those aren't const fn's.

You can calculate the length of the array with u64 and &'static str constant elements.
This example writes to the array at runtime,but it does avoid heap allocation.

I feel like it'd be better to use a procedural macro if what you need is serializing literals to CBOR byte arrays,all at compile time.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.