How to generate a static and unique ID at compile time?

I want to implement a static unique ID generator, usage like this:

// These two are guaranteed to have different values.
const UNIQUE_ID_1: u64 = unique!();
const UNIQUE_ID_2: u64 = unique!();

The only solution I found is using TypeId, but it is not u64, and seems not elegant.

// These two are guaranteed to have different values.
const UNIQUE_ID_1: TypeId = {
    struct _Unique;
    TypeId::of::<_Unique>()
};
const UNIQUE_ID_2: u64 = {
    struct _Unique;
    TypeId::of::<_Unique>()
};

Is that a better solution? Macros and build-scripts are fine. Thanks.

does it need to be globally unique, or is it enough to be unique within a single file?

you can combine the line and column number of a callsite into a u64, which is unique within the same file:

macro_rules! unique {
    () => {
        const {
            use std::panic::Location;
            let location = Location::caller();
            (location.line() as u64) << 32 | location.column() as u64
        }
    }
}

if you need a globally unique value, then it's better to use a procedural macro. a simple random number generator gives you probabilistically "unique" IDs.

but if you need guaranteed uniqueness, you'll have to somehow use a central authority to coordinate different macro invocation instances, such as a persistent data store. however, prodedural macros with side effects are usually discouraged. it's ugly and could harm the compile time drastically.

I would just use a seeded or hashed random numer generator for it. then use a test case to check if they collide.

Under which conditions should this ID be unique?

  • is it per file?
    • what if the file is included multiple times?
  • is it per crate?
    • what if you release multiple versions of the same crate? What if multiple versions of the crate are included in same project?
  • is it per executable?
    • does this need to support dynamic linking?
  • does it need to be a const or can it be a static or a function returning the id?
  • does it need to support being used in a generic context (that is, getting a different id for each generic instantiation)?
  • does it need to be guaranteed to be unique or can there be an astronomically small probability of it not being unique?

Depending on the answers there could be no reasonable way to do this.

Thanks for reply. The constant should be associated to a type, then the scope is only need to be in the type, in other words, different types can have same unique-ID constants.

It can be returned from a function, that is to say the following code is allowed, but the return value of unique1 function should be unchanged and different to values from other functions of this type. And this function may be generated by procedural macros.

struct MyType;

impl MyType {
    // anytime you call, you can only get the same id.
    fn unique1() -> u64 {...}
}

If only the given type will know about the value of this constant at any given point, why not make it arbitrary? Set it to const CONST: u64 = 0xDEAD_BEEF; for all and be done with it. Different types can have the same "unique id" as the other types, after all: as per your own requirement. Or are you planning to define several such constants per type? What are you actually going to do with them?

if uniqueness being very likely is enough then const random may be what you're looking for, a macro that generates any amount of random bytes.

if you use a u64 collisions are very unlikely, if you use a u128 collisions are never going to happen, i would recommend using u128.

well, I'm learning procedural macros these days, so I wrote this as a practice:

// usage:
// list the method names separated by commas
#[genid(unique1, unique2, unique3)]
struct MyType;

fn main() {
    println!("{}, {}, {}", MyType::unique1(), MyType::unique2(), MyType::unique3());
}
# crates/genid/Cargo.toml
[package]
name = "genid"
version = "0.1.0"
edition = "2024"

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = "3.0"
// crates/genid/src/lib
StreamExt;
use quote::quote;
use syn::DeriveInput;
use syn::Ident;
use syn::Token;
use syn::parse_macro_input;
use syn::punctuated::Punctuated;

#[proc_macro_attribute]
pub fn genid(
	attr: proc_macro::TokenStream,
	item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
	// parse as DeriveInput, make sure we are applied on a type definition
	// not other items like mod, fn, etc.
	let def = item.clone();
	let def = parse_macro_input!(def as DeriveInput);
	let type_name = def.ident;
	let ids: Punctuated<Ident, Token![,]> =
		parse_macro_input!(attr with Punctuated::parse_separated_nonempty);
	let ids = ids.iter().enumerate().map(|(i, method_name)| {
		// ******************************************************
		// change how the UNIQUE id is generated here
		// this example just uses an incremental counter
		// ******************************************************
		let id = i as u64;
		quote! {
			const fn #method_name() -> u64 {
				#id
			}
		}
	});
	let mut methods = quote! {};
	methods.append_all(ids);
	// quote expectes proc_macro2, need to convert
	let item = proc_macro2::TokenStream::from(item);
	quote! {
		#item
		impl #type_name {
			#methods
		}
	}
	.into()
}

What I'm going to implement is cacheable function call.

Here is the code the user may write like:

struct Foo {
    cache_table: CacheTable,
}

impl GetCacheTable for Foo {
    fn get_ct(&self) -> &CacheTable {
        &self.cache_table
    }
}

impl Foo {
    #[cached]
    fn heavy_computation(&self) -> i32 {
        (0..100).sum()
    }
}

My task is to implement the cached macro. This is what expected to generate:

struct Foo {
    cache_table: CacheTable,
}

impl GetCacheTable for Foo {
    fn get_ct(&self) -> &CacheTable {
        &self.cache_table
    }
}

impl Foo {
    fn heavy_computation(&self) -> i32 {
        let f = || {
            (0..100).sum()
        };
        const KEY: u64 = generate_some_unique_value!();
        
        // If we want to know which value we should reuse,
        // we must have a unique key to access
        GetCacheTable::get_ct(self).use_cache(KEY, f)
    }
}

And, oh, the function pointer should be the unique thing :thinking:

