Understanding the Mulle-ObjC Universe: How to Run Multiple Independent Runtimes
The Mulle-ObjC universe is a global state container (struct _mulle_objc_universe) that encapsulates all runtime data—including class tables, method caches, and memory allocators—for a single Objective-C environment, enabling multiple isolated runtimes to coexist within the same process.
The mulle-objc/mulle-objc-runtime implements a unique architecture where the entire Objective-C execution context lives inside a "universe" data structure. Unlike conventional runtimes that rely on a single global state, this design allows developers to instantiate multiple independent universes, each maintaining completely isolated class hierarchies, protocol tables, and garbage collection states.
What Is the Mulle-ObjC Universe?
In the Mulle-ObjC runtime, a universe is the root data structure that owns every aspect of runtime state for a single Objective-C environment. Defined in src/mulle-objc-universe.h as struct _mulle_objc_universe, this C struct contains:
- Class and protocol hash tables
- Method caches and selector tables
- Load information and tagged-pointer support
- Garbage-collection state and memory allocators
- Thread-local configuration data
By default, the library creates one global universe per process, accessed through mulle_objc_global_get_defaultuniverse() or its inline wrapper mulle_objc_global_get_universe_inline(). All standard Objective-C operations implicitly target this default universe unless configured otherwise.
Why Run Multiple Independent Runtimes?
Multiple universes solve critical architectural challenges where complete isolation is required between Objective-C environments. Common scenarios include:
- Plugin architectures where third-party modules must not pollute the host application's class namespace
- Isolated test harnesses that require clean runtime states between test suites
- Embedded systems running distinct subsystems with conflicting class names
To support this, the runtime provides the compiler option -fobjc-universename=<name>, which causes generated code to reference a named universe instead of the default global one. Each named universe maintains its own struct _mulle_objc_universe instance with completely separate memory allocators and method caches.
Creating and Initializing a Custom Universe
Establishing an independent runtime requires three distinct phases: registration, initialization, and thread binding.
Registering a Universe
The entry point for universe creation is mulle_objc_global_register_universe(), declared in src/mulle-objc-universe.h. This function allocates a new universe structure (or returns an existing one) and registers it in the global hash table:
struct _mulle_objc_universe *
mulle_objc_global_register_universe( mulle_objc_universeid_t id,
char *name );
Internally, this calls the low-level __register_mulle_objc_universe() function defined in src/mulle-objc-universe.c.
Initializing with the Bang Routine
Registration alone does not prepare the universe for use. The initialization phase, triggered by _mulle_objc_universe_bang(), performs heavy-weight setup including method-cache creation, tagged-pointer table initialization, and garbage-collector configuration:
void _mulle_objc_universe_bang( struct _mulle_objc_universe *universe,
void (*bang)( struct _mulle_objc_universe *,
struct mulle_allocator *,
void *),
void *userinfo,
struct mulle_allocator *allocator);
You can supply a custom callback function to modify initialization behavior, such as substituting a custom memory allocator. If no custom logic is needed, pass NULL for the bang parameter to use default initialization.
Thread Association
Each thread must be explicitly bound to a universe via mulle_objc_thread_setup_threadinfo() (defined in src/mulle-objc-universe.h). This function creates the thread-local struct _mulle_objc_threadinfo and registers it with the garbage collector:
void mulle_objc_thread_setup_threadinfo( struct _mulle_objc_universe *universe );
Once bound, all subsequent Objective-C operations on that thread automatically resolve to the associated universe through thread-local storage lookups.
Running Multiple Runtimes: Compile-Time and Runtime
Compile-Time Configuration
To create fully isolated plugin modules, compile each with a distinct universe name:
clang -fobjc-universename=PluginA -c plugin_a.m -o plugin_a.o
clang -shared plugin_a.o -o libplugin_a.so
clang -fobjc-universename=PluginB -c plugin_b.m -o plugin_b.o
clang -shared plugin_b.o -o libplugin_b.so
This generates object files referencing different global symbols (e.g., __register_mulle_objc_universe$PluginA), ensuring complete isolation at the linker level.
Runtime Implementation
The host program loads and manages these independent universes programmatically:
#include <mulle-objc-runtime/mulle-objc-runtime.h>
int main(void)
{
/* Initialize first plugin universe */
struct _mulle_objc_universe *uA =
mulle_objc_global_register_universe(
mulle_objc_universeid_from_string("PluginA"),
"PluginA");
_mulle_objc_universe_bang(uA, NULL, NULL, NULL);
mulle_objc_thread_setup_threadinfo(uA);
/* Use PluginA classes... */
/* Initialize second plugin universe */
struct _mulle_objc_universe *uB =
mulle_objc_global_register_universe(
mulle_objc_universeid_from_string("PluginB"),
"PluginB");
_mulle_objc_universe_bang(uB, NULL, NULL, NULL);
mulle_objc_thread_setup_threadinfo(uB);
/* Use PluginB classes... */
/* Cleanup */
_mulle_objc_universe_crunch(uA, _mulle_objc_universe_defaultcrunch);
_mulle_objc_universe_crunch(uB, _mulle_objc_universe_defaultcrunch);
return 0;
}
Because each universe owns its own universe->memory.allocator and class tables, objects created in uA cannot be looked up or messaged from uB, preventing cross-contamination between runtime environments.
Multithreaded Universe Isolation
For true parallelism, assign each universe to a dedicated thread:
#include <pthread.h>
#include <mulle-objc-runtime/mulle-objc-runtime.h>
static void *run_universe(void *arg)
{
const char *name = (const char *)arg;
struct _mulle_objc_universe *uni =
mulle_objc_global_register_universe(
mulle_objc_universeid_from_string(name), (char *)name);
_mulle_objc_universe_bang(uni, NULL, NULL, NULL);
mulle_objc_thread_setup_threadinfo(uni);
/* Universe-specific work */
struct _mulle_objc_infraclass *cls =
_mulle_objc_universe_lookup_infraclass(uni,
mulle_objc_classid_from_string("UniverseSpecificClass"));
return NULL;
}
int main(void)
{
pthread_t t1, t2;
pthread_create(&t1, NULL, run_universe, (void *)"UniverseA");
pthread_create(&t2, NULL, run_universe, (void *)"UniverseB");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
Summary
- The Mulle-ObjC universe (
struct _mulle_objc_universe) defined insrc/mulle-objc-universe.his the root container for all runtime state, including class tables, method caches, and memory allocators. - Multiple independent runtimes are created using
mulle_objc_global_register_universe()and initialized via_mulle_objc_universe_bang()fromsrc/mulle-objc-universe.c. - Thread binding through
mulle_objc_thread_setup_threadinfo()determines which universe handles Objective-C messages on that thread. - The
-fobjc-universenamecompiler flag generates code targeting specific named universes, enabling plugin architectures with zero symbol collision risk. - Each universe maintains complete isolation—objects, classes, and static strings from one universe are invisible to others, including separate garbage collection domains.
Frequently Asked Questions
What is the difference between the default universe and a named universe?
The default universe is created automatically when the process starts and is accessed via mulle_objc_global_get_defaultuniverse(). A named universe is explicitly created via mulle_objc_global_register_universe() with a unique identifier and name string. Named universes reside in a global hash table and can be retrieved by name, whereas the default universe uses a hardcoded global pointer. According to dox/API_UNIVERSE.md, named universes are essential for isolation scenarios, while the default universe provides backward-compatible behavior for single-runtime applications.
Can objects be shared between different Mulle-ObjC universes?
No. Each universe maintains its own memory allocator (universe->memory.allocator) and class table. Because class pointers and object headers are universe-specific, passing an object pointer from one universe to another results in undefined behavior—likely crashes during message sending or method lookup. As implemented in src/mulle-objc-universe.c, the runtime performs no cross-universe validation; isolation is absolute by design.
How do I switch between universes in a single-threaded application?
Call mulle_objc_thread_setup_threadinfo() with the target universe pointer to rebind the current thread. This updates the thread-local storage that the runtime checks via mulle_objc_global_get_universe_inline(). However, you must ensure no Objective-C objects from the previous universe remain on the stack or in registers, as they will become invalid once the switch occurs. For frequent switching, consider using separate threads instead to avoid state corruption.
What happens if I don't call _mulle_objc_universe_bang()?
The universe remains uninitialized, lacking critical structures such as method caches, tagged-pointer tables, and garbage-collection roots. Attempting to create classes or send messages before "banging" the universe results in immediate segmentation faults or assertion failures. The runtime relies on _mulle_objc_universe_bang() (documented in book/chapter11-universe-configuration.md) to establish the invariant state required for all subsequent operations.
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 →