Objective-C の __block の参照カウンタを調査中…

と呟いたところ、本の著者さんからというお返事をいただいたので、 splhack: 『iOS 4プログラミングブック』 第5章マルチスレッド 補遺 その2 の例を自分でも確かめてみました。

という設定で上記の @ さんの記事のコード(以下に再掲)をビルドし、

#import <Foundation/Foundation.h>
#import <stdio.h>

extern const char *_Block_byref_dump(void *);

void dump(int line, int *p)
{
    p -= 4;
    printf("\ndump line:%d\n", line);
    puts(_Block_byref_dump(p));
}

int *test()
{
    __block int total = 11;
    
    dump(__LINE__, &total);
    
    void (^block_on_stack)() = ^{
        
        ++total;
        
        dump(__LINE__, &total);
    };
    
    block_on_stack();
    
    printf("\n___ Block_copy ___\n");
    void (^block_on_heap)() = Block_copy(block_on_stack);
    
    dump(__LINE__, &total);
    
    block_on_stack();
    
    block_on_heap();
    
    printf("\n___ Block_release ___\n");
    Block_release(block_on_heap);
    
    dump(__LINE__, &total);
    
    block_on_stack();
    
    return &total;
}

int main()
{
    dump(__LINE__, test());
}

Assembly を見ると、test() の最後で以下のようなコードが実行されているのが見つかります。

	.loc	1 45 1                  ## (略)/main.m:45:1
	movl	-112(%ebp), %eax        ## 4-byte Reload
	movl	%eax, (%esp)
	movl	$8, 4(%esp)
	calll	__Block_object_dispose

どうやら __block 変数の __forwarding 先の参照カウントは、Block_copy() した時にスタックからの参照分とヒープから参照分だけカウンタを増やしておいて、Block_release() 時とスコープの終わりに挿入される _Block_object_dispose() 時に減らしているようですね。

https://llvm.org/svn/llvm-project/compiler-rt/trunk/BlocksRuntime/runtime.c

void _Block_object_dispose(const void *object, const int flags) {
    // (略)
    if (flags & BLOCK_FIELD_IS_BYREF)  {
        // get rid of the __block data structure held in a Block
        _Block_byref_release(object);
    }
    // (略)
}

というわけで

という心配は杞憂のようでした。

ところで、実行してみたところ次のような出力を得ました。

dump line:17
byref data block 0xbffff9c0 contents:
  forwarding: 0xbffff9c0
  flags: 0x0
  size: 20


dump line:23
byref data block 0xbffff9c0 contents:
  forwarding: 0xbffff9c0
  flags: 0x0
  size: 20


___ Block_copy ___

dump line:31
byref data block 0x13e450 contents:
  forwarding: 0x13e450
  flags: 0x1000004
  size: 20


dump line:23
byref data block 0x13e450 contents:
  forwarding: 0x13e450
  flags: 0x1000004
  size: 20


dump line:23
byref data block 0x13e450 contents:
  forwarding: 0x13e450
  flags: 0x1000004
  size: 20


___ Block_release ___

dump line:40
byref data block 0x13e450 contents:
  forwarding: 0x13e450
  flags: 0x1000002
  size: 20


dump line:23
byref data block 0x13e450 contents:
  forwarding: 0x13e450
  flags: 0x1000002
  size: 20


dump line:49
byref data block 0x13e450 contents:
  forwarding: 0x13e450
  flags: 0x1000001
  size: 20

@ さんの記事の結果とは以下の点が若干異なっています。

  • 謎1: flags の下位 16bit の参照カウントが、Block_copy() で 4 増え、Block_release() で 2 減る
    • _Block_object_dispose() 内では flags を 1 しか減らしていないように見えるのですが…
  • 謎2: 最後の dump line:49 では参照カウントが 1 のまま
    • 参照カウントは OSAtomicCompareAndSwapInt() で減らされるので、最後はちゃんと 0 になるべきであるはず


というお返事をいただいたので libclosure-53 を覗いてみると、

http://www.opensource.apple.com/source/libclosure/libclosure-53/Block_private.h

enum {
    BLOCK_DEALLOCATING =      (0x0001),
    BLOCK_REFCOUNT_MASK =     (0xfffe),
    BLOCK_NEEDS_FREE =        (1 << 24),
    (略)
};

となっており、参照カウントは1bit左へシフトされていました。上記の結果になるのも納得です。