WORK IN PROGRESS #
Summary #
@property (class, ...) declares class-level state stored as instance
variables of the infraclass, appended to the classpair allocation. No new
keywords. Access via self->_ivar in + methods, or via synthesized
accessors from anywhere. Categories require @property(dynamic) for class
properties (accessor-only, no ivar). Class property ivars compose through
inheritance exactly like instance ivars.
User-Facing Syntax #
1@interface Foo : NSObject
2@property (class, copy) NSString *sharedName;
3@property (class, assign) int sharedCount;
4@end
5
6@implementation Foo
7
8+ (void) reset
9{
10 [self setSharedCount:0];
11 [self setSharedName:@"default"];
12}
13
14@end
From instance methods:
1- (void) printShared
2{
3 // self is an instance — no direct ivar access, use accessor
4 NSLog(@"%@", [[self class] sharedName]);
5}
Category class properties #
Categories must declare class properties as dynamic and provide accessors
manually. The compiler enforces this — same rule as instance properties in
categories.
1// Foo+Logging.h
2@interface Foo (Logging)
3@property (class, dynamic, assign) BOOL loggingEnabled;
4@end
5
6// Foo+Logging.m
7@implementation Foo (Logging)
8static BOOL _loggingEnabled = NO;
9
10+ (BOOL) loggingEnabled { return _loggingEnabled; }
11+ (void) setLoggingEnabled:(BOOL) v { _loggingEnabled = v; }
12@end
The file-static is in Foo+Logging.m — shared across all subclasses of Foo
that don't override the accessor. This is the correct pattern for
inheritable settings: [Bear setLoggingEnabled:YES] dispatches to
Foo(Logging)'s setter (if Bear doesn't override it), setting the same
_loggingEnabled that [Animal loggingEnabled] reads.
For per-class settings, the subclass overrides the accessor with its own static:
1// Bear+Logging.m
2@implementation Bear (Logging)
3static BOOL _bearLoggingEnabled = NO;
4
5+ (BOOL) loggingEnabled { return _bearLoggingEnabled; }
6+ (void) setLoggingEnabled:(BOOL) v { _bearLoggingEnabled = v; }
7@end
Now [Bear setLoggingEnabled:YES] only affects Bears. Full control, no magic.
Memory Layout #
classpair allocation (contiguous):
┌──────────────────────────────────────────────────────────────┐
│ struct _mulle_objc_objectheader infraclassheader │
│ struct _mulle_objc_infraclass infraclass ← self in + │
│ unsigned char _padding[...] │
│ struct _mulle_objc_objectheader metaclassheader │
│ struct _mulle_objc_metaclass metaclass │
│ ... (mixins, protocolids, categoryids, lock, etc.) │
│ uint32_t classindex │
├──────────────────────────────────────────────────────────────┤
│ CLASS PROPERTY IVARS (appended by runtime at creation) │
│ [inherited superclass class ivars first, then own] │
│ NSString *_sharedName; // offset = BASE + 0 │
│ int _sharedCount; // offset = BASE + sizeof(ptr) │
└──────────────────────────────────────────────────────────────┘
Where:
1#define MULLE_OBJC_CLASSPAIR_IVAR_BASE \
2 (sizeof(struct _mulle_objc_classpair) - \
3 offsetof(struct _mulle_objc_classpair, infraclass))
self in + methods = &classpair->infraclass.
Class property ivar at: (char *)self + MULLE_OBJC_CLASSPAIR_IVAR_BASE + field_offset.
This is a compile-time constant from the runtime headers.
Inheritance #
Class property ivars compose through inheritance exactly like instance ivars.
The compiler lays out superclass class ivars first, then subclass own ivars.
classinstancesize includes the full inherited + own size.
1@interface Animal : NSObject
2@property (class, assign) int population;
3@end
4
5@interface Bear : Animal
6// inherits _population at BASE+0
7@property (class, assign) int bearCount; // own ivar at BASE+sizeof(int)
8@end
[Animal population]→ Animal's accessor,self = &Animal_pair.infraclass, offset BASE+0[Bear population]→ Animal's inherited accessor,self = &Bear_pair.infraclass, offset BASE+0 ✅[Bear bearCount]→ Bear's accessor, offset BASE+sizeof(int)
Each class has independent storage — [Animal population] and
[Bear population] are different memory locations in different classpairs.
The offsets are the same, but self differs.
Subclass redeclaring a superclass class property is an error (same as instance ivars). Overriding the synthesized accessor method is allowed.
Per-class vs shared-hierarchy state #
Synthesized class property storage is per-class. [Bear new] calling
Animal's +new will access Bear's _population (because self is Bear's
infraclass at runtime).
For shared-hierarchy state (total of all Animals including subclasses),
use a @property(class, dynamic) in the base class with a file-static
backing — the file-static is compile-time bound to the declaring .m file,
so it's always the same variable regardless of which subclass dispatches to it:
1// Animal.m
2@interface Animal (Population)
3@property (class, dynamic, assign) int totalPopulation;
4@end
5
6@implementation Animal (Population)
7static int _totalPopulation = 0;
8+ (int) totalPopulation { return _totalPopulation; }
9+ (void) setTotalPopulation:(int) v { _totalPopulation = v; }
10@end
11
12@implementation Animal
13+ (instancetype) new
14{
15 Animal *a = [super new];
16 _totalPopulation++; // always Animal.m's variable, regardless of receiver
17 return a;
18}
19@end
Loadclass Changes #
Drop +classExtraSize #
The +classExtraSize method is removed. Its purpose (enlarging the classpair)
is replaced by a struct field. This is a breaking change — any existing use
of +classExtraSize must be migrated.
Drop selfstorage #
No longer needed — there is no external storage to patch.
New field: classinstancesize #
Add to struct _mulle_objc_loadclass (not base — only for classes):
1struct _mulle_objc_loadclass
2{
3 struct _mulle_objc_loadclassbase base;
4 ...
5 int instancesize;
6 int classinstancesize; // NEW: total size incl. inherited
7 struct _mulle_objc_ivarlist *instancevariables;
8 struct _mulle_objc_ivarlist *classvariables; // own class property ivars only
9 mulle_objc_classid_t *mixinids;
10};
classinstancesize = total size of all class property ivars including
inherited ones (same as instancesize includes superclass instance ivars).
The runtime uses classinstancesize as the extrasize parameter when
calling mulle_objc_universe_new_classpair. The allocated memory is zeroed.
classvariables ivar list #
The compiler emits ivar descriptors for the class's own class properties
in classvariables (not inherited ones — same as instancevariables).
Each ivar has:
name:"_sharedCount"(underscore-prefixed, like instance ivars)signature: type encodingoffset:MULLE_OBJC_CLASSPAIR_IVAR_BASE + field_position(absolute, includes inherited)
The runtime registers this ivar list on the metaclass, enabling introspection.
Accessor Synthesis #
For each class property without a user-provided accessor, the compiler synthesizes getter/setter with direct mutex locking (no message send):
1+ (int) sharedCount
2{
3 mulle_objc_infraclass_lock_classproperty(self);
4 int v = self->_sharedCount;
5 mulle_objc_infraclass_unlock_classproperty(self);
6 return v;
7}
8
9+ (void) setSharedCount:(int)value
10{
11 mulle_objc_infraclass_lock_classproperty(self);
12 self->_sharedCount = value;
13 mulle_objc_infraclass_unlock_classproperty(self);
14}
For copy properties:
1+ (void) setSharedName:(NSString *)value
2{
3 mulle_objc_infraclass_lock_classproperty(self);
4 NSString *old = self->_sharedName;
5 self->_sharedName = [value copy];
6 [old autorelease];
7 mulle_objc_infraclass_unlock_classproperty(self);
8}
User-provided accessors suppress synthesis (same rule as instance properties).
+lock/+unlock are for user code doing compound operations — synthesized
accessors never call them (avoids override surprises and dispatch overhead).
Locking #
Metaclass recursive mutex #
The runtime initializes the metaclass mutex whenever any class property list
arrives with n > 0 — either from the main class (classinstancesize > 0)
or from a category (classproperties.n > 0). This ensures the mutex is
always available when class properties exist, regardless of where they are
declared.
Destroyed at class teardown after property clearing.
+lock / +unlock / +tryLock #
Implemented in NSObject as regular class methods — inherited by all classes:
1+ (void) lock { mulle_objc_infraclass_lock_classproperty(self); }
2+ (void) unlock { mulle_objc_infraclass_unlock_classproperty(self); }
3+ (int) tryLock { return mulle_objc_infraclass_trylock_classproperty(self); }
No compiler synthesis needed. Categories can use [self lock] freely because
it is always inherited from NSObject.
Compound operations #
1[Foo lock];
2[Foo setSharedName:@"hello"];
3[Foo setSharedCount:42];
4[Foo unlock];
Recursive mutex allows re-entry from synthesized accessors within the lock.
Lifecycle — No Synthesized Methods #
The runtime manages class property ivar lifecycle entirely from metadata.
Creation #
- Runtime allocates classpair with
extrasize=classinstancesize(zeroed) - Runtime inits metaclass mutex when any class property list has
n > 0(main class or category — whichever arrives first) - Class is ready —
+initializecalled lazily on first message (user code)
Teardown #
+deinitializecalled (user code)- Runtime clears class properties: walks the full class hierarchy property lists (own + inherited superclass), calls setter with nil (or direct ivar release for readonly). Each class's own property list is walked once — the walk traverses superclasses to cover all ivars stored in this classpair.
- Runtime destroys metaclass mutex
Property clearing — migrate from MulleObjC #
Existing code to migrate:
Source: /home/src/srcO/mulle-objc/MulleObjC/src/function/MulleObjCProperty.m
Functions to move into the runtime (rename to C conventions, drop
MulleObjCObjectIsInstance assert or replace with runtime equivalent):
_MulleObjCInstanceClearProperty→_mulle_objc_instance_clear_property_MulleObjCInstanceClearPropertyNoReadOnly→_mulle_objc_instance_clear_property_noreadonly_MulleObjCClassWalkClearableProperties→mulle_objc_infraclass_walk_clearable_properties_MulleObjCInstanceClearProperties→mulle_objc_instance_clear_properties
The walk function is used as-is (with superclass traversal) for both instance dealloc and class teardown:
1int mulle_objc_infraclass_walk_clearable_properties(
2 struct _mulle_objc_infraclass *infra,
3 mulle_objc_walkpropertiescallback_t f,
4 void *userinfo);
Usage:
- Instance dealloc:
userinfo = instance - Class teardown:
userinfo = infraclass(cast toidfor setter dispatch — the infraclass IS the class object)
The superclass walk is correct for both cases: instance dealloc clears inherited instance ivars, class teardown clears inherited class ivars stored in this classpair.
After migration, MulleObjC calls the runtime functions directly — the MulleObjCProperty.m wrappers become thin or disappear.
Why no synthesized lifecycle methods #
- Init: memory is zeroed at allocation. No method needed.
- Deinit: runtime walks property list + calls setters. Same mechanism as instance dealloc property clearing.
- User hooks:
+initialize/+deinitializeremain available for custom logic (setting non-zero defaults, registering observers, etc.)
Categories #
@property(class) in categories requires dynamic #
Category class properties must be declared dynamic — same rule as instance
properties in categories. The compiler does not synthesize storage or accessors.
The user provides the implementation.
1@interface Foo (Extra)
2@property (class, dynamic, assign) int extraValue;
3@end
4
5@implementation Foo (Extra)
6static int _extraValue;
7
8+ (int) extraValue
9{
10 mulle_objc_infraclass_lock_classproperty(self);
11 int v = _extraValue;
12 mulle_objc_infraclass_unlock_classproperty(self);
13 return v;
14}
15+ (void) setExtraValue:(int)value
16{
17 mulle_objc_infraclass_lock_classproperty(self);
18 _extraValue = value;
19 mulle_objc_infraclass_unlock_classproperty(self);
20}
21@end
- No ivar descriptor registered (not introspectable)
[self lock]/[self unlock]available — inherited from NSObject- Consistent with: categories cannot add instance ivars
- Same enforcement as instance properties in categories (already implemented)
self->_field in + Methods — Implementation #
Approach: Internal struct type for self #
In + methods of a class with class properties, Sema gives self an internal
struct pointer type that includes the class property ivars at their correct
offsets. This makes self->_field resolve as a normal C MemberExpr.
The struct #
For a class Foo (inheriting from NSObject with no class properties) with
class property int sharedCount, the compiler internally creates:
1struct __Foo_classivars {
2 char __spacer[MULLE_OBJC_CLASSPAIR_IVAR_BASE]; // absorbs infraclass + classpair tail
3 // inherited superclass class ivars here (if any)
4 int _sharedCount; // own ivar
5};
For a subclass Bar : Foo adding NSString *name:
1struct __Bar_classivars {
2 char __spacer[MULLE_OBJC_CLASSPAIR_IVAR_BASE];
3 int _sharedCount; // inherited from Foo, same offset
4 NSString *_name; // own
5};
Sema #
In ActOnStartOfObjCMethodDef, when the method is a + method and the class
has class properties (own or inherited):
- Build
RecordDeclwith achar[]spacer + inherited class ivars + own class ivars - Create pointer type:
struct __Foo_classivars * - Set
self's type to this pointer type internally
Then self->_sharedCount resolves naturally via MemberExpr on the struct.
CodeGen #
No special handling needed — MemberExpr on a struct pointer emits a GEP at
the field's offset. The spacer ensures the offset is correct:
1; self->_sharedCount
2%ptr = getelementptr %struct.__Foo_classivars, ptr %self, i32 0, i32 1
3%val = load i32, ptr %ptr
Messaging still works #
self is typed as a struct pointer internally, but ObjC message sends on
struct pointers already work in mulle-objc (the metaABI casts to void *
for mulle_objc_object_call). So [self sharedCount] still dispatches
correctly — the struct type doesn't interfere with messaging.
Instance methods #
In - methods, self keeps its normal type (Foo *). Class property ivars
are not accessible via self->_field in instance methods — use
[[self class] sharedCount] instead.
What This Removes (vs current v1 implementation) #
Selfkeyword — gone__Self__file-static structs — gone- Object header on storage — gone
selfstoragefield in loadclass — gone- Runtime isa-patching — gone
+classExtraSizemethod — gone (replaced byclassinstancesizefield)+initializeSelf/+deinitializeSelf— gone (runtime handles lifecycle)
What This Keeps #
@property (class, ...)syntax- Synthesized accessors with direct mutex locking
+lock/+unlock/+tryLock(provided by NSObject, inherited by all classes)- Metaclass recursive mutex (init triggered by any class property list with n > 0)
classpropertiesproperty list in loadclassclassvariablesivar list in loadclass- Warning suppression for auto-synthesized accessors
- Category class properties (require
dynamic, user-provided accessors) - Runtime-managed ivar cleanup at teardown
Runtime Changes Required #
- Add
classinstancesizefield tostruct _mulle_objc_loadclass - Use
classinstancesizeas extrasize inmulle_objc_universe_new_classpair - Init metaclass mutex when any class property list has
n > 0(main class or category) - At teardown: walk own
classpropertieslist, call setters with nil (migrate from MulleObjCProperty.m — see above) - Remove
selfstoragefrom_mulle_objc_loadclassbase - Remove isa-patching code from
mulle-objc-load.c - Remove
+classExtraSizecall from classpair creation - Define
MULLE_OBJC_CLASSPAIR_IVAR_BASEmacro in public header
Compiler Changes Required #
- Remove
SelfVarDecl creation fromSemaDeclObjC.cpp - Remove
[Self msg]handler fromSemaExprObjC.cpp - Remove
EmitClassPropertyStoragefromCGObjCMulleRuntime.cpp - Compute class ivar layout including inherited ivars (same as instance ivar layout)
- Emit
classinstancesize(total incl. inherited) in loadclass struct - Emit
classvariablesivar list (own ivars only, absolute offsets) in loadclass struct - In
ActOnStartOfObjCMethodDef: build internal__Foo_classivarsstruct forselfin+methods (spacer + inherited + own class ivars) - Synthesize accessors using
self->_field(resolved via internal struct type) with directmulle_objc_infraclass_lock/unlock_classpropertycalls - No
+lock/+unlock/+tryLocksynthesis — provided by NSObject - Enforce
dynamicfor class properties in categories (already done for instance properties) - Update rewriter
- Bump load version to v22