iOS on Zen

NSDevelopment blog

Story about libdispatch

Intro

Once upon a time I got interested in reason of why dispatch_object and ARC are compatible. It was about 2 years ago and since that moment I totally forgot about anything related to dispatch_queue and ARC. However, about a week ago Autocomplete added [queue isKindOfClass:] for dispatch_queue_t by accident. Moreover - no errors or warning was produced!

Thats confused me for a moment but when I went to the declaration everything start to fall into place. So this post is all about my weekend’s evening :)

A little bit of history

Going back to 2011 Apple introduces ARC - users don’t have to use retain, release or autorelease anymore, weak references available, developers happy.

Year later with announce of iOS 6 Apple moving dispatch_object unders ARC as well. It means no more dispatch_release and dispatch_retain! Memory management code is not identical for ARC and non-ARC environment but everything else is the same!

So how they did it?

All in declaration

First of all lets take a look at dispatch_queue_t declaration:

queue.h
1
DISPATCH_DECL(dispatch_queue);

Looking at DISPATCH_DECL declaration we can found huge number of #if defines, which help to emit different code for every environment: C, Objective-C and C++.

Going forward I would like to highlight part of libdispatch’s inheritance structure: root os_object, which provides basic memory management functionality. Then dispatch_object - root for libdispatch and number of inherited objects like dispatch_queue, dispatch_group and so on. We need to understand, that under C this inheritance is a fake because of C-lang nature, but the overall structure should be the same for all 3 langs.

NSObject<OS_dispatch_object> * aka dispatch_object_t

For Objective-C if we print object’s description in debugger the OS_dispatch_queue class will be revealed. Superclass - OS_dispatch_object. And it is totally not surprising from declaration of DISPATCH_DECL which is in fact a macro for OS_OBJECT_DECL_SUBCLASS(name, dispatch_object):

object.h
1
2
3
4
5
6
7
8
9
#if OS_OBJECT_USE_OBJC
#import <objc/NSObject.h>
#define OS_OBJECT_CLASS(name) OS_##name
#define OS_OBJECT_DECL(name, ...) \
     @protocol OS_OBJECT_CLASS(name) __VA_ARGS__ \
     @end \
     typedef NSObject<OS_OBJECT_CLASS(name)> *name##_t
#define OS_OBJECT_DECL_SUBCLASS(name, super) \
     OS_OBJECT_DECL(name, <OS_OBJECT_CLASS(super)>)

As you can see any dispatch_object’s subclass declared as:

1
2
3
4
@protocol OS_dispatch_subclass <OS_dispatch_object>
@end

typedef NSObject<OS_dispatch_subclass> *dispatch_subclass_t;

Whereas dispatch_object declared as:

1
2
3
4
@protocol OS_dispatch_object
@end

typedef NSObject<OS_dispatch_object> *dispatch_object_t;

@interface OS_object

So behind dispatch_name_t lies NSObject<OS_dispatch_name>. But debugger says about OS_dispatch_name, as a class not a protocol. Looks like someone is hidding the real declaration from us! Fortunately, libdispatch is Open Source and we can find out, who those OS_dispatch_objects guys are. We’re looking for some macros or declaration regarding dispatch_object or similar.

You can download libdispatch and inspect source on your own. It contains a lot of stuff to look at! For example list of supported platforms has: macosx, iphoneos, iphoneossimulator, iphoneosnano, iphoneosnanosimulator. Haven’t seen them before :)

After some inspection I found, that there are some macros, which define OS_dispatch_* classes. Lets open dispatch_internal.h. There we can find DISPATCH_CLASS_DECL(queue); among other stuff. Jump to definition opens object_internal.h

1
#define DISPATCH_CLASS_DECL(name) DISPATCH_SUBCLASS_DECL(name, dispatch_object)

Which in fact gets expanded into:

1
2
3
4
@interface OS_dispatch_queue : OS_dispatch_object <OS_dispatch_queue>
@end

// here comes other stuff, that will be desсribed later

So dispatch_queue_t simply an Obj-C class and now it is clear, why libdispatch’s objects are compatible with ARC.

Now lets try to find a little bit more about OS_* and OS_dispatch_* internals. To do so we need to take a look at corresponding declaration:

