Generic statics within generic functions

First off, please forgive my ignorance towards the current implementation and limitations of the compiler. The following may or may not be reasonably feasible.

I'm not currently advocating for global generic statics as that can be tricky to implement correctly. I do, however, assume that static generics within a function body should be fairly trivial (to reason about, not necessarily implement). I imagine the instance of the static can be created at the same time as the instance of the function.

This is a pattern I've encountered recently when implementing some of my libraries. This would theoretically allow libraries to create static data for users types.

I apologize for the following obscure example that probably has countless better solutions. My current use case, in my library, is to create a single instance of an objc subclass of NsWindow for each users type that implements a WindowImpl trait. The only other ways that keeps this abstracted away from the user is to create a new subclass every time a Window<impl WindowImpl> is created or to cache the subclasses with a static CACHE: Mutex<HashMap<TypeId, ObjcClass>>.

// Library Code

trait SomeTrait { .. }

type ExecFn<T> = fn (this: &T)

struct Operation<T: SomeTrait> {
    name: &'static str,
    exec: ExecFn<T>
}

fn get_operation<T: SomeTrait> -> &'static Operation<T> {
    // could easily be name mangled to the rust equivalent of 
    // get_operation_SomeConcreteType_INSTANCE
    static INSTANCE: Operation<T> = Operation {
        name: "add",
        exec: |_| { /* do add operation */ }
    };
    &INSTANCE
}

fn get_operation_2<T: SomeTrait> -> &'static Operation<T> {
    static INSTANCE: Operation<T> = Operation {
        name: "sub",
        exec: |_| { /* do sub operation */ }
    };
    &INSTANCE
}

// User Code

struct SomeConcreteType { .. }

impl SomeTrait for SomeConcreteType { .. }

fn execute() {
    let this = SomeConcreteType { .. };
    get_operation::<SomeConcreteType>().exec(&this);
    get_operation2::<SomeConcreteType>().exec(&this);
}
1 Like

The reason generic statics are not provided is that statics in Rust guarantee address stability (uniqueness), and there is no known way to guarantee that for generic statics across crates and codegen units.

1 Like

Are you talking about monomorphization at compile time here? There's no dymanic generation of statics or functions.

Are you sure you don't want consts? For example.

2 Likes

The problem is that there is not “the” instance of the function even for a specific set of type arguments; there are potentially many instances of the function, created while compiling many different crates and different codegen units (CGUs). These duplicate functions’ machine code later gets deduplicated by the linker (if you are doing static linking), but the code is a simpler problem because it is neither mutable nor guaranteed a unique address.

2 Likes

Thank you, that is a way I haven't though of using traits and am now 100 percent going to use in the future. Unfortunately, that works for that bad example I gave, but I ultimately need a static LazyLock.

I have cross-platform windowing library that I'm cleaning up and slowly releasing on crates. One of my goals was to remove all dynamic dispatch between the os to the user. For Windows, I can just set the windows winproc address to a fn win_proc<I: WindowImpl>(..) which can then statically call functions on the provided WindowImpl type.

For macOS, I have to register a whole objc subclass of NSWindow with the objc runtime. My current unreleased version uses a single subclass of NSWindow that dynamically calls functions on a Box<dyn WindowImpl>. If I had generic statics it would be trivial to register a new subclass of NSWindow that can statically dispatch WindowImpl functions.

This might totally be a skill issue but the best solution I've landed on that keeps the class registration internal to the library is to just cache each instance in a static HashMap<TypeId, ObjcClass> base on the TypeId of the type implementing WindowImpl. This solution is fine and ironically seems to keep the spirit of objc but I've just run into a couple instances where generic statics would create a quick and painless solution if feasible to implement.

Still pseudocode but closer to the actual scenario :

trait WindowImpl: Sized {
    fn on_key(this: &mut Window<Self>, event: KeyEvent)
}

struct Window<I: WindowImpl> {
    ptr: *const NSWindow,
    _marker: core::marker::PhantomData<I>,
}

fn get_objc_window_class<I: WindowImpl>() -> ObjcWindowClass<I> {
    static INSTANCE: LazyLock<ObjcWindowClass<I>> = LazyLock::new(|| {
        // register NSWindow class implementation with objective c runtime
        // directs callbacks to `WindowImpl` instance variable (ie. on_key, etc.)
    })
    &INSTANCE
}

