Singleton Interfaces
Singletons are classes of which there should only be a single instance throughout the lifetime of the process. Although Plasma uses the singleton pattern quite extensively for built-in classes, such as plTaskSystem
and plResourceManager
, those classes don't use dedicated singleton infrastructure. Instead, they only expose static functions, and there is no need for any instance.
Accessing such singletons is trivial, as you can always call their functions directly. However, there is another type of singleton, which does require special handling.
There are cases where you want to define an interface to make certain functionality available, but you may have different implementations. Only one implementation should ever be active, though. Concrete examples are the integrations of third party libraries. For example there is an plFrameCaptureInterface
. This class defines an interface through which plGameApplicationBase
can do a capture of the rendered frame, which can be used for debugging graphics issues. However, how such a frame capture could be taken, depends on the platform, the installed tools, the used graphics API and so on. This functionality may be available or not and the exact implementation that is needed can differ drastically.
Therefore, we want to be able to dynamically load the necessary implementation and make it available through the abstract interface. For the plFrameCaptureInterface
we have an implementation by our RenderDoc integration. In the future we might have a second implementation for PIX or some other platform specific tool.
Using the singleton infrastructure, we can simply load an engine plugin that contains an implementation, and from that plugin register our implementation for that interface. Other code can then query for an instance of this interface and, if available, use it without knowing anything about the implementation, and without the need to link against that library.
Implementing Singletons
This section shows all the pieces needed for a singleton.
Interface Base Class
First, you need to have a virtual base class that declares the actual interface.
This is the class through which other code will later access the functionality, so it must be in a shared location.
Interface Implementation
Next, you need one or more implementations of your interface. You can, of course, have zero implementations, if all you want to provide is the option for future extensibility, and your code should generally be able to handle the fact that no implementation is currently loaded.
Note the PL_DECLARE_SINGLETON_OF_INTERFACE
macro. This adds one part of the required functionality. For one, this class adds a function to query the one and only instance of your class (GetSingleton()
). Also, it prevents you from creating two instances of this class, as that would violate the singleton contract.
Finally, you need to add this to you cpp file:
The macro again inserts vital code for your singleton to work. The constructor also has to follow the pattern shown above.
You can now implement the desired behavior for the overridden functions.
Instantiating Singletons
The Plasma singleton infrastructure does not automatically create an instance of singleton classes. It is up to you whether, when and how you create your instance. The most common way to do this, is to leverage the startup system to hook into the engine startup process at the right time.
For details, read that chapter, but here is what you would typically do. At startup you instantiate your singleton implementation:
And at shutdown you make sure to clean it up again:
Accessing Singletons
There are two ways that you can access your singleton instance. In a piece of code that knows for certain that it will only run in conjunction with a specific singleton implementation, you can access it directly:
This is the most efficient way. However, use cases for this should be relatively rare. The more common situation is, when you want to get the implementation for an interface. To do so, you need to go through plSingletonRegistry
:
Here we don't need to know anything about the implementation and therefore have no link dependency on the library that provides it. This is how most code would access a singleton implementation. Be aware that this requires a more expensive lookup, so locally cache the result, if you want to do multiple function calls on it.
See Also
Engine Plugins