First off, to be clear, "local variables scoped within a module" are still "globals". They're not visible to everyone, but there is a single global location storing that variable, and any/all threads of execution that do have visibility on that variable are sharing access to the same global copy.
This creates a huge problem with race conditions in most languages. If Rust allowed it, and you did:
static mut someint: i64 = 0;
pub fn public_api() {
someint += 1;
}
then two threads called public_api
at the same time, you'd risk dropped increments, and on some architectures, torn data (e.g. if 64 bit values are actually manipulated as a pair of 32 bit values, then an increment that caused carry over to the high "word" would be changing each independently, and another thread might read a half-way state that's a mix of pre- and post- increment components, completely corrupting the value; for a value that's initially 0x00000000FFFFFFFF
, a pair of increments from different threads that should produce 0x0000000100000001
, could not only produce 0x0000000100000000
[an expected result from a dropped increment] but possibly produce 0x0000000000000000
, 0x0000000000000001
, causing two small additions to result in a massive subtraction; other weirdo results might be possible on unusual compilers/architectures).
Most languages solve this by not solving it; they give you locks and atomics and say "hey, if you don't use them, that's on you". TypeScript (run on a typical, single-threaded JS interpreter) solves it by not offering true threading (making races impossible; control is only handed off between tasks cooperatively, so it can't be interrupted halfway through an increment or the like by preemption).
Rust's solution is to make it illegal to do anything that can't be verified to be safe; any and all shared mutable data must either be properly protected, or manipulated solely in unsafe
contexts (where you pinky-swear you won't do anything actually unsafe, and if you're wrong, all of Rust's nice guarantees go out the window).
All that said, you can use globals (with visibility scoped to specific locations). Globals are allowed when at least one of the following things are true:
- They're immutable
- They're guaranteed atomic or protected by locks that prevent racy access
- They're thread local (so they're global per thread, rather than per process) so each thread is operating on them independently
They may be bad style, and the language makes it more difficult to create complex globals (read: you're not allowed to perform heap-allocation in the initializer for a global because only compile-time things may be used as initializer; you typically use libraries & macros that ensure said things are initialized exactly once at run-time) and doesn't care if that inconveniences you, because there should be a disincentive to use them, but it's not going to prevent you from doing so.
Rather than regurgitate how you do all this, I'll point you to this answer showing various ways to initialize heap-allocated globals, or atomic/mutex protected mutable types, this answer on using thread locals, or, if you absolutely must, this answer on just being unsafe
(shudders). All of them are suitable for your use case your "local variables scoped within a module" are just globals with appropriate visibility modifiers.