fadedbee
fadedbee

Reputation: 44745

How to use a macro to generate compile-time unique integers?

I need several parts of a program, in different modules, to have a unique integer.

for example:

pub fn foo() -> u64 {
    unique_integer!()
}

pub fn bar() -> u64 {
    unique_integer!()
}

(foo() should never return the same as bar(), but the values themselves are meaningless and do not need to be stable across builds. All invocations of foo() must return the same values, as must all invocations to bar(). It is preferred, but not essential, that the values are contiguous.)

Is there a way of using a macro to do this?

Upvotes: 3

Views: 1553

Answers (4)

Locke
Locke

Reputation: 8944

No, you can not use a regular macro for this. However, you might be able to find a procedural macro crate which might give this functionality.

That being said...

This does not count as safe rust, but if we are okay with throwing safety out the window then this should do the trick.

macro_rules! unique_u64 {
    () => {{
        struct PlaceHolder;
        let id = ::std::any::TypeId::of::<PlaceHolder>();
        unsafe { ::std::mem::transmute::<_, u64>(id) }
    }};
}

This is probably undefined behavior, but since we know that every type should have a unique TypeId it would have the desired effect. The only reason I know that this is even possible is because I have looked at the structure of TypeId and know it contains a single u64 to distinguish types. However, there are currently plans to change TypeId from being a u64 to something more stable and less prone to this kind of unsafe code. We have no guarantees on what the contents of TypeId might change to and when it does change it might silently fail if it still has the same size as a u64.

Alternatively,

We can achieve a similar result in safe rust by hashing the TypeId. Now, it slightly breaks the rules since we do not have any guarantee that it will always produce a unique result. However, it seems highly unlikely that 2 different TypeIds would hash to the same value. Plus this stays within safe rust and is unlikely to break for future releases of Rust.

macro_rules! unique_u64 {
    () => {{
        use ::std::hash::{Hash, Hasher};
        
        struct PlaceHolder;
        let id = ::std::any::TypeId::of::<PlaceHolder>();
        let mut hasher = ::std::collections::hash_map::DefaultHasher::new();
        id.hash(&mut hasher);
        hasher.finish()
    }};
}

Upvotes: 2

Deadbeef
Deadbeef

Reputation: 1681

You could compute a compile-time hash using the module path (which contains the crate and modules leading up to the file), the file name, column and line number of the macro invocation like this:

pub const fn hash(module_path: &'static str, file: &'static str, line: u32, column: u32) -> u64 {
    let mut hash = 0xcbf29ce484222325;
    let prime = 0x00000100000001B3;

    let mut bytes = module_path.as_bytes();
    let mut i = 0;

    while i < bytes.len() {
        hash ^= bytes[i] as u64;
        hash = hash.wrapping_mul(prime);
        i += 1;
    }

    bytes = file.as_bytes();
    i = 0;
    while i < bytes.len() {
        hash ^= bytes[i] as u64;
        hash = hash.wrapping_mul(prime);
        i += 1;
    }

    hash ^= line as u64;
    hash = hash.wrapping_mul(prime);
    hash ^= column as u64;
    hash = hash.wrapping_mul(prime);
    hash
}

macro_rules! unique_number {
    () => {{
        const UNIQ: u64 = crate::hash(module_path!(), file!(), line!(), column!());
        UNIQ
    }};
}

fn foo() -> u64 {
    unique_number!()
}

fn bar() -> u64 {
    unique_number!()
}

fn main() {
    println!("{} {}", foo(), bar()); // 2098219922142993841 2094402417770602149 on the playground
}

(playground)

This has the benefit of consistent results, when compared to the top answer that can return different values depending on the order of invocation, and this is also entirely computed in compile time, which remove the runtime overhead of maintaining a counter.

The only downside to this is that you could get hash value collisions. But the chance is low. If you want, you could try implementing an algorithm that computes perfect hash values. The example shown uses the FNV algorithm which should be decent but not perfect.

Upvotes: 4

Stargateur
Stargateur

Reputation: 26727

Not exactly a macro but anyway it's a proposition:

#[repr(u64)]
enum Unique {
    Foo,
    Bar,
}

pub fn foo() -> u64 {
    Unique::Foo as u64
}

pub fn bar() -> u64 {
    Unique::Bar as u64
}

Compiler should warn you if you don't use a variant.

Upvotes: 2

Cerberus
Cerberus

Reputation: 10218

It's possible to do something like this with once_cell, using a static atomic variable as a counter:

use core::sync::atomic::{Ordering, AtomicU64};
use once_cell::sync::Lazy;

static COUNTER: AtomicU64 = AtomicU64::new(0);

fn foo() -> u64 {
    static LOCAL_COUNTER: Lazy<u64> = Lazy::new(|| COUNTER.fetch_add(1, Ordering::Relaxed));
    *LOCAL_COUNTER
}

fn bar() -> u64 {
    static LOCAL_COUNTER: Lazy<u64> = Lazy::new(|| COUNTER.fetch_add(1, Ordering::Relaxed));
    *LOCAL_COUNTER
}

fn main() {
    dbg!(foo()); // 0
    dbg!(foo()); // still 0
    dbg!(bar()); // 1
    dbg!(foo()); // unchanged - 0
    dbg!(bar()); // unchanged - 1
}

Playground

And, yes, the repeating code can be, as usual, wrapped in macro:

macro_rules! unique_integer {
    () => {{
        static LOCAL_COUNTER: Lazy<u64> = Lazy::new(|| COUNTER.fetch_add(1, Ordering::Relaxed));
        *LOCAL_COUNTER
    }}
}

fn foo() -> u64 {
    unique_integer!()
}

fn bar() -> u64 {
    unique_integer!()
}

Upvotes: 1

Related Questions