How to Make a macro_rules! Macro For Creating a Byte Array From a String

Hey there!

I have a simple type like this:

struct Key<const N: usize = 24>(pub [u8; N]);

The idea is to store a small ascii string on the stack. I have a new function for creating a Key from a string reference, but I want to make it easier to create one in a const context.

So far I can do it without a macro like this:

pub const ID: Key = Key(*b"core:idle\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");

The annoying part is padding the key to the proper number of bytes.

Is there a way to make a macro_rules! macro for simplifying this to something like:

pub const ID: Key = key!("core:idle");

I don't think you'll be able to use macro_rules for that. A procedural macro could definitely do it though.

Generally repetitions that don't strictly depend on the number of input tokens are hard to do with macro_rules.

It is technically possible to do with const fn behind a macro, though certainly not easy and involving a potentially frightening amount of unsafe.

If you're interested, the const_format utilizes these hacks to implement a const item compatible concat!, and I've done a similar thing for my own cstr8! macro concatenating a nul byte to a string constant. You'd essentially do a similar thing to concatenate to [0; N] and then transmute_copy out [u8; N], rather than my +1 or const_format's much more involved formatting.

Thanks for your guys replies.

After looking at your macro @CAD97 I realized I could do more than I though in a const fn and was able to just make my new() method const by avoiding the is_ascii() method on slices and just using u8::is_ascii():


impl<const N: usize> Key<N> {
    /// Create a [`Key`] from a string.
    ///
    /// # Errors
    ///
    /// Returns an error if the input is too long, or if it is non-ascii.
    pub const fn new(s: &str) -> Result<Self, KeyError> {
        if s.len() > N {
            return Err(KeyError::TooLong);
        }
        let s_bytes = s.as_bytes();
        let mut data = [0u8; N];
        let mut i = 0;
        while i < s.len() {
            let byte = s_bytes[i];
            if !byte.is_ascii() {
                return Err(KeyError::NotAscii);
            }
            data[i] = byte;
            i += 1;
        }

        Ok(Self(data))
    }

    /// Create a new key, and error if it cannot be parsed at compile time.
    #[track_caller]
    pub const fn new_const(s: &str) -> Self {
        match Self::new(s) {
            Ok(key) => key,
            Err(e) => match e {
                KeyError::TooLong => panic!("Key too long"),
                KeyError::NotAscii => panic!("Key not ascii"),
            },
        }
    }
}

It works great now!

1 Like

.... duh, of course you don't need the full madness of const concat, you can just copy bytes over into your fixed-size array. I have overgeneralization brain worms, it seems :upside_down_face:

Rather than new -> Result<T> and new_const -> T, I've also seen new -> T and try_new -> Result. It depends on what you expect to be the common case — if you expect most use sites to end up being new(…).unwrap(), just have new be a panicking constructor and let the less common case use try_new.

I've not seen any convention for a panicking variant of new -> Result, because there mostly isn't a need for that, since callers can do new(…).unwrap() if they want the panicking semantics. It's expected that eventually Result::unwrap will be const, so most libraries don't really want to add extra API surface just for a temporary limitation of the language, as the caller can of course do the match-panic if they really want.

In one library where I ran into this (nonzero associated const translating C enumerated values) I used a private Self::cook function around the exported Result-returning from_raw. It's in macro-generated code, so repeating a bunch of match for the constants wasn't going to bother me, except that it showed up in rustdoc as the value of the constant. :sweat_smile: (This got reported as a bug and fixed, IIRC; rustdoc will now not show overly long associated constant initializers.)


Because new_const is not restricted to use in constant evaluation, I'd recommend changing "and error if it cannot be parsed at compile time" to "panicking if it can't be parsed". It's not magic; let x = new_const("±"); will happily panic at runtime and not even generate a const_err lint.

2 Likes

Yeah, I know I haven't seen any prior convention for that either.

Actually, maybe a better solution is just to move it to a macro for creating it at compile time. Then we can even force it to be const evaluated:


/// Create a new const [`Key`] parsed at compile time.
#[macro_export]
macro_rules! key {
    ($s:literal) => {{
        const KEY: Key = match Key::new($s) {
            Ok(key) => key,
            Err(KeyError::TooLong) => panic!("Key too long"),
            Err(KeyError::NotAscii) => panic!("Key not ascii"),
        };
        KEY
    }};
}

Ah interesting, good point, thanks. I thought that it would be evaluated at compile time because it could, but then I suppose it only could if the string literal is known at compile time anyway, so yeah, that thinking didn't make sense in the first place.

1 Like

const doesn't mean compile-time only, it means also evaluatable at compile-time. It is thus perfectly fine to call a const fn with a runtime argument – the only result of which is that the expression no longer qualifies as const, but as a regular old function call, it still compiles and works identically.

As far as I can tell, the only way to force an expression to be const-evaluated is to use it as the initilaizer expression of a const or static binding (as opposed to a const function).

1 Like