object_private.h
1
2
3
4
5
@interface OS_object : NSObject
- (void)_xref_dispose;
- (void)_dispose;
@end
typedef OS_object *_os_object_t;

Under the hood all the OS_object does is perform memory management handling by passing retain, release to os-object-specific retain count implementation and disposing handling. Also there is some weak-references handling, but to be honest i haven’t dig it a lot, since it is out of topic.

As you remember Obj-C support for libdispatch implemented atop of cross-platform C core. Therefore in order to utilize single memory management code they simply pass all the retain-release calls to os-specific functions like _os_object_retain and _os_object_release respectively.

Curious about implementation details? Check out OS_object and memory management details

@interface OS_dispatch_object

Now lets dive into OS_dispatch_object:

object_internal.h
1
2
3
4
5
6
7
8
9
10
@interface OS_dispatch_object : OS_object <OS_dispatch_object>
@end

extern struct dispatch_object_vtable_s {
  // will be covered later
} _dispatch_object_vtable;

struct dispatch_object_s {
  // will be covered later
};

Things get interesting here! Besides class declaration there are 2 extra structures, which is used during allocation! But before we look at how these structures are actually used, I would like to reveal implementation of OS_dispatch_object first:

object.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@implementation OS_dispatch_object

- (id)init {
  self = [super init];
  [self release];
  self = nil; // trollface.jpg
  return self;
}

- (void)_xref_dispose {
  _dispatch_xref_dispose(self);
  [super _xref_dispose];
}

- (void)_dispose {
  return _dispatch_dispose(self); // calls _os_object_dealloc()
}

- (NSString *)debugDescription {
  // nothing interesting for now
}

@end

As you can see main purpose of this class is to perform some extra cleanup during disposing. Also -init implemented in such a funny way because of all allocation and initialization done in corresponding C-functions, like dispatch_queue_create.

Allocation and Init

Briefly reader should be aware of two separate versions of alloc-code which are separated by #define checks: C version with calloc and the one we’re going to review in a moment - Obj-C.

Lets jump to one of the well-known functions - dispatch_queue_create:

I deliberately simplifying and inlining alloc code since we’re interested only in how Obj-C compatibility implemented.

1
2
3
4
5
6
7
8
dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
{
  dispatch_queue_t dq = _dispatch_alloc(&_dispatch_queue_vtable, sizeof(struct dispatch_queue_s));

  // ...

  return dq;
}
1
2
3
4
5
6
7
8
9
10
11
12
void *_dispatch_alloc(const void *vtable, size_t size)
{
  return _os_objc_alloc(cls, size);
}

id _os_objc_alloc(Class cls, size_t size)
{
  id obj;
  size -= sizeof(((struct _os_object_s *)NULL)->os_obj_isa);
  obj = class_createInstance(cls, size);
  return obj;
}

For clarity and simplicity of understanding lets inline allocation:

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
{
  size_t isa_size = sizeof(((struct _os_object_s *)NULL)->os_obj_isa);
  size_t extra_size = sizeof(struct dispatch_queue_s) - isa_size;
  dispatch_queue_t dq = class_createInstance(&_dispatch_queue_vtable, extra_size);

  // ...

  return dq;
}

It is time to reveal meaning of ‘dispatch_object_vtable_s’. To be honest I was confused for a minute when opened this function for the first time. I hope you remember dispatch_object_vtable_s and dispatch_object_s. But before line-by-line description I’d like to show you, how ‘dispatch_data_t’ gets allocated:

1
2
3
4
5
6
7
dispatch_data_t _dispatch_data_alloc(size_t n, size_t extra)
{
  size_t extra_size = /* some size computation */
  dispatch_queue_t dq = class_createInstance((Class)&OBJC_CLASS_$_OS_dispatch_data, extra_size);

  return data;
}

This code should be much more verbose. Useful for us here is the explanation of the meaning of &_dispatch_queue_vtable. Now it is time to expand already mentioned dispatch_object_vtable_s and dispatch_object_s:

object_internal.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct dispatch_object_s;

