Xcode Clang does not treat non-escaping blocks as global blocks
I've noticed that the Clang included with Xcode16 (and possibly older), does not treat blocks that capture but are marked non-escaping as global blocks, while open source Apple Clang does. It seems like https://reviews.llvm.org/D49303 has effectively been reverted in Xcode's Clang.
I have a simple program:
#import <Foundation/Foundation.h>
@interface Foo: NSObject
- (void)takeABlock: (void (^NS_NOESCAPE)(void))block;
@end
@implementation Foo
- (void)takeABlock: (void (^NS_NOESCAPE)(void))block {
block();
}
@end
int main(int argc, char** argv) {
Foo* foo = [Foo new];
int captured_local = argc + 1;
[foo takeABlock: ^{ printf("%d", captured_local); }];
}
If I dump the IR with open source Swift's Clang as follows:
$SWIFT_LLVM_PATH/clang++ -Oz -isysroot $(xcrun --show-sdk-path) -framework Foundation -S -emit-llvm simple-test.mm -o swift/simple-test.ll
I get the following definition for main:
; Function Attrs: minsize mustprogress norecurse optsize ssp uwtable(sync)
define noundef i32 @main(i32 noundef %argc, ptr nocapture noundef readnone %argv) local_unnamed_addr #1 {
entry:
%block = alloca <{ ptr, i32, i32, ptr, ptr, i32 }>, align 8
%0 = load ptr, ptr @"OBJC_CLASSLIST_REFERENCES_$_", align 8
%1 = load ptr, ptr @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !11
%call = tail call noundef ptr @objc_msgSend(ptr noundef %0, ptr noundef %1) #5
%add = add nsw i32 %argc, 1
store ptr @_NSConcreteGlobalBlock, ptr %block, align 8
%block.flags = getelementptr inbounds <{ ptr, i32, i32, ptr, ptr, i32 }>, ptr %block, i64 0, i32 1
store <2 x i32> <i32 -796917760, i32 0>, ptr %block.flags, align 8
%block.invoke = getelementptr inbounds <{ ptr, i32, i32, ptr, ptr, i32 }>, ptr %block, i64 0, i32 3
store ptr @__main_block_invoke, ptr %block.invoke, align 8
%block.descriptor = getelementptr inbounds <{ ptr, i32, i32, ptr, ptr, i32 }>, ptr %block, i64 0, i32 4
store ptr @"__block_descriptor_36_e5_v8\01?0l", ptr %block.descriptor, align 8
%block.captured = getelementptr inbounds <{ ptr, i32, i32, ptr, ptr, i32 }>, ptr %block, i64 0, i32 5
store i32 %add, ptr %block.captured, align 8, !tbaa !12
%2 = load ptr, ptr @OBJC_SELECTOR_REFERENCES_.4, align 8, !invariant.load !11
call void @objc_msgSend(ptr noundef %call, ptr noundef %2, ptr nocapture noundef nonnull %block) #5
ret i32 0
}
We still seem to honor the change made in https://reviews.llvm.org/D49303. This is the version of Apple Clang I am testing with:
clang version 17.0.0 (https://github.com/swiftlang/llvm-project.git 95f3fb07f8f52940912fdb0ba6f58fd0fe0f2511)
Target: arm64-apple-darwin23.6.0
Thread model: posix
InstalledDir: /Users/nuriamari/git/swift-project/build/Ninja-RelWithDebInfoAssert/llvm-macosx-arm64/bin
But if I repeat with Xcode16's Clang like so:
xcrun clang++ -Oz -isysroot $(xcrun --show-sdk-path) -framework Foundation -S -emit-llvm simple-test.mm -o xcode/simple-test.ll
We don't treat the block as global even though it is marked with NS_NOESCAPE:
; Function Attrs: minsize norecurse optsize ssp uwtable(sync)
define i32 @main(i32 noundef %0, ptr nocapture noundef readnone %1) local_unnamed_addr #1 {
%3 = alloca <{ ptr, i32, i32, ptr, ptr, i32 }>, align 8
%4 = load ptr, ptr @"OBJC_CLASSLIST_REFERENCES_$_", align 8
%5 = tail call ptr @objc_opt_new(ptr %4)
%6 = add nsw i32 %0, 1
store ptr @_NSConcreteStackBlock, ptr %3, align 8 ; the isa pointer is different
%7 = getelementptr inbounds <{ ptr, i32, i32, ptr, ptr, i32 }>, ptr %3, i64 0, i32 1
store <2 x i32> <i32 -1073741824, i32 0>, ptr %7, align 8 ; and the flags also reflect the difference
%8 = getelementptr inbounds <{ ptr, i32, i32, ptr, ptr, i32 }>, ptr %3, i64 0, i32 3
store ptr @__main_block_invoke, ptr %8, align 8
%9 = getelementptr inbounds <{ ptr, i32, i32, ptr, ptr, i32 }>, ptr %3, i64 0, i32 4
store ptr @"__block_descriptor_36_e5_v8\01?0l", ptr %9, align 8
%10 = getelementptr inbounds <{ ptr, i32, i32, ptr, ptr, i32 }>, ptr %3, i64 0, i32 5
store i32 %6, ptr %10, align 8, !tbaa !12
call void @"objc_msgSend$takeABlock:"(ptr noundef %5, ptr noundef undef, ptr nocapture noundef nonnull %3) #4
ret i32 0
}
Is there a reason this behavior was turned off in Xcode Clang? It seems beneficial to keep it. cc @ahatanaka @rjmccall