Instance variable access in Objective-C

12 Feb 2014

In the last post I mentioned using pointer arithmetic to access private instance variables in Objective-C.

I received interesting feedback from a reader wanting to know more about how instance variables work in general.

To understand instance variables, we’ll look at how Objective-C runtime implements objects. I can’t think of a legitimate use for this in production, so this post is intended for fun.


//Given this:
@interface Foo() {
    @public
    NSString *_name;
}
@end

@implementation Foo  @end

//Why does this declare a pointer to a foo instance's _name!
NSString *name = (__bridge id)*(void **)((__bridge void *)aFooInstance + 4);


Building and running the above code, we can attach lldb instantiate a Foo to test things out:

(lldb) $expr Foo *$aFoo = [Foo new]

(lldb) expr $aFoo->_name = @"Boo";
(NSString *) $24 = 0x08c612a0 @"Boo"


aFoo is a pointer to some allocated memory in the heap, which lldb nicely visualizes if we dereference aFoo:

(lldb) p *$aFoo
(Foo) $0 = {
  NSObject = {
    isa = Foo
  }
  _name = 0x00004884 @"Boo"
}


An instance of Foo has a few fields in it. The first field of aFoo is isa, which is a pointer to aFoo’s class.

(lldb) p $aFoo->isa
(Class) $9 = Foo


Since isa is the first field of the object, we can use pointer arithmetic to access it.

(lldb) po *(id *)((char *)aFoo + 0)
Foo


If you’re wondering why the instance variable _name can be accessed by adding 4 to foo, its because 4 bytes is the size of a pointer to the NSString Class on my computer.

(lldb) p sizeof([NSString class])
(unsigned long) $18 = 4


Lets take a look at the Objective-C code:

(objc.h)

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;


An id is 4 bytes in size because it is actually declared as a pointer to a typdef struct objc_object, which is 4 bytes.

(lldb) p sizeof(struct objc_object)
(unsigned long) $20 = 4

(lldb) p sizeof(id)
(unsigned long) $18 = 4


Since a pointer NSString class is an id, it makes sense that it is 4 bytes large.

By now, it is obvious why the original code accesses the same value pointed to by the instance variable _name.

Without the casts for ARC support - the original example is much clener. We can try it in the debugger:

(lldb) po *(id *)((char *)foo + 4)
Boo


in English:

Cast foo to a pointer to a char, Add 4, Cast that to a pointer to an id, Dereference that.

Runtime

Calculating the offset is unnecessary do in production, because the runtime does it well. If you read through the runtime, you will see that the function:

id object_getIvar(id obj, Ivar ivar)


Can access instance variables in constant time, because the offset is already stored by the runtime

(objc-runtime-new.h)

typedef struct ivar_t {
    // *offset is 64-bit by accident even though other 
    // fields restrict total instance size to 32-bit. 
    uintptr_t *offset;
    const char *name;
    const char *type;
    // alignment is sometimes -1; use ivar_alignment() instead
    uint32_t alignment  __attribute__((deprecated));
    uint32_t size;
} ivar_t;

Published on 12 Feb 2014 Find me on Twitter!