├── .gitignore ├── Cargo.toml ├── README.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target 3 | **/*.rs.bk 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "const-concat" 3 | version = "0.1.1" 4 | authors = ["Jef "] 5 | description = "Heinous hackery to concatenate constants" 6 | license = "Unlicense" 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Const string concatenation 2 | 3 | Rust has some great little magic built-in macros that you can use. A particularly-helpful one for building up paths and other text at compile-time is `concat!`. This takes two strings and returns the concatenation of them: 4 | 5 | ```rust 6 | const HELLO_WORLD: &str = concat!("Hello", ", ", "world!"); 7 | 8 | assert_eq!(HELLO_WORLD, "Hello, world!"); 9 | ``` 10 | 11 | This is nice, but it falls apart pretty quickly. You can use `concat!` on the strings returned from magic macros like `env!` and `include_str!` but you can't use it on constants: 12 | 13 | ```rust 14 | const GREETING: &str = "Hello"; 15 | const PLACE: &str = "world"; 16 | const HELLO_WORLD: &str = concat!(GREETING, ", ", PLACE, "!"); 17 | ``` 18 | 19 | This produces the error: 20 | 21 | ``` 22 | error: expected a literal 23 | --> src/main.rs:3:35 24 | | 25 | 3 | const HELLO_WORLD: &str = concat!(GREETING, ", ", PLACE, "!"); 26 | | ^^^^^^^^ 27 | 28 | error: expected a literal 29 | --> src/main.rs:3:51 30 | | 31 | 3 | const HELLO_WORLD: &str = concat!(GREETING, ", ", PLACE, "!"); 32 | | ^^^^^ 33 | ``` 34 | 35 | Well with `const_concat!` you can! It works just like the `concat!` macro: 36 | 37 | ```rust 38 | #[macro_use] 39 | extern crate const_concat; 40 | 41 | const GREETING: &str = "Hello"; 42 | const PLACE: &str = "world"; 43 | const HELLO_WORLD: &str = const_concat!(GREETING, ", ", PLACE, "!"); 44 | 45 | assert_eq!(HELLO_WORLD, "Hello, world!"); 46 | ``` 47 | 48 | All this, and it's implemented entirely without hooking into the compiler. So how does it work? Through dark, evil magicks. Firstly, why can't this just work the same as runtime string concatenation? Well, runtime string concatenation allocates a new `String`, but allocation isn't possible at compile-time - we have to do everything on the stack. Also, we can't do iteration at compile-time so there's no way to copy the characters from the source strings to the destination string. Let's look at the implementation. The "workhorse" of this macro is the `concat` function: 49 | 50 | ```rust 51 | pub const unsafe fn concat(a: &[u8], b: &[u8]) -> Out 52 | where 53 | First: Copy, 54 | Second: Copy, 55 | Out: Copy, 56 | { 57 | #[repr(C)] 58 | #[derive(Copy, Clone)] 59 | struct Both(A, B); 60 | 61 | let arr: Both = 62 | Both(*transmute::<_, &First>(a), *transmute::<_, &Second>(b)); 63 | 64 | transmute(arr) 65 | } 66 | ``` 67 | 68 | So what we do is convert both the (arbitrarily-sized) input arrays to pointers to constant-size arrays (well, actually to pointer-to-`First` and pointer-to-`Second`, but the intent is that `First` and `Second` are fixed-size arrays). Then, we dereference them. This is wildly unsafe - there's nothing saying that `a.len()` is the same as the length of the `First` type parameter. We put them next to one another in a `#[repr(C)]` tuple struct - this essentially concatenates them together in memory. Finally, we transmute it to the `Out` type parameter. If `First` is `[u8; N0]` and `Second` is `[u8; N1]` then `Out` should be `[u8; N0 + N1]`. Why not just use a trait with associated constants? Well, here's an example of what that would look like: 69 | 70 | ```rust 71 | trait ConcatHack { 72 | const A_LEN: usize; 73 | const B_LEN: usize; 74 | } 75 | 76 | pub const unsafe fn concat( 77 | a: &[u8], 78 | b: &[u8], 79 | ) -> [u8; C::A_LEN + C::B_LEN] 80 | where 81 | C: ConcatHack, 82 | { 83 | #[repr(C)] 84 | #[derive(Copy, Clone)] 85 | struct Both(A, B); 86 | 87 | let arr: Both = 88 | Both(*transmute::<_, &[u8; C::A_LEN]>(a), *transmute::<_, &[u8; C::B_LEN]>(b)); 89 | 90 | transmute(arr) 91 | } 92 | ``` 93 | 94 | This doesn't work though, because [type parameters are not respected when calculating fixed-size array lengths][fixed-size-length-problems]. So instead we use individual type parameters for each constant-size array. 95 | 96 | [fixed-size-length-problems]: https://github.com/rust-lang/rust/issues/43408#issuecomment-318258935 97 | 98 | Wait, though, if you look at [the documentation for `std::mem::transmute`][transmute] at the time of writing it's not a `const fn`. What's going on here then? Well, I wrote my own `transmute`: 99 | 100 | [transmute]: https://doc.rust-lang.org/1.26.0/std/mem/fn.transmute.html 101 | 102 | ```rust 103 | #[allow(unions_with_drop_fields)] 104 | pub const unsafe fn transmute(from: From) -> To { 105 | union Transmute { 106 | from: From, 107 | to: To, 108 | } 109 | 110 | Transmute { from }.to 111 | } 112 | ``` 113 | 114 | This is allowed in a `const fn` where `std::mem::transmute` is not. Finally, let's look at the macro itself: 115 | 116 | ```rust 117 | #[macro_export] 118 | macro_rules! const_concat { 119 | ($a:expr, $b:expr) => {{ 120 | let bytes: &'static [u8] = unsafe { 121 | &$crate::concat::< 122 | [u8; $a.len()], 123 | [u8; $b.len()], 124 | [u8; $a.len() + $b.len()], 125 | >($a.as_bytes(), $b.as_bytes()) 126 | }; 127 | 128 | unsafe { $crate::transmute::<_, &'static str>(bytes) } 129 | }}; 130 | ($a:expr, $($rest:expr),*) => {{ 131 | const TAIL: &str = const_concat!($($rest),*); 132 | const_concat!($a, TAIL) 133 | }}; 134 | } 135 | ``` 136 | 137 | So first we create a `&'static [u8]` and then we transmute it to `&'static str`. This works for now because `&[u8]` and `&str` have the same layout, but it's not guaranteed to work forever. The cast to `&'static [u8]` works even though the right-hand side of that assignment is local to this scope because of something called ["rvalue static promotion"][rv-static-promotion]. 138 | 139 | The eagle-eyed among you may have also noticed that `&[u8; N]` and `&[u8]` have different sizes, since the latter is a fat pointer. Well my constant `transmute` doesn't check size (union fields can have different sizes) and for now the layout of both of these types puts the pointer first. There's no way to fix that on the current version of the compiler, since `&slice[..]` isn't implemented for constant expressions. 140 | 141 | This currently doesn't work in trait associated constants. I do have a way to support trait associated constants but again, you can't access type parameters in array lengths so that unfortunately doesn't work. Finally, it requires quite a few nightly features: 142 | 143 | ```rust 144 | #![feature(const_fn, const_str_as_bytes, const_str_len, const_let, untagged_unions)] 145 | ``` 146 | 147 | ## UPDATE 148 | 149 | I fixed the issue where the transmute relies on the pointer in `&[u8]` being first by instead transmuting a pointer to the first element of the array. The code now looks like so: 150 | 151 | ```rust 152 | pub const unsafe fn concat(a: &[u8], b: &[u8]) -> Out 153 | where 154 | First: Copy, 155 | Second: Copy, 156 | Out: Copy, 157 | { 158 | #[repr(C)] 159 | #[derive(Copy, Clone)] 160 | struct Both(A, B); 161 | 162 | let arr: Both = Both( 163 | *transmute::<_, *const First>(a.as_ptr()), 164 | *transmute::<_, *const Second>(b.as_ptr()), 165 | ); 166 | 167 | transmute(arr) 168 | } 169 | ``` 170 | 171 | [rv-static-promotion]: https://github.com/rust-lang/rfcs/blob/master/text/1414-rvalue_static_promotion.md 172 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature( 2 | const_fn, 3 | const_fn_union, 4 | untagged_unions, 5 | const_raw_ptr_deref 6 | )] 7 | 8 | pub const unsafe fn transmute(from: From) -> To { 9 | union Transmute { 10 | from: std::mem::ManuallyDrop, 11 | to: std::mem::ManuallyDrop, 12 | } 13 | 14 | std::mem::ManuallyDrop::into_inner(Transmute { from: std::mem::ManuallyDrop::new(from) }.to) 15 | } 16 | 17 | pub const unsafe fn concat(a: &[u8], b: &[u8]) -> Out 18 | where 19 | First: Copy, 20 | Second: Copy, 21 | Out: Copy, 22 | { 23 | #[repr(C)] 24 | #[derive(Copy, Clone)] 25 | struct Both(A, B); 26 | 27 | let arr: Both = Both( 28 | *transmute::<_, *const First>(a.as_ptr()), 29 | *transmute::<_, *const Second>(b.as_ptr()), 30 | ); 31 | 32 | transmute(arr) 33 | } 34 | 35 | #[macro_export] 36 | macro_rules! const_concat { 37 | () => { 38 | "" 39 | }; 40 | ($a:expr) => { 41 | $a 42 | }; 43 | ($a:expr, $b:expr) => {{ 44 | let bytes: &'static [u8] = unsafe { 45 | &$crate::concat::< 46 | [u8; $a.len()], 47 | [u8; $b.len()], 48 | [u8; $a.len() + $b.len()], 49 | >($a.as_bytes(), $b.as_bytes()) 50 | }; 51 | 52 | unsafe { $crate::transmute::<_, &'static str>(bytes) } 53 | }}; 54 | ($a:expr, $($rest:expr),*) => {{ 55 | const TAIL: &str = const_concat!($($rest),*); 56 | const_concat!($a, TAIL) 57 | }}; 58 | ($a:expr, $($rest:expr),*,) => { 59 | const_concat!($a, $($rest),*) 60 | }; 61 | } 62 | 63 | #[cfg(test)] 64 | mod tests { 65 | #[test] 66 | fn top_level_constants() { 67 | const SALUTATION: &str = "Hello"; 68 | const TARGET: &str = "world"; 69 | const GREETING: &str = const_concat!(SALUTATION, ", ", TARGET, "!"); 70 | const GREETING_TRAILING_COMMA: &str = const_concat!(SALUTATION, ", ", TARGET, "!",); 71 | 72 | assert_eq!(GREETING, "Hello, world!"); 73 | assert_eq!(GREETING_TRAILING_COMMA, "Hello, world!"); 74 | } 75 | } 76 | --------------------------------------------------------------------------------