That's the underlying challenge, though - it's the importing crates that have all the control and power, right up until they call code in the exporting crate. Nothing stops C from calling A::needs_init
or B::calls_dependent
before A's static initialization code runs, especially once you consider environments like WebAssembly.
This is what lazy initialization works around, at the expense of 3 instructions per global access - it ensures that when C calls A::needs_init
or B::calls_dependent
, A's initializer runs, because it's run lazily on demand. And if having the first call to A::needs_init
or B::calls_dependent
run slow is not always acceptable, A can export a eager_init
function that does all the lazy init work up-front.
Part of the question, though, is how much work we actually ought to do for this case - if you can pass a global context by reference, instead of having it found lurking in the environment, you get performance benefits where it enables extra optimizations, and you get to have multiple global contexts in one program. If you can't pass a reference to the context, but can add a parameter, you get the ZST init token trick. If you can afford 3 instructions when looking up your context, you can use OnceLock
or LazyLock
.
And there are at least two directions that I'm aware of that people have thought seriously about which would be more general. One is just doing a better job of VRA, so that the 3 instruction penalty for OnceLock
and friends goes away if it's possible for the compiler to see that it's been initialized already, just as the 3 instruction penalty for slice indexing goes away if the compiler can see that the slice is clearly large enough; the other is capabilities/contexts which would provide a way to pass the global context around without requiring it to be a parameter.