extern struct dispatch_object_vtable_s {
  // Apple: Must match size of compiler-generated OBJC_CLASS structure rdar://10640168
  void *_os_obj_objc_class_t[5];

  unsigned long const do_type;
  const char *const do_kind;
  size_t (*const do_debug)(struct dispatch_object_s *, char *, size_t);
  void (*const do_invoke)(struct dispatch_object_s *);
  unsigned long (*const do_probe)(struct dispatch_object_s *);
  void (*const do_dispose)(struct dispatch_object_s *);
} _dispatch_object_vtable;

struct dispatch_object_s {
  const struct dispatch_object_vtable_s *do_vtable,
  int volatile do_ref_cnt,
  int volatile do_xref_cnt
  struct dispatch_object_s *volatile do_next;
  struct dispatch_queue_s *do_targetq;
  void *do_ctxt;
  void *do_finalizer;
  unsigned int volatile do_suspend_cnt;
};

As you can see from Apple’s comment the vtable holds dispatch-specific fields as well as reserves space for Obj-C compiler-generated structure, which one is prefixed by OBJC_CLASS_$_. dispatch_object_s in turn mimics NSObject where isa pointer comes as a first field.

When a new object is created, memory for it is allocated, and its instance variables are initialized. First among the object’s variables is a pointer to its class structure. This pointer, called isa, gives the object access to its class and, through the class, to all the classes it inherits from.

Knowing the essence of dispatch_*_vtable_s and dispatch_*_s lets go through allocation of dispatch_queue_t one more time:

1
2
3
size_t isa_size = sizeof(((struct _os_object_s *)NULL)->os_obj_isa);
size_t extra_size = sizeof(struct dispatch_queue_s) - isa_size;
dispatch_queue_t dq = class_createInstance(&_dispatch_queue_vtable, extra_size);

The best way to describe extra size computation is to compare memory layout of OS_dispatch_object vs dispatch_object_s:

Since OS_dispatch_object doesn’t have anything except isa in it the instance size will be equal to the size of that pointer. However, dispatch_object_s has a lot more data. Hence libdispatch needs somehow add extra bytes to instance. Fortunately, Obj-C runtime allow us to allocate extra bytes for Class’s instance as a single chunk of memory via class_createInstance(Class cls, size_t extraBytes).

What it gives us? Due to how objects represented in the memory we can treat OS_dispatch_object as dispatch_queue_s *. In the end pointer’s type is just a hint. It helps both developer and compiler to treat particular memory region as Int * or maybe as NSObject *. The only requirement here is to respect bounds of the destination memory region. Therefore OS_dispatch_queue allocated with appropriate size easily passed into dispatch_async as dispatch_queue_s *.

Alias

The next question is how Obj-C runtime fetch correct class metadata from &dispatch*vtable pointer. Briefly it is done via linker aliases when dispatch_queue_vtable_s gets co-located with OS_dispatch_queue class structure.

Lets open libdispatch.xcconfig on OBJC_LDFLAGS key:

1
OBJC_LDFLAGS = -Wl,-alias_list,$(SRCROOT)/xcodeconfig/libdispatch_objc.aliases /* more flags here */

The content of libdispatch_objc.aliases describes aliases between Obj-C class structures and vtables:

1
2
3
4
5
_OBJC_CLASS_$_OS_dispatch_semaphore __dispatch_semaphore_vtable
_OBJC_CLASS_$_OS_dispatch_group __dispatch_group_vtable
_OBJC_CLASS_$_OS_dispatch_queue __dispatch_queue_vtable

// and so on

For more understanding take a look at how Obj-C class and vtable gets co-located:

Linker co-locate dispatch_object_vtable_s and OS_dispatch_object at the same address within Mach-O executable. Therefore pointer to vtable gets treated by Obj-C Runtime as a Class. Unfortunately I was unable to reproduce the same behaviour in my sample app. Search on the web lists mostly Apple’s OpenSource website. Hope someone from readers will get more lucky there.

Summary

As you may see things under the hood look more complicated than public interface declares. Apple engineers did really great job in order to make 3rd party developers life even easier. For sure knowledge of how libdispatch internal works won’t help us in everyday development. However complex libraries done by real professionals always interesting to explore isn’t it?

This is it for today, thanks for reading :)

Comments