impl<I: WindowImpl> Window<I> {
    pub fn new(implementation: I) -> Self {
        let class = get_objc_window_class::<I>()
        let ptr = class.alloc();
        ptr.init_with_impl(implemenation)
        Self {
            ptr
            _marker: core::marker::PhantomData 
        }
    }
}

Thanks for the rundown. Definitely some hopeful thinking on my part.

It seems to me that making this statically dispatched is unlikely to have any significant benefit, since

  • there is already an unavoidable Objective-C dynamic dispatch, so you can't get the inlining-and-optimization benefits of fully static dispatch, and
  • window events don’t generally arrive at a high enough rate that the slight costs of one extra function call will be significant.

It 100% percent won't. In the windowing library case it was just cool that on Windows a user of the library could implement the trait and it was pretty much guaranteed that their code would get inlined directly into the winproc. It more or less would just be an achievement to me if I could deliver the shortest path from OS to callback.

What ultimately prompted my post is that I started writing a mips emulator which has the same type of DI pattern where you can inject behavior into it to emulate different cpu models. I ultimately ran into another situation where generic statics would have been a very fast and "elegant" solution.

It's definitely a "me" problem. Not necessary by any stretch of the imagination. It was just something that I encountered more than once recently and thought it would be "a cool to have".

Do you actually care about it being a single static?

Because if you're just trying to get a &'static, you can do that by returning &const { ... } and relying on rvalue promotion.


Said otherwise, the things that are easy to implement are already supported by putting them in const blocks. Generic statics aren't supported because there's no known (good) way to solve the duplication problems, but consts are allowed to be duplicated so it's fine.

I was totally unaware you could return a reference to a const. That may actually be a good solution to one of the scenarios I ran into. I will have to do some further experimentation to see if I could make that work but it looks promising.

In my reply to @quinedot, I gave a better example which highlights the first time I ran into this. In that scenario I would need a single instance of the static due to it registering an class with the Objective-C runtime.

There are plenty of alternative solutions, however, I think it could be a useful language feature for libraries, if it was feasible.

Right now it seems like the three solutions to the scenarios I've ran into are :

  • make a new instance of the same data on every new
    • keeps the burden of data creation inside the library
    • unnecessary data duplication (fortunately in reality the size of this data is probably small)
    • doesn't necessarily work in situations where a singleton is needed
  • cache each created instance of this data in a HashMap for reuse
    • burden still contained within library
    • added complexity of lookup and creation if not found
  • make the user create the static data for each of their types
    • pushes the burden of data generation onto the user of the library
    • static INSTANCE: SomeRandomLibraryData<UserType> = SomeRandomLibraryData::new() seems pretty gross for an Api.

They all work fine. Generic statics just seem like a way more "clean" and "elegant" way for libraries to "generate" data for types made by users.

Keep in mind that you can provide a simple macro_rules! macro for the “force each implementor to provide their own static” approach. Like library_static!(UserType); or whatever.

Maybe have fn get_static() -> &'static LibraryStatic<Self> be a trait method of WindowImpl, and library_static!(UserType); could expand to an implementation of that method. IIRC, macros can expand to entire method implementations (function signature and all).

This might not be OP's case, but there is one situation where consts don't solve this issue: if you want to have internal mutability.

Note: mutability makes duplication more observable, but there's no solution even if I'm ok with that.

Even if duplication is not acceptable, the duplicated static could be used as a fast cached access to a global TypeIdMap<MyStaticValue>, removing the lookup after the first access.

Putting an item in a function just affects its name, not its semantics. There is no way in which generic statics inside a function are any simpler than global generic statics.

That seems like a reasonable for user code way to implement generic statics, if that's the semantics they desire. I don't think it'd be good to do that implicitly though.

Yeah, once things are mutable, duplication isn't acceptable any more, so the const solution falls flat.

How can you be "ok with that", where "that" is "you'll get a completely unspecified amount of duplicates of this memory and no guarantees at all about which part of the code sees which duplicate"?

Not sure if you saw my edit, but that's one example: the internal mutability is only used to cache an idempotent operation that depends on the generic type, the simpliest example of which is a get_or_insert operation on a classic global TypeIdMap.

Hm yeah, that would work.