Until optimizer deduplicates function pointers.

fn f() {}
fn g() {}

fn main() {
    let ptr_f = f as fn() as usize;
    let ptr_g = g as fn() as usize;
    println!("0x{ptr_f:016x} 0x{ptr_g:016x}  collided: {}", ptr_f == ptr_g);
    // 0x00005a5080b59da0 0x00005a5080b59da0  collided: true
}

You instead want to use TypeId of f as the key.

Yes, the compiler may do some optimization that break the uniqueness guarantee, but that's okay because as long as the function pointer is same the result is same.
But actually and indeed I am considering use type id of the function item as unique key, but that was what I suppose to do with closures, because I cannot hash a closure.
TypeId should not collides even the function body of two closures is the same, I guess?

use std::any::TypeId;

fn main() {
    let value = 2;
    
    let tid1 = type_id_of_val(move || {
        value + 1
    });
    
    let tid2 = type_id_of_val(move || {
        value + 1
    });
    
    assert_ne!(tid1, tid2); // ok
}

fn type_id_of_val<F: 'static>(_: F) -> TypeId {
    TypeId::of::<F>()
}

If you're not against casting around raw fn() pointers as their usize addresses, try:

fn main() {
    let foo = Foo::key();
    let bar = Bar::key();
    let diff = foo.abs_diff(bar);
    std::process::exit(diff as i32); // != 0
}

struct Foo; 

impl Foo {
    fn key() -> u64 {
        static FOO: u64 = 64;
        &FOO as *const u64 as usize as _
    }
}

struct Bar; 

impl Bar {
    fn key() -> u64 {
        static BAR: u64 = 64;
        &BAR as *const u64 as usize as _
    }
}

The final asm ends up as light as you'd expect:

playground::main:
	pushq	%rax
	leaq	<playground::Foo>::key::FOO(%rip), %rax
	leaq	<playground::Bar>::key::BAR(%rip), %rcx
	movl	%eax, %edx
	subl	%ecx, %edx
	movl	%ecx, %edi
	subl	%eax, %edi
	cmpq	%rcx, %rax
	cmovael	%edx, %edi
	callq	*std::process::exit@GOTPCREL(%rip)

The addresses of all the non-zero-sized static's are guaranteed to be disjoint, as per the reference. As long as you don't mess around with their ZST cousins (think: static A/B: () = () - which happily collide), you won't need any compile time generation of any unique IDs at all.

The good news are: The Rust Lib team has already done all the hard work!

You don't need this #[cached], but just OnceLock in std::sync - Rust

Your code becomes:

use std::sync::OnceLock;

fn heavy_computation() -> i32 {
    static CACHED: OnceLock<i32> = OnceLock::new();
    
    let f = || {
        println!("Running heavy computation...");
        (0..100).sum()
    };
    
    *CACHED.get_or_init(f)
}

fn main() {
    println!("heavy call #1: {x}", x = heavy_computation() );
    println!("heavy call #2: {x}", x = heavy_computation() );
}

If you need a unique ID with a very low probability of a match, like other said, you can use a 128 bit ID generator, such as the rand or getrandom libraries, plus a 128 bit type (Rust lib's UUID also uses this generator and is 128 bit, just in a different format). Now how to use it at compile time, as const functions have many limitations and can not yet to call this. The good news is that the generator code can be called standalone without any dependencies on your other code. So you can generate the ID at compile time with a proc macro, then the proc macro also rewrite your code to insert the generated constant ID

Also, since you've already mentioned that you want to create a tool to memoize function calls, is the call static only the first time, or can it be dynamic, meaning it can be called multiple times with different parameters and you want the cache to still catch it?

If it's static just once, as others have mentioned, you can use std oncecell

But if it's dynamic, you need heap allocation that allows you to retrieve the value by key to store the cache collection, for example, a hashmap

I created something like this. You can also add, for example, a maximum storage limit, overriding one of the caches when it's full, or other strategies that suit your needs, and change the key to use ID generator

use std::collections::HashMap;
use std::hash::Hash;

pub struct MemoizeSingleThreaded<T, U, F>
where
    T: Hash + Eq,
    U: Clone,
    F: FnMut(&T) -> U,
{
    logic: F,
    cache: HashMap<T, U>,
    max_size: usize,
}

impl<T, U, F> MemoizeSingleThreaded<T, U, F>
where
    T: Hash + Eq + Clone,
    U: Clone,
    F: FnMut(&T) -> U,
{
    pub fn new(max_size: usize, logic: F) -> Self {
        Self {
            logic,
            cache: HashMap::with_capacity(max_size),
            max_size,
        }
    }

    pub fn call(&mut self, arg: T) -> U {
        if let Some(result) = self.cache.get(&arg) {
            return result.clone();
        }

        let result = (self.logic)(&arg);

        if self.cache.len() >= self.max_size {
            let key_to_remove = self.cache.keys().next().cloned();
            if let Some(k) = key_to_remove {
                self.cache.remove(&k);
            }
        }

        self.cache.insert(arg, result.clone());
        result
    }
}

ftest::test!(memoize_single_thrraded_test, {

    test_memoize {
      let mut m = MemoizeSingleThreaded::new(10, |&(a, b): &(i32, i32)| {
        a * b
      });
      
      let a = [
          (2, 3, 6),
          (8, 2, 16),
          (9, 9, 81)
      ];
      
      for (a, b, c) in a {
          let res = m.call((a, b));
          assert_eq!(res, c);
      }
    }
    
});