Matt Galloway

My home on the 'net.

A look inside blocks: Episode 3 (Block_copy)

This post has been a long time coming. It’s been a draft for many months, but I’ve been busy writing my book and didn’t have time to finish it off. But now I’ve finished it and here it is!

Following on from episode 1 and episode 2 of my look inside blocks, this post takes a deeper look at what happens when a block is copied. You’ve likely heard the terminology that “blocks start off on the stack” and “you must copy them if you want to save them for later use”. But, why? And what actually happens during a copy? I’ve long wondered exactly what the mechanism is for copying a block. For example, what happens to the values captured by the block? In this post I take a look.

What we know so far

From episodes 1 and 2, we found out that the memory layout for a block is like this:

Block layout diagram

In episode 2 we found out that this struct is created on the stack when the block is initially referenced. Since it’s on the stack, the memory can be reused after the enclosing scope of the block ends. So what happens then if you want to use that block later on? Well, you have to copy it. This is done with a call to Block_copy() or rather just send the Objective-C message copy to it, since a block poses as an Objective-C object. This just calls Block_copy().

So what better than to take a look at what Block_copy() does.

Block_copy()

First of all, we need to look in Block.h. Here there are the following definitions:

1
2
3
#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))

void *_Block_copy(const void *arg);

So Block_copy() is purely a #define that casts the argument passed in to a const void * and passes it to _Block_copy(). There is also the prototype for _Block_copy(). The implementation is in runtime.c:

1
2
3
void *_Block_copy(const void *arg) {
    return _Block_copy_internal(arg, WANTS_ONE);
}

So that just calls _Block_copy_internal() passing the block itself and WANTS_ONE. To see what this means, we need to look at the implementation. This is also in runtime.c. Here is the function, with the irrelevant stuff removed (mostly garbage collection stuff):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;
    const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;

    // 1
    if (!arg) return NULL;

    // 2
    aBlock = (struct Block_layout *)arg;

    // 3
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }

    // 4
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }

    // 5
    struct Block_layout *result = malloc(aBlock->descriptor->size);
    if (!result) return (void *)0;

    // 6
    memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first

    // 7
    result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
    result->flags |= BLOCK_NEEDS_FREE | 1;

    // 8
    result->isa = _NSConcreteMallocBlock;

    // 9
    if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
        (*aBlock->descriptor->copy)(result, aBlock); // do fixup
    }

    return result;
}

And here is what that method does:

  1. If the passed argument is NULL then just return NULL. This makes the method safe to passing a NULL block.

  2. Cast the argument to a pointer to a struct Block_layout. You may remember what one of these is from episode 1. It’s the internal data structure that makes up a block including a pointer to the implementation function of the block and various bits of metadata.

  3. If the block’s flags includes BLOCK_NEEDS_FREE then the block is a heap block (you’ll see why shortly). In this case, all that needs doing is the reference count needs incrementing and then the same block returned.

  4. If the block is a global block (recall these from episode 1) then nothing needs doing and the same block is returned. This is because global blocks are effectively singletons.

  5. If we’ve gotten here, then the block must be a stack allocated block. In which case, the block needs to be copied to the heap. This is the fun part. In this first step, malloc() is used to create a portion of memory of the required size. If that fails, then NULL is returned, otherwise we carry on.

  6. Here, memmove() is used to copy bit-for-bit then current, stack allocated block to the portion of memory we just allocated for the heap allocated block. This just makes sure that all the metadata is copied over such as the block descriptor.

  7. Next, the flags of the block are updated. The first line ensures that the reference count is set to 0. The comment indicates that this is not needed – presumably because at this point the reference count should already be 0. I guess this line is left in just in case a bug ever exists where the reference count is not 0. The next line sets the BLOCK_NEEDS_FREE flag. This indicates that it’s a heap block and the memory backing it will, once the reference count drops to zero, require free-ing. The | 1 on this line sets the reference count of the block to 1.

  8. Here the block’s isa pointer is set to be _NSConcreteMallocBlock, which means it’s a heap block.

  9. Finally, if the block has a copy helper function then this is invoked. The compiler will generate the copy helper function if it’s required. It’s required for blocks that capture objects for example. In such cases, the copy helper function will retain the captured objects.

That’s pretty neat, eh! Now you know what happens when a block is copied! But that’s only half of the picture, right? What about when one is released?

Block_release()

The other half of the Block_copy() picture is Block_release(). Once again, this is actually a macro that looks like this:

1
#define Block_release(...) _Block_release((const void *)(__VA_ARGS__))

Just like Block_copy(), Block_release() calls through to a function after casting the argument for us. This just helps out the developer, so that they don’t have to cast themselves.

Let’s take a look at _Block_release() (with slight rearrangement for clarity and garbage collection specific code removed):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void _Block_release(void *arg) {
    // 1
    struct Block_layout *aBlock = (struct Block_layout *)arg;
    if (!aBlock) return;

    // 2
    int32_t newCount;
    newCount = latching_decr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK;

    // 3
    if (newCount > 0) return;

    // 4
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)(*aBlock->descriptor->dispose)(aBlock);
        _Block_deallocator(aBlock);
    }

    // 5
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        ;
    }

    // 6
    else {
        printf("Block_release called upon a stack Block: %p, ignored\n", (void *)aBlock);
    }
}

And here’s what each bit does:

  1. First the argument is cast to a pointer to a struct Block_layout, since that’s what it is. And if NULL is passed in, then we return early to make the function safe against passing in NULL.

  2. Here the portion of the block flags that signifies the reference count (recall from Block_copy() the part where the flags were set to indicate a reference count of 1) is decremented.

  3. If the new count is greater than 0, then there’s still things holding a reference to the block and so the block does not need to be freed yet.

  4. Otherwise, if the flags include BLOCK_NEEDS_FREE, then this is a heap allocated block, and the reference count is 0, so the block should be freed. First of all though, the dispose helper function of the block is invoked. This is the antonym of the copy helper function. It performs the reverse, such as releasing any captured objects. Finally, the block is deallocated through use of _Block_deallocator. If you go hunting in runtime.c then you’ll see that this ends up being a function pointer to free, which just frees memory allocated with malloc.

  5. If we made it here and the block is global, then do nothing.

  6. If we made it all the way to here, then something strange has happened because a stack block has attempted to be released, so a log line is printed to warn the developer. In reality, you should never see this being hit.

And that is that! There’s not really much more to it!

What’s next?

That concludes my tour into blocks, for now. Some of this material is covered in my book. It’s more about how to use blocks effectively, but there’s still a good portion of deep-dive material that should be of interest if you enjoyed this.

Comments