Why is Clang confused by @try{} in a block with no

2019-02-16 21:11发布

问题:

Under normal conditions, when a block is declared to return a value, but no return statement actually appears in the block, Clang fails to compile it with an error (of a missing return value).

However, this breaks when that block contains @try{} @catch(...){} or @try{} @finally{}.

Does anyone know why?

The way I found this was when using @weakify() and @strongify() in RACExtScope in ReactiveCocoa, in one block I forgot to return a signal. But the compiler didn't warn me and crashed on runtime, which lead me to dig into it, preprocess the code and find that this causes it. Any explanation would be very much appreciated, I honestly don't know why this would happen, thanks!

I also created a gist, in case someone had a comment/suggestion: https://gist.github.com/czechboy0/11358741

int main(int argc, const char * argv[])
{
    id (^iReturnStuff)() = ^id() {
        @try{} @finally{}
        //if you comment out line 4, Clang will not compile this.
        //if you leave it like this, Clang will compile and run this, even though
        //there's no value being returned.
        //is there something special in @try{} that turns off compiler errors?
    };
    return 0;
}

回答1:

Clang's block specification makes brief mention of control flow in a block. I've reproduced it here (emphasis mine)

The compound statement of a Block is treated much like a function body with respect to control flow in that goto, break, and continue do not escape the Block. Exceptions are treated normally in that when thrown they pop stack frames until a catch clause is found.

Reading through a little further, you really get the sense that exceptions in Objective-C are downright weird. From the section on exceptions

The standard Cocoa convention is that exceptions signal programmer error and are not intended to be recovered from. Making code exceptions-safe by default would impose severe runtime and code size penalties on code that typically does not actually care about exceptions safety. Therefore, ARC-generated code leaks by default on exceptions, which is just fine if the process is going to be immediately terminated anyway. Programs which do care about recovering from exceptions should enable the option.

From the above, one could reasonably deduce that the ObjC exceptions specification is so fragile or malleable that not even the compiler writers can guarantee stable code against it, therefore they just disabled all reasonable termination checks in once @try-@catch are encountered.

This can also be seen in the code generated by Clang with and without the try-catches. First, without

___main_block_invoke:
    pushq   %rbp
    movq    %rsp, %rbp
    movabsq $0, %rax
    movq    %rdi, -8(%rbp)
    movq    %rdi, -16(%rbp)
    popq    %rbp
    ret

This is pretty simple x86 that pushes a new stack frame, moves 0 (nil) into the return register, then returns. Now, with the try-catch block:

___main_block_invoke:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $64, %rsp
    movq    %rdi, -16(%rbp)
    movq    %rdi, -24(%rbp)
    movb    $0, -25(%rbp)
    movl    -32(%rbp), %eax
    testb   $1, -25(%rbp)
    movl    %eax, -48(%rbp)         ## 4-byte Spill
    jne LBB1_1
    jmp LBB1_3
    LBB1_1:
    callq   _objc_exception_rethrow
    jmp LBB1_2
    LBB1_2:
    LBB1_3:
    movl    -48(%rbp), %eax         ## 4-byte Reload
    movl    %eax, -32(%rbp)
    movq    -8(%rbp), %rdi
    addq    $64, %rsp
    popq    %rbp
    jmp _objc_autoreleaseReturnValue ## TAILCALL
    LBB1_4:
    movl    %edx, %ecx
    movq    %rax, -40(%rbp)
    movl    %ecx, -44(%rbp)
    testb   $1, -25(%rbp)
    jne LBB1_5
    jmp LBB1_7
    LBB1_5:
    callq   _objc_end_catch
    jmp LBB1_6
    LBB1_6:
    jmp LBB1_7
    LBB1_7:
    jmp LBB1_8
    LBB1_8:
    movq    -40(%rbp), %rdi
    callq   __Unwind_Resume
    LBB1_9:
    movq    %rdx, -56(%rbp)         ## 8-byte Spill
    movq    %rax, -64(%rbp)         ## 8-byte Spill
    callq   _objc_terminate

Besides the more complicated function proem, notice the lack of a proper ret. The function still has two exit points,

jmp   _objc_autoreleaseReturnValue

and

call  _objc_terminate

The first is a relatively new feature of the language where, when in the tailcall position, it can be used to omit -autoreleases in favor of drawing upon thread-local variables by examining the code that came before it. The second begins immediate termination of the process and jumps into the C++ exception handling mechanism. What this means is that the function does, in fact, have the requisite exit points to keep CLANG from complaining about missing return statements. Unfortunately, what it also means is that CLANG's forgoing of messing with the ObjC exception mechanism can potentially message garbage, as you've seen. This is one of the reasons EXTScope has switched to using the @autoreleasepool directive to eat that sigil.