Copy blocks - copy captured variables of primitive types

I searched for several topics about copying blocks, but I could not find the information that interests me.

When we define a block, we have the ability to capture variables from its scope. Since blocks are stored on the stack and variables are captured by value, everything is clear here:

  • in the case of primitive types, we acquire an additional variable (also localized on the stack), for example. some const int that has the same meaning as the original int variable
  • in the case of pointers, we acquire a copy of a specific pointer - so that the reference counter for the specified object increases by 1.

Now I do not know what will happen when we move (copy) a block from the stack to the heap. For captured pointers, it's simple - we get a copy of these pointers. But what happens to the captured variables of primitive types? Variables in the heap are allocated dynamically, so we can only refer to them with pointers. This means that we cannot just copy, for example, int variable into a heap - we can dynamically allocate the int variable, assign it to some int * pointer, and write the corresponding value through this pointer - the same as in the original int variable. But for this we need some additional mechanism that will be behind the scenes. In addition, when we fix a certain variable in a block, the block “prepares” itself to work with a variable of a certain size and in a certain way - if we change a variable of a primitive type to a pointer, it will usually have a different size, and this will require a different way of processing it. ..

So can someone please tell me how it works in depth? Or am I just mistaken at some point?

+4
source share
4 answers

Details on gory can be found in the Block Implementation Specification .

The easiest way 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 this 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 the encoding of the signature of the block type:

 struct Block_descriptor_1 block_descriptor_1 = { .size = sizeof(struct Block_literal_1), .signature = " v4@ ?0" }; 

And he creates a function containing the block body:

 void outerFunction_block_invoke(void *voidLiteral) { struct Block_literal_1 *literal = (struct Block_literal_1 *)voidLiteral; printf("%d\n", literal->x); } 

Note that the body of the block was rewritten in such a way that access to the variable x from the enclosing area is now access to the element of the block literal.

Finally, the compiler overwrites the original function to create a block literal on the stack and to use the address of this block literal instead of a 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); } 

Note that a block literal begins with a pointer to a special Objective-C Class . This allows you to process block literature as an Objective-C object.

If you add the __block attribute to the local variable x , it becomes more complex. In this case, the compiler must create another structure for storing this variable along with information about this variable (for example, how to save and release it if the variable is a pointer to an object). All of this is explained in the specification that I linked at the top of this answer.

+7
source

I'm not quite sure what you are asking, but perhaps a simple explanation of how the blocks actually work will clarify the situation.

A block is actually an instance of an anonymous class. The class has ivar for each captured variable. When a block instance is created, all captured variables are copied to their corresponding ivars on the block, which are currently stored on the stack.

When a block is copied to the heap, it creates a new block object on the heap and copies all ivars from the stack block to the heap block (it also saves all captured obj-c objects on it). There is no confusion over pointers to primitive values ​​and much more; there is only one malloc'd region on the heap that contains all the fixed values, like any other obj-c object.

Meanwhile, the actual code in the block simply accesses the captured variables using the equivalent of implicit_block_pointer->backing_ivar , just like the method on the object will access the ivars of the object.

+3
source

What you do is the lack of pointers if you could learn to program with Algol-68 ... ( ref loc anyone?)

[It is somewhat simplified to present the essence of what is happening.]

When you declare a variable, say:

 int x; 

You instruct the compiler to find a location that can store an int representation, and use that location for the values ​​you reference using the name x .

Skipping the “find” bit, the compiler builds an internal table, a character table that maps the names, x here, to locations — and the locations are represented either as absolute addresses (or pointers) or as offsets from something, such as something like “7 place on the stack. " Sometimes, something is stored inside a computer in a special named place called a register, for example. there is a register in which the stack pointer is stored, and therefore the variables stored as offsets to the stack pointer are located as offsets from the value stored in this register.

Using this table, a compiler lookup of x can determine the address where the representation of x is stored.

At the machine command level, reading or writing a variable involves using an instruction that takes an address. Therefore, by the time you go to the actual code of the machine, all variables are referenced via pointers.

Now to your block building. When you write our example variable x integer, the compiler allocates a place for it in the structure that describes the block. In the symbol table, he creates an entry for x and displays it as "6th place in the variable block area". The location of the variable block region is located in a certain place, possibly in a register, like the stack pointer above, and then machine instructions determine the value of x as an offset from this location.

When a block is based on the stack, the blocking area of ​​the block will be on the stack, when it is on the heap, it will be on the heap, but since its location is stored in the register until the block is executed, the code of the block never needs to be changed - x always at the same offset relative to the block variable region.

Hope all that makes sense!

+2
source

An additional mechanism that "works behind the scenes" is access to the C-member C. On the heap stack, blocks are structures with elements for each captured variable (they are also Objective-C objects). When a block is executed, a function is called that takes a pointer to the block as an argument. This function accesses captured variables of type blockPointer->capturedVar1 . It doesn't matter where blockPointer points to this point - all that matters is the space allocated for the captured variable inside any structure.

You may find it enlightening: http://clang.llvm.org/docs/Block-ABI-Apple.html

+1
source

All Articles