4

I have a Swift object that takes a dictionary of blocks (keyed by Strings), stores it and runs block under given key later at some point depending on external circumstances (think different behaviours depending on the backend response):

@objc func register(behaviors: [String: @convention(block) () -> Void] {
  // ...
}

It's used in a mixed-language project, so it needs to be accessible from both Swift and Objective-C. That's why there's @convention(block), otherwise compiler would complain about not being able to represent this function in Objective-C.

It works fine in Swift. But when I try to invoke it from Objective-C like that:

[behaviorManager register:@{
  @"default": ^{
    // ...
  }
}];

The code crashes and I get following error:

Could not cast value of type '__NSGlobalBlock__' (0x...) to '@convention(block) () -> ()' (0x...).

Why is that, what's going on? I thought @convention(block) is to specifically tell the compiler that Objective C blocks are going to be passed, and that's exactly what gets passed to the function in the call.

9
  • 2
    You shouldn't have to do this, but you might try sending a copy message to the block: [^{ ...} copy]. Commented Nov 4, 2022 at 16:07
  • 1
    I don't have the answer due the lack of information on the whole classes. But I'll hint you to read the implementation about the NSGlobalBlock. In some circumstances, it get deallocated because no strong pointer to it, and ARC take it down. Source: clang.llvm.org/docs/Block-ABI-Apple.html I also saw something about copying the block. Commented Nov 4, 2022 at 17:06
  • 2
    I can reproduce the problem, it looks like a bug to me. Copying the block does not help in my test. Commented Nov 4, 2022 at 17:19
  • 1
    Some older threads about problems with passing blocks between Swift and Objective-C: stackoverflow.com/q/24586293/1187415, stackoverflow.com/q/24595692/1187415, stackoverflow.com/q/46224806/1187415. It seems that one has to pass the block as an AnyObject, and then “unsafeBitCast” it back to a Swift block. Commented Nov 4, 2022 at 21:15
  • 1
    Or wrap the block in a custom class, as here: stackoverflow.com/a/24760061/1187415. Commented Nov 4, 2022 at 21:39

1 Answer 1

2

That's why there's @convention(block), otherwise compiler would complain about not being able to represent this function in Objective-C

For the sake of consistency: commonly you use @convention attribute the other way around - when there is an interface which takes a C-pointer (and implemented in C) or an Objective-C block (and implemented in Objective-C), and you pass a Swift closure with a corresponding @convention as an argument instead (so the compiler actually can generate appropriate memory layout out of the Swift closure for the C/Objective-C implementation). So it should work perfectly fine if it's Objective-C side where the Swift-created closures are called like blocks:

@interface TDWObject : NSObject

- (void)passArguments:(NSDictionary<NSString *, void(^)()> *)params;

@end

If the class is exposed to Swift the compiler then generates corresponding signature that takes a dictionary of @convention(block) values:

func passArguments(_ params: [String : @convention(block) () -> Void])

This, however, doesn't cancel the fact that closures with @convention attribute should still work in Swift, but the things get complicated when it comes to collections, and I assume it has something with value-type vs reference-type optimisation of Swift collections. To get it round, I'd propose to make it apparent that this collection holds a reference type, by promoting it to the [String: AnyObject] and casting later on to a corresponding block type:

@objc func takeClosures(_ closures: [String: AnyObject]) {
    guard let block = closures["One"] else {
        return // the block is missing
    }
    let closure = unsafeBitCast(block, to: ObjCBlock.self)
    closure()
}

Alternatively, you may want to wrap your blocks inside of an Objective-C object, so Swift is well aware of that it's a reference type:

typedef void(^Block)();

@interface TDWBlockWrapper: NSObject

@property(nonatomic, readonly) Block block;

@end

@interface TDWBlockWrapper ()

- (instancetype)initWithBlock:(Block)block;

@end

@implementation TDWBlockWrapper

- (instancetype)initWithBlock:(Block)block {
    if (self = [super init]) {
        _block = block;
    }
    return self;
}

@end

Then for Swift it will work as simple as that:

@objc func takeBlockWrappers(_ wrappers: [String: TDWBlockWrapper]) {
    guard let wrapper = wrappers["One"] else {
        return // the block is missing
    }
    wrapper.block()
}
Sign up to request clarification or add additional context in comments.

2 Comments

Hi. The suggestion to change the type of collection to "[String: AnyObject]", and then cast it by calling "let objCBlock = unsafeBitCast(block, to: (@convention(block) () -> Void).self)" seems to do the trick. I even tried unsafeBitCast before asking this question, but I must've been doing something wrong. Thank you! It doesn't solve underlying issue per se, but that's impossible given that it looks like a compiler bug. I filed a radar to Apple, and in the meantime I'll accept this answer as it lays out possible workarounds. Thanks again.
One final piece of advice I have for this situation: when calling this function from Swift, it any blocks in the collection need to be casted "as @convention(block) () -> Void as AnyObject".

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.