However I think it would be a gigantic footgun to just implicitly allow duplicable mutable memory. (And const definitely shouldn't introduce mutable memory IMO, it's called "const" after all.) This would need some new language feature to explicitly opt-in to these pretty awkward semantics.

1 Like

Yes, I'm not saying that this should be supported by const, just that I see a usecase that's not being covered.

A generic static that you must annotate with #[allow_duplicates] or something like this would work fine for me.

I think that's a good idea (I've proposed it before a few times).

There are ways to deduplicate generic statics at runtime when you need that, by basically doing something like the following:

// this all is a replacement for:
// pub static MY_STATIC<T>: MyType<T> = MyType::new();

pub fn get_my_static<T>() -> &'static MyType<T> {
    // we put the statics inside the function for neatness, however being inside the function doesn't change their semantics at all, since they're items. the only difference is privacy/visibility.
    #[allow_duplicates]
    static MY_STATIC_PTR<T>: AtomicPtr<MyType<T>> = AtomicPtr::new(std::ptr::null_mut());
    #[allow_duplicates]
    static MY_STATIC_STORAGE<T>: MyType<T> = MyType::new();
    
    // this can be replaced with a hash-map that doesn't need allocations and just uses
    // statics for storage.
    // since this static is non-generic, we can depend on it to not be duplicated.
    static MY_STATIC_TYPE_MAP: Mutex<BTreeMap<TypeId, NonNull<()>>> = Mutex::new(BTreeMap::new());

    unsafe {
        // try to get our cached pointer to the static
        let mut retval = MY_STATIC_PTR::<T>.load(Ordering::Acquire);
        if retval.is_null() {
            std::hint::cold_path();
            // we don't have one cached yet
            let mut type_map = MY_STATIC_TYPE_MAP.lock().unwrap();
            // get the pointer or add a pointer to MY_STATIC_STORAGE::<T> if there isn't one for T.
            retval = type_map
                .entry(TypeId::of::<T>())
                .or_insert(
                    (&raw const MY_STATIC_STORAGE::<T>).cast::<()>.cast_mut(),
                )
                .cast::<MyType<T>>();
            // cache the pointer, we will always get the same pointer for each T,
            // so no need to worry about overwriting other stores, since they will store the same thing.
            MY_STATIC_PTR::<T>.store(retval, Ordering::Release);
        }
        &*retval
    }
}

I’m not entirely sure how to take that “shouldn’t”, but in the case you are not aware, a const currently can introduce mutable memory; the following program prints true true, indicating two cells exist:

use std::cell::Cell;
const FOO: Cell<bool> = Cell::new(true);
fn main() {
    println!("{} {}", FOO.take(), FOO.take());
}

However, this only applies to const values themselves; promotion, even inside a const, will not:

const FOO: &Cell<bool> = &Cell::new(true);
error[E0492]: interior mutable shared borrows of temporaries that have their lifetime extended until the end of the program are not allowed
 --> src/main.rs:2:26
  |
2 | const FOO: &Cell<bool> = &Cell::new(true);
  |                          ^^^^^^^^^^^^^^^^ this borrow of an interior mutable value refers to such a temporary

I was slightly sloppy in my terminology. I was talking about which global memory gets introduced by consts. In your example, no mutable global memory is introduced. The memory that the value of FOO itself is stored in is immutable. I know this because I (re)wrote the code in the compiler that manages this. :wink:

But consts are values so every time you write FOO, you get a copy of that memory. The actual global allocation a const is stored in is basically invisible to programmers. As you noted, this changes once there is a pointer indirection, where programmers can get references to said global allocations. (Your example does not involve promotion, it involves the outer-scope-lifetime-extension rule. It's easy to confuse the two as both have the effect of creating 'static lifetimes. You can tell the difference by looking at the MIR.)

We are fine with implicitly introducing global immutable memory in Rust. That can generally not cause any surprises, except if you start comparing raw pointers to inspect pointer identities. Global mutable memory on the other hand is a beast that needs a lot of careful management, so it's not something we ever do implicitly.

1 Like

I see. I think of the const, itself, as being a pure value, not relating to any memory at all (in the program being compiled, as opposed to the state of the compiler) — in my mental model, only each of the const’s uses is in program memory, and the const itself is only a value that is not yet in memory. So I did not recognize which memory you were referring to.