Block's underlying implementation (3): _block storage domain class descriptor

We know that in Block usage, when we want to modify the value of the intercepted variable, we need to add the _block modifier to the variable; and in some cases, we need to add the _block modifier to avoid circular references.
So why do we need to add _block to modify the value of the intercepted variable?
The previous two articles have written blocks without parameters and intercepted variables respectively. Next, add _block to the blocks of intercepted variables to compile and see what's different.
First, we create the required changes and blocks, which are as follows:

int main(){
    __block int val = 10;
    void(^blk)(void) = ^{
        val  = 1;
    };
    blk();
    return 0;
};

Next, you compile the command clang-rewrite-objc source file name to see the compiled code.
The compiled code is as follows:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref

        (val->__forwarding->val) = 1;
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(){
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {
                 (void*)0,
                 (__Block_byref_val_0 *)&val, 
                 0, 
                 sizeof(__Block_byref_val_0),
                 10
           };
    void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));

    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
};

Let's take a look at the differences between the two previous articles.

  • The variable Val of the int type added with _block is converted into the variable of the structure type of _Block_byref_val_0 through the block grammar, and is added as the member variable of _main_block_impl_0.
  • Then there is an additional function pointer for copy and dispose in _main_block_desc_0.
    Let's first look at the declaration of the _Block_byref_val_0 structure
struct __Block_byref_val_0 {
 void *__isa;
 __Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

Its structure is the isa function pointer, the _forwarding pointing to itself, and the assignment of some flags, sizes and intercepted variables.
Here's a question, why is there a _forwarding pointing to its member variable?
In addition, the _Block_byref_val_0 structure of the _block variable is not in the _main_block_impl_0 structure of the Block, which is designed to use the _block variable in multiple Blocks.
Another difference is the assignment in the _main_block_func_0 function.

(val->__forwarding->val) = 1;

Why not use val - > Val to change the value directly instead of val - > forwarding - > val?
Before we solve these two problems, let's first look at the location of the Block.
From the previous code, we can see the initialization of _main_block_impl_0

impl.isa = &__NSConcreateStackBlock

Block has two other similar classes:

  • _ NSConcreteStackBlock is stored on the stack and destroyed when it leaves the stack.
  • _ NSConcreteGlobal Block is a static global block that does not access any external variables.
  • _ NSConcreteMallocBlock is stored in the heap and destroyed when the reference count is zero.

As you can see from the previous code, the original type of block is _NSConcreateStackBlock, which is in the stack. That is to say, if the scope of the variable to which it belongs ends, the block will be discarded. Because the _block variable is also configured in the stack, the scope of the variable ends, and the _block variable will be discarded. If so, the existence of the block beyond the scope will not exist. At this point, our NSConcreteMallocBlock came on the scene and saved everything. Blocks provided a way to copy the Block and _block variables from the stack to the heap to solve this problem.
Copy the Block and _block variables that are configured on the stack to the heap so that the block on the heap can continue even if the variable scope described in the Block grammar ends.
Block copied to the heap writes the _NSConcreteMallocBlock class object to the member variable isa of Block's structural strength.
The _block variable can be accessed correctly when the _block variable is configured on the stack or on the stack by using the structural member variable _forwarding.
At this time, there is a problem. We said before that forwarding refers to itself. If only the variables are copied from the stack to the stack, it can not meet the conditions. So when the block variable is copied from the stack to the stack, the forwarding on the stack points to the variables on the stack, not to itself.

Finally, let's talk about the differences in _main_block_des_0.
Let's take a look at the implementation of two function pointers copy and dispose. The code is as follows:

static void __main_block_copy_0 (struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0 (struct __main_block_impl_0*src)     {
    _Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}

As you can see from the above code, although we have implemented it, we will find that we have not been invoked, so where are these functions invoked?
When a block is copied from the stack to the heap, it holds the variable (equivalent to retain) using the _Block_object_assign function. When a block on the heap is discarded, the variable (equivalent to release) is released using the _Block_object_dispose function.
This is what the system does when variables with _block modifiers are intercepted by Block and their values are modified.

Posted by sn202 on Mon, 27 May 2019 16:15:19 -0700