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;
}
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.