How to Implement the MulleObjCSingleton Protocol in Mulle-ObjC
Adopt the <MulleObjCSingleton> protocol in your class interface to automatically receive a thread-safe +sharedInstance method, hidden storage, and lifecycle management without writing boilerplate synchronization code.
The MulleObjCSingleton protocol in the mulle-objc/mulleobjc repository provides a production-ready singleton pattern that eliminates manual thread-safety concerns. When you implement the MulleObjCSingleton protocol, the runtime automatically handles instance creation, storage, and cleanup through specialized infrastructure hooks defined in src/protocol/MulleObjCSingleton.m.
How the MulleObjCSingleton Protocol Works
The protocol operates through a coordinated sequence of runtime interactions. During class initialization, the runtime marks your class with the internal MULLE_OBJC_INFRA_IS_SINGLETON flag, enabling specialized allocation pathways that bypass standard instance creation rules.
Protocol Adoption and Class Registration
Declare the protocol in your interface file. This imports the required method signatures from src/protocol/MulleObjCSingleton.h and signals the runtime to inject singleton behavior during +initialize.
The implementation calls MulleObjCSingletonMarkClassAsSingleton(self) inside +initialize (lines 73-81 in MulleObjCSingleton.m), setting the singleton bit that the allocator checks during subsequent +sharedInstance calls.
Instance Creation and Constantification
When code invokes +sharedInstance, the runtime executes MulleObjCSingletonCreate(Class) (lines 93-130). This function either retrieves an existing entry from the per-universe concurrent hashmap _ephemeralSingletonInstances or allocates a fresh instance via MulleObjCSingletonNew(self).
The MulleObjCSingletonNew function (lines 35-80) allocates the object using the standard allocator, then invokes __initSingleton if defined; otherwise it falls back to -init. The resulting object is immediately constantified, making the runtime treat it as immutable and safe for cross-thread sharing without locks.
Implementing the Protocol in Your Class
Follow these implementation rules to ensure correct behavior within the mulle-objc runtime.
1. Declare Protocol Adoption
Add <MulleObjCSingleton> to your class interface. This minimal declaration triggers the entire infrastructure.
#import <MulleObjC/MulleObjC.h>
@interface MyManager : NSObject <MulleObjCSingleton>
@end
2. Implement Optional Initialization
Provide __initSingleton only if you require one-time setup logic. The protocol automatically forwards +sharedInstance calls to this method during first access.
@implementation MyManager
- (id)__initSingleton
{
self = [super init];
if (self)
{
// Perform expensive one-time configuration here
}
return self;
}
@end
If you omit __initSingleton, the protocol uses the standard -init method.
3. Access the Singleton
Retrieve the instance through the automatically provided class method:
MyManager *mgr = [MyManager sharedInstance];
NSLog(@"Singleton verified: %s", MulleObjCInstanceIsSingleton(mgr) ? "YES" : "NO");
The helper MulleObjCInstanceIsSingleton(id) (defined in MulleObjCSingleton.h, lines 62-71) checks the internal infra-class flag to verify singleton status.
Static vs. Ephemeral Singleton Modes
The protocol supports two storage models controlled by the MULLE_OBJC_EPHEMERAL_SINGLETON environment variable.
Static singletons (default) live for the entire process lifetime. The instance remains constantified until program termination, with no cleanup overhead.
Ephemeral singletons activate when MULLE_OBJC_EPHEMERAL_SINGLETON=YES is set. In this mode, the runtime creates a separate instance per universe using the _ephemeralSingletonInstances concurrent hashmap (lines 102-115). This isolates state between test runs or thread-local contexts.
Configure ephemeral mode for unit testing:
export MULLE_OBJC_EPHEMERAL_SINGLETON=YES
./test_runner
Subclassing Considerations
When subclassing a singleton-conforming class, the child inherits the +sharedInstance implementation without additional declarations.
@interface BaseService : NSObject <MulleObjCSingleton>
@end
@implementation BaseService
@end
@interface SubService : BaseService
@end
@implementation SubService
/* Inherits sharedInstance behavior from BaseService */
@end
Calling [SubService sharedInstance] returns a distinct singleton instance specific to the subclass. Do not redeclare the protocol in subclasses, as this would violate the singleton inheritance chain.
Critical Implementation Rules
Adherence to these constraints prevents runtime conflicts:
- Never override
+allocin singleton classes. The infrastructure expects all allocation to occur exclusively withinMulleObjCSingletonNew. - Do not call
[super initialize]unless inheriting from another singleton-conforming class. The protocol's+initializeimplementation (lines 73-81) already handles required setup. - Avoid manual instance storage. The runtime manages the
sharedInstancestorage pointer internally; direct assignment bypasses thread-safety checks.
For reference examples, examine test/MulleObjCSingleton/singleton.m in the repository, which demonstrates correct usage patterns and common pitfalls.
Summary
- Adopt
<MulleObjCSingleton>in your interface to enable automatic singleton behavior in the mulle-objc runtime. - The runtime marks classes via
MulleObjCSingletonMarkClassAsSingletonduring+initialize, setting theMULLE_OBJC_INFRA_IS_SINGLETONflag. - Use
__initSingletonfor custom one-time setup; otherwise-initserves as the default initializer. - Access instances through
+sharedInstance, which routes throughMulleObjCSingletonCreatefor thread-safe retrieval. - Choose between static (process-lifetime) and ephemeral (per-universe) modes using the
MULLE_OBJC_EPHEMERAL_SINGLETONenvironment variable. - Subclasses inherit singleton behavior without protocol redeclaration.
Frequently Asked Questions
Can I use MulleObjCSingleton with standard Apple Objective-C or GNUstep?
No, the protocol requires the Mulle-ObjC runtime infrastructure. The MulleObjCSingletonMarkClassAsSingleton function and constantification process depend on mulle-objc specific meta-object structures and the _ephemeralSingletonInstances hashmap not present in other Objective-C implementations.
What happens if I accidentally override +alloc in a singleton class?
Overriding +alloc breaks the singleton contract expected by MulleObjCSingletonNew in src/protocol/MulleObjCSingleton.m. The runtime may create multiple instances or fail to constantify the object properly, resulting in non-thread-safe behavior and potential memory leaks during universe cleanup at lines 124-132.
How do I test code that uses singletons without shared state leaking between tests?
Set the MULLE_OBJC_EPHEMERAL_SINGLETON environment variable to YES before launching your test harness. This configures the runtime to use the per-universe concurrent hashmap (lines 102-115), creating fresh singleton instances for each universe and preventing cross-test contamination.
Is the +sharedInstance method thread-safe?
Yes. The implementation uses atomic operations within MulleObjCSingletonCreate to ensure that exactly one instance is created even when multiple threads simultaneously invoke +sharedInstance for the first time. The constantification process further ensures that the resulting object is safely shareable across all threads without additional synchronization.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →