I've searched several topics about copying blocks, but I couldn't have found information that I'm interested in.
When we define a block, we have an opportunity to capture variables from its enclosing scope. Since blocks are stored on the stack, and variables are captured by value, everything is clear here:
Now, I don't know what happens when we move (copy) a block from the stack to the heap. For captured pointers it is simple - we acquire a copy of those pointers. But what happens with captured variables of primitive types? Variables on the heap are allocated dynamically, so we can refer to the them only with pointers. It means that we cannot simply copy the e.g. int variable to the heap - we can dynamically allocate an int variable, assign it to some int * pointer, and through that pointer write the appropriate value - the same as in the original int variable. But for this we would need some additional mechanism, that woks behind the scenes. Additionally, when we capture some variable in the block, block "prepares" itself to operate on a variable of specific size and in specific manner - if we would change the variable of primitive type to a pointer, it usually would have different size and it would require different way of handling it...
So could someone please tell me, how does it work in depth? Or am I simply wrong at some point?
You can find the gory details in the Block Implementation Specification.
It is easiest to explain with an example. Consider this simple function containing a simple block:
void outerFunction() {
int x = 7;
dispatch_block_t block = ^{
printf("%d\n", x);
};
dispatch_sync(dispatch_get_main_queue(), block);
}
Remember that dispatch_block_t
is a typedef for void (^)(void)
.
To compile that code, the compiler will first create two structure definitions:
struct Block_descriptor_1 {
unsigned long reserved;
unsigned long size;
const char *signature;
};
struct Block_literal_1 {
void *isa;
int flags;
int reserved;
void (*invoke)(void *);
struct Block_descriptor_1 *descriptor;
int x;
};
Then it creates a global variable of type Block_descriptor_1
, which contains the size of Block_literal_1
and an encoding of the type signature of the block:
struct Block_descriptor_1 block_descriptor_1 = {
.size = sizeof(struct Block_literal_1),
.signature = "v4@?0"
};
And it creates a function containing the body of the block:
void outerFunction_block_invoke(void *voidLiteral) {
struct Block_literal_1 *literal = (struct Block_literal_1 *)voidLiteral;
printf("%d\n", literal->x);
}
Notice that the body of the block has been rewritten so that the access to the variable x
from the enclosing scope is now an access to a member of the block literal.
Finally, the compiler rewrites the original function to create a block literal on the stack and to use the address of that block literal instead of the block:
void outerFunction2() {
int x = 7;
struct Block_literal_1 block_literal_1 = {
.isa = __NSConcreteStackBlock,
.flags = BLOCK_HAS_SIGNATURE,
.invoke = outerFunction_block_invoke,
.descriptor = &block_descriptor_1,
.x = x
};
dispatch_sync(dispatch_get_main_queue(),
(__bridge dispatch_block_t)&block_literal_1);
}
Notice that the block literal starts with a pointer to a special Objective-C Class
. This allows the block literal to be treated as an Objective-C object.
If you add the __block
attribute to the local variable x
, it gets more complicated. In that case, the compiler has to create another structure to hold that variable along with information about the variable (like how to retain and release it if the variable is a pointer to an object). This is all explained in the specification I linked at the top of this answer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With