Singletons (in Rust)
Singletons are generally not easy to implement safely in any programming language, especially in a multithreaded environment. However Rust’s emphasis on (possibly guaranteed) on memory and concurrency safety sprinkled with some additional language constraints makes singletons awkward to implement at best, or plain annoying at worst. There are solutions however. This post will solely focus on the concept of Singletons, in general, and how to implement them in Rust.
This post was written for Rust 1.49
. Given the language is, albeit being +10 years old, still evolving significantly the implementations might not stand the test of time for very long although the general concept will remain valid.
Design
This section is language agnostic.
Singleton definition
A singleton (design pattern) is roughly defined as: restricting the instantiation of state to a single instance (for object-oriented languages, an object). The singleton is generally globally accessible throughout the system as opposed to passing around a reference of the single state instance.
The singleton introduces global state in the system, it should NOT be confused with a simple static function. For example a singleton to keeping state related to the available hardware peripherals (think: sensors, GPIO pins and alike for which there are only a predefined number available) in an embedded context. Some people perceive a singleton more as a manager for shared, unique, resources.
Several decisions have to be made during its design which may differ per use-case:
- Initialization (when/how/where;static/lazy; once/multiple times; configurable or hard-coded)
- Availability (always available or not, related to initialization)
- Mutable/immutable singleton state
- Accessibility/discoverability (globally accessible or not; multithread safe or not)
- Destruction (are there special requirements, when/how/where)
- Impact on testing
- (Prevent multiple instantiations)
Different decisions on the points above will result in different implementations and should be carefully considered.
Technical definition
Technically, low level, a singleton is very easy to implement: having a fixed pointer or reference to the singleton instance in memory suffices. The complexity comes with the chosen programming language and how it can guarantee there is only ever one single instance of the singleton (together with the other design decisions). So implementing a singleton is more a “language problem”, hence design pattern1. One will quickly notice when comparing implementations across different languages they can differ quite a bit; however this is out of scope for this post.
Design decisions
First I will have to clarify two terms: singleton state to refer to the implementation of the state itself possibly allowing modification and singleton system referring to the implementation making sure the singleton (state) is accessible and there can only ever be one instance. In some languages (e.g. C#, Java, …) it is possible to create a generic singleton system which can turn any state into a singleton.
Single state versus single reference
The opposition boils down to, can the singleton instance be changed during run-time or not.
If it cannot (single state) the state itself can still be either mutable or immutable. An advantage of this approach, it is safe to store and pass references along e.g. to closures. If the singleton is globally available however, and it usually is, passing along references should not be required. The singleton can simply be accessed directly upon need.
While single reference might be hard to properly implement in most languages (tracking references to the old instance) in rust the borrow checker (and language design) provide excellent guarantees2 which one can rely on. Besides the base implementation some more design questions must be answered though: who can change the instance? when can the instance be changed? are custom implementation allowed (or only a single or predefined types?), should the system be notified when a change occurs? As you can see the design/implementation becomes most often more complex compared to a single state singleton.If support for storing or passing along references is required, which again if the singleton in globally available should not be the case, one might need to define its own dynamic handle which always points to the current instance of the singleton. Otherwise stored references might still refer to, and use, the old singleton instance, hence introducing multiple active singleton instances which voids the purpose of the Singleton.
One could argue single reference is not a real implementation of the singleton pattern given the singleton state (instance) can be changed, momentarily introducing 2 states. Depending on the use-case this might not be acceptable.
Initialization
Fixed or custom (configurable) singleton state implantation support
Initialization will largely decide how testable the components are within the system. Worst case the Singleton is available on system level which entails if custom singleton state types cannot be specified, all components must always be tested in the context of the full system. While some components could maybe still be mocked, depending on the design, the full system context has to be provided (running) in case a component uses the Singleton. If the Singleton is only available for a subsystem the problem is contained to said subsystem/subcomponent. Depending on the implementation details it might still be possible to test the singleton state implementation separate from the whole system (with unit tests). Note: even when supporting multiple singleton state implementations it does not automatically imply the singleton state implementation can be changed at runtime, e.g. dynamic one-time initialization. For a system wide available Singleton to test/verify the assembly without changing it the singleton state type must be configurable at runtime; whether this is an actual requirement depends on your use-case3.
The most flexible way of supporting custom singleton state implementations is dependency injection which allows one to specify the actual implementation type (dependency) externally hence injecting the dependency (e.g. mock in a test context). There are many ways to implement dependency injection designs, usually differentiating on dependency discovery, ranging from dynamic discovery of new implementation types at run-time4 (e.g. in external assemblies or from a remote server; the latter requiring serious cyber security considerations).Implementing dependency injection becomes easier if the application and its initialization is split, e.g. the application is a library which is initialized by a mini application who’s job it is to just initialize the application. This allows for test to initialize the application on their own and providing (or “injecting”) their own implementations. In Rust this is even
explicitly supported by specifying a main.rs
and lib.rs
file in the same (application) project. An additional advantage of this approach, the singleton can remain a one-time initialization (single state) singleton however static initialization is, most likely, no longer possible. One should be cautious to make sure no additional instances of the singleton can be created through the dependency injection framework. While dependency injection is possible in rust (there are several crates available) is generally more popular in (object oriented) languages with (full) reflection support.
Another option is to use a single reference singleton design instead which supports custom singleton state implementations, although the same considerations regarding dependency injection apply.
Supporting custom singleton state implementations will most likely complicate your singleton design significantly; often a fixed singleton state type will do if the system remains testable.
Static versus lazy static initialization
Singleton initialization might be heavy on resources and if never used during execution it is wasteful or even worse impact the program’s performance for its whole lifetime (e.g. memory restricted). Therefore it might be worthwhile to consider only lazy initializing the singleton and defer its actual initialization when used for the first time (possibly introducing a performance impact for the first caller; which might or might not be acceptable depending on the use-case). Another reason why to use lazy initialization if initialization is dependent on other static variables given in most languages the order of static initialization (of none constants) is not guaranteed5 or even supported.
Lazy initialization might require special attention in a multithreaded environment.
The reverse of lazy initialization is sometimes called eager initialization, meaning initialization is always performed. While we discussed it in a static context eager or lazy initialization neither have to be static.
Note: whether or not the singleton in lazily initialized does not imply anything about its availability (more on this later).
Static versus run-time initialization
While static initialization offers the benefits of making the Singleton always available (from the start) it makes run-time dependency injection impossible by design. In addition some languages do not support initialization of non trivial types, e.g. Rust does not allow it.
Run-time initialization offers greater flexibility (dependency injection, configure the singleton with parameters, …) but introduces the problem of making sure the Singleton is properly initialized before it is used and preventing re-initialization if that is not allowed.
When the singleton is initialized will impact the availability of the Singleton, see next section.
Availability
While per definition there must only be a single instance at all times, if any, that implies nothing about its availability. The singleton could only become available late in the system’s initialization or unavailable early in the system destruction depending on the requirements.
Borrow singleton
Based on the principle of optional availability a design to implement a singleton is to make its managed resources borrowable. Hence a borrow singleton, it exposes resources that can be claimed (and never returned for the lifetime of the application) or borrowed (and returned when no longer required); if applicable the singleton state as a whole can be claimed or borrowed. Special attention should be provided during implementation to avoiding borrowed state being lost when going out of scope (destroyed) without being returned to the singleton. Borrowing Singletons are popular in embedded Rust to
manage peripherals. For example to manage GPIO pins: once a pin is in use it will generally remain in use and is never returned to the pool of resources (singleton). Using a (borrowing) singleton in this scenario will prevent 2 components using the same GPIO pin simultaneously. When a resource is no longer available and requested it is up to the client on how to handle such scenario, the singleton may provide additional support in the form of spinlocks, mutexes, requests for resources, events, … depending on the requirements. Needless to say a borrowing singleton is required to be mutable. The simplest form of a borrowing singleton is one that only allow borrowing by putting all resources behind a lock, as long as the lock is in use the resource cannot be used by anybody else. Alternatively the state ownership could transferred to the client leaving the field empty (nullpointer
or empty optional) in the singleton.
While usually less useful one could also make the singleton as a whole borrowable so it can only be used by one client at the same time6. However usually this is too coarse grained as for example one rarely needs to claim/access all device peripherals, see the previous example, potentially stalling other clients waiting on the singleton to become available to claim other peripherals.
Great care should be taken during implementation on thread safety; if applicable.
Using a borrowing singleton usually requires a bit more state/error handling in the client code. In general they only make sense in the context of access to resources, not for immutable singletons exposing state.
Mutability
Depending on the use-case the singleton state might be mutable or immutable. Immutable is usually easier to implement and per definition thread-safe.
Immutable singletons are often used to expose (fixed) state throughout the application (e.g. global config file parameters) however one should critically ask oneself whether or not a Singleton is the right design for these use-cases. While it could make sense there must indeed exist only one such (config/setting) instance if it must not be globally accessible throughout the system one could consider passing the instance around with a reference instead. Strictly speaking it would still be a singleton, according to the definition, however in practice this is why many people only consider something a Singleton if it is “global accessible” as well.
Mutable singletons are more common, note they might require special attention wrt interior thread-safety see next section.
Accessibility
Generally speaking Singletons are made accessible statically through a static variable or function. While it is possible to implement a Singleton passed along by reference there is practically no use-case for it (none that i have ever encountered or could think of while writing this post). As explained before one could argue it would still be considered a Singleton since many of the design tradeoff we have discussed become obsolete.
For mutable Singletons in a multithreaded environment one should be careful about race conditions, these can be solved by access control to the Singleton (e.g. through a mutex or read/write lock) or have thread safe interior mutability of the singleton state. Which fits best depends on your use case, most often a single mutex is too coarse grained and might require client to wait more than required if not all state must be mutated. However this depend on how often the Singleton is accessed and the number of its managed resources/state.
If the Singleton is globally available it is effectively a “hidden” dependency which is the exact reason why Singletons impact verification/testing.
Destruction
Destruction of the singleton instance is usually related to how it is initialized. One should take care on when the state is no longer available/unusually since depending on the nature of the Singleton other components of the system could still depend on it. To avoid these problems it is no uncommon to destroy the Singleton as last or let the language features take care of it automatically when applicable (e.g. garbage collected languages). As example when one has a Singleton for global configuration data which is persisted to file how are changes to the state handled after the file has been written to persistent storage? Is it ok to silently drop them?
Not all Singletons must be destructed depending on what state/resources they hold.
In Rust
drop
is NOT called on static items.
Implementation
As mentionned before, Rust’s design decisions make it harder and easier at the same time to implement Singletons; compared to other languages. Due to the memory safety and race condition constraints some designs are not possible/harder to implement, however when implemented usually provide better guarantees.
Especially static mut
is a problem, it is per definition unsafe (allows multiple aliasing + mutation at the same time, hence possible race conditions which is undefined behavior in Rust) and therefor, albeit possible, discouraged to use. Singletons in Rust are also discussed in the Rust
embedded book, especially the borrow singleton design. Here is an
example of a logger implemented as Singleton for embedded Rust.
Rust does offer more limited encapsulation features compared to an object oriented programming language like C#. Therefor it might require more attention to make sure the singleton has the proper encapsulation. For example if the stuct is exposed out of a module every one can create a new instance of it. However the normal scoping rules of rust still apply, hence static variables can be added to function bodies and used as structs fields as well. However a full analysis of Rusts encapsulation limitations, and solutions, is out of scope for this post and are largely neglected in the examples.
Note on the implementations: these are just examples which you can/might need to adjust depending on your needs e.g. remove/add Mutex
or replace the mutex by a SpinLock
ect. This post, in no way shape or form, tries to provide a complete list of implementations.
All example files are available for download at the end of the post.
Implementation examples (in Rust)
Mutable lazy single instance
Unfortunately Rust, currently, only allows primitive types to be initialized statically. While there is an RFC in progress to support lazy static initialization at the moment external crates, or implementing it oneself, is required. I would strongly advice the former e.g. the very popular lazy_static (currently 1.4.0) or one_cell (currently 1.3.1).
When Mutex
is removed the singleton becomes immutable. One could consider using a RwLock
instead depending on the use-case.
It works quite simple, the static instance is initialized when first accessed. It is protected by a mutex making it safe in a multithreaded environment. That’s it.
|
|
When using one_cell
instead:
static ARRAY: Lazy<Mutex<Vec<u8>>> = Lazy::new(|| Mutex::new(vec![]));
- Threadsafe
- Single state
- Lazy static initialization
- Simple implementation (+not using
unsafe
)
- Not the best ergonomic
- Requires an external crate
- Coarse grained locking
Drop
is not supported
Mutable lazy single instance, nightly
Using nightly features, once_cel, a lazy mutable singleton can be implemented in Rust (with std) without external crates. This implementation is inspired by the stdio implementation in std (one can check the source code yourself).
Implementation wise similar to the previous section, where instead of lazy_static
the nightly feature once_cel
is used instead. There is some more boilerplate (indirection) to automatically call lock()
so the client doe not have to; the downside is that all API calls have to be duplicated. Whether or not this trade of is worth it depends on your use-case.
This implementation well suited for global implementations, e.g. logging, io, …
|
|
- Threafsafe
- Does not require external crates
- Mutable singleton state
- Single state
- Lazy initialization
- Uses a nightly feature
- Some boilerplate (for better ergonomics)
Drop
is not supported- Coarse grained locking
Mutable lazy, auto descoped, single instance
Just like with the previous lazy singleton this will use an external crate for the initialization. This design is especially useful if no system resource should be used when the Singleton is not used and/or when the Singleton can be re-initialized during the program’s lifetime.
The Singleton instance is lazy initialized upon first need (new()
called on MyStruct
). Its implementation consists of a static, weak, shared state singleton (MySharedState
), a struct to hold the actual singleton state with reference counting (MyStruct
) and an struct using the actual singleton state (Resource
). All resources get a copy of MyStruct
, holding the singleton state, and when the last MyStruct
is dropped the singleton is automatically descoped as well. One could see this as an “on need” shared state singleton.
This design is particularly useful for libraries since you do not want to retain more resources as required and have no control over how the library is used. It can also be used as borrowing singleton for resources, e.g. GPIO pins.
|
|
- Threadsafe
- Auto descoped when not needed
- Single state
- Lazy initialization
- Can implement borrowing singleton
Drop
will automatically be called, if implemented- Supports fine-grained locking
- Requires an external crate
- Requires a bit of boilerplate
- Uses
unsafe
Single, primitive, value states
For the special use-case where the global state only consists of (a few), trivial-typed, fields one can use atomics.
The values are mutable, thread-safe and statically initialized.
|
|
- Static initialization
- Simple implementation (+not using
unsafe
) - No external crates
- Fine grained locking & control
- Ergonomically rather verbose
- Only for trivial types
Thread local singleton
This implementation is based on Sentry’s blogpost by Armin Ronacher you can ready his excellent blogpost instead.
This singleton design is a default constructed globally available single reference, thread safe, immutable singleton which ensures local consistency. The latter means the singleton state is read-only but can be changed to another instance, however once a reference to a particular state is obtained said state is guaranteed to never change. However the current global singleton state may be updated in the meanwhile. This entails there can be multiple singleton states active at the same time, technically violating the Singleton definition.
So how does this work, the clue is using thread_local
storage and thread-safe (atomic) immutable reference counting memory management (Arc
). Client are given a new read-only reference (see Clone()
on line 10) to the current instance which will bind it to the current thread (local storage) and make sure said instance will exists as long as the reference is not dropped. By saving the instance in thread local storage it will live as long as the thread is alive giving every thread its own singleton instance (copy).To make sure changing the config is safe the (global) singleton reference is guarded with a read-write (RwLock
) lock.
Note even though the debug_mode
field is public (and thus mutable) due to the Config
being wrapped in an Arc
, which makes it immutable, the value cannot be changed saving us making the field private and writing a getter for it.
One may switch Arc
for Rc
and RwLock
for RefCell
if in a single threaded environment. If you are just working with thread locals
you can also combine RefCell
with Arc
.
|
|
To recap: instead of using interior mutability where an object changes its internal state, consider using a pattern where you promote new state to be current and current consumers of the old state will continue to hold on to it by putting an Arc into an RwLock.
- Default eager static initialization
- (Thread) Local consistency
- Simple implementation (+not using
unsafe
) - Good ergonomy
- No locking after acquiring the handle
- Mutable singleton state
- No external crates
- Not a real singleton
- Only for immutable data
design patterns are designs which yield to certain set of properties which are commonly required. They are often tied to languages, for example in language A you want to have an implementation with a property Y for which you can implement a particular design pattern (or come up with your own implementation/design) but in another language B, Y might come for free or is not applicable so does not require a design pattern. Some patterns are rather universal, like singleton, albeit can differ significantly in implementation between programming languages. Also within a design pattern implementation there are different flavours as demonstrated in this pose. ↩︎
the compiler (barring compiler bugs) statically guarantees references always point to valid objects. More so, without using
unsafe
, one cannot store references without life time guarantees or being from a references counted wrapper. ↩︎often is regulated markets like medical devices the deliverable, assembly, must be verified as how it is delivered so separate builds for production and testing is not allowed by default and has to be justified. ↩︎
assuming compile time or link time dependency injection/code generation is not feasible e.g. like svd2rust provides for hardware features. ↩︎
commonly dubbed the “Static Initialization Order Fiasco”. ↩︎
often, in more naive implementations, this is implemented by accident as a side effect of protecting the whole singleton behind a mutex for thread safety. While effective it might not be the most efficient. ↩︎
- Permalink: //oostens.me/posts/singletons-in-rust/
- License: The text and content is licensed under CC BY-NC-SA 4.0. All source code I wrote on this page is licensed under The Unlicense; do as you please, I'm not liable nor provide warranty.