Concatenate const strings

Is there a way to do something like this?

const BASE:&'static str = "path/to";
const PART:&'static str = "foo";
const PATH:&'static str = BASE + PART;

We can do it with numbers...

const BASE:u32 = 40;
const PART:u32 = 2;
const PATH:u32 = BASE + PART;

Seems this addresses my use-case in the meantime: Const_format 0.2: compile-time string formatting :slight_smile:

See also, concat!().

1 Like

I'm fairly certain that doesn't do what OP is asking, at least from the example they provided.

With the recent improvements to const fn it is possible to concat &str into &[u8]. It still needs a macro but not a procedural macro. However, I'm not sure how to get a &str output at compile time. I think you need to do that conversion at runtime:

Playground

macro_rules! combine {
    ($A:expr, $B:expr) => {{
        const LEN: usize = $A.len() + $B.len();
        const fn combine(a: &'static str, b: &'static str) -> [u8; LEN] {
            let mut out = [0u8; LEN];
            out = copy_slice(a.as_bytes(), out, 0);
            out = copy_slice(b.as_bytes(), out, a.len());
            out
        }
        const fn copy_slice(input: &[u8], mut output: [u8; LEN], offset: usize) -> [u8; LEN] {
            let mut index = 0;
            loop {
                output[offset+index] = input[index];
                index += 1;
                if index == input.len() { break }
            }
            output
        }
        combine($A, $B)
    }}
}

const BASE: &'static str = "path/to";
const PART: &'static str = "foo";
const PATH: &'static [u8] = &combine!(BASE, PART);

fn main() {
    // Once you're confident it's working you can use `from_utf8_unchecked` here.
    let s = std::str::from_utf8(PATH).expect("Something went badly wrong at compile time.");
    dbg!(s);
}

Hopefully const generics and continued const fn improvements will make this situation better in the future.

1 Like
macro_rules! c_c_combo {
    ( $combined:ident : $($name:ident : $type:ty = $value:expr);+ $(;)? ) => {
        $(
            const $name: $type = $value;
        )+
        const $combined: &'static str = concat!($($value),+);
    }
}

c_c_combo! (PATH:
    BASE: &'static str = "path/to";
    PART: &'static str = "foo";
);

fn main() {
    println!("{}", BASE);
    println!("{}", PART);
    println!("{}", PATH);
}

Though admittedly, it's not sophisticated enough to handle more complicated combining scenarios. (Your full use case is unclear to me.)

1 Like

How about

macro_rules! combine {
    ($A:expr, $B:expr) => {{
        const A: &str = $A;
        const B: &str = $B;
        const LEN: usize = A.len() + B.len();
        const fn combined() -> [u8; LEN] {
            let mut out = [0u8; LEN];
            out = copy_slice(A.as_bytes(), out, 0);
            out = copy_slice(B.as_bytes(), out, A.len());
            out
        }
        const fn copy_slice(input: &[u8], mut output: [u8; LEN], offset: usize) -> [u8; LEN] {
            let mut index = 0;
            loop {
                output[offset+index] = input[index];
                index += 1;
                if index == input.len() { break }
            }
            output
        }
        const RESULT: &[u8] = &combined();
        // how bad is the assumption that `&str` and `&[u8]` have the same layout?
        const RESULT_STR: &str = unsafe { std::mem::transmute(RESULT) };
        RESULT_STR
    }}
}

const BASE: &str = "path/to";
const PART: &str = "foo";
const PATH: &str = combine!(BASE, PART);

fn main() {
    let s = PATH;
    dbg!(s);
}

Edit: Maybe this is an improvement:

        const RESULT: &[u8] = &combined();
        const RESULT_P1: *const [u8] = RESULT;
        // AKAIK `as`-casts take pointer layout into account
        const RESULT_P2: *const str = RESULT_P1 as *const str;
        // hence, this approach seems somewhat more sane to me
        // even though in non-const code I would of course
        // prefer to use a borrow+dereference
        const RESULT_STR: &str = unsafe { std::mem::transmute(RESULT_P2) };
        RESULT_STR
1 Like

That's great! I think your version should be safe in practice. But I'm not qualified to say if it's safe in future versions of Rust. But hopefully in the future these kinds of hacks will be less necessary.

From what I understand, "&str has the layout of &[u8]" is de facto guaranteed by the existence of stable methods like as_bytes_mut(). Or for that matter, like str::from_utf8_unchecked() (which is just a transmute like yours, and unstably const: issue 75196).

See also:

3 Likes

I'll just point out that concatc requires Rust nightly, you can use concatcp in stable Rust (since 1.46.0) for &'static str and integer arguments though.

Don't know the best way to document whether an item requires Rust nightly or not, right now it's documented in the root module.

Perhaps using doc_cfg on docs.rs, you could make more visible which macro requires which feature (and thus which ones require nightly and which don’t). Look at e.g. the tokio crate for how that works.

Edit: You also need to set up the Cargo.toml as described here (the part about [package.metadata.docs.rs]). And you can also test what the resulting documentation looks like locally by passing --cfg docsrs manually (make sure to also activate all features and run cargo doc on nightly for the local test).

1 Like