@objc func foo() async?

I tried to make an @objc function async. Said function is registered via the NotificationCenter as an observer. While the compiler did not complain, I get a reproducible crash in objc_retain() when posting the notification.

Is that supposed to work? If not, would a compiler error not be a good thing then? Or should I open a bug report?

I would expect this to work, but I think we'll need more information about what specifically is going on. Filing a bug with a reproducible test case would help a lot here.

Doug

1 Like

Alright. Created a bug entry and a reproducible test case:

Swift Bug: [SR-15291] Concurrency: Crash when invoking async @objc function · Issue #57613 · apple/swift · GitHub

Example Project: GitHub - mickeyl/swift-bugs: Example Projects for Swift Bug Tracker Issues – directory SR15291

Cheers, :m:

FWIW, this is still broken in 5.5.2-dev as per Xcode 13.2RC. Is this so obscure that no-one else stumbled over it?

I sooo share this feeling sometimes (last one, oh, and FB9801372 as well), living in an eternal and unsufficiently tested beta, losing time hitting bugs, finding workarounds, building minimum reproducible cases, writing consensual reports of head-banging bugs, etc.

We live in a time of crowdsourced & unpaid testing, but without the expected rewards: thanks, quick fixes, and fast releases.

7 Likes

Still a problem with Xcode 13.3RC.

Still a problem with Xcode 13.4RC.

Still a problem with Xcode 14.0 beta 1.

FWIW, now that issues have moved, the corresponding issue is here

Still the same with Xcode 15.0 beta 1.

I vaguely remember a discussion that this will be left as is. If so, please someone (other than me) close it as WONTFIX ­– perhaps with an explanation for the records.

I’m a bit confused why this would be considered a bug, aside from the fact that maybe it should emit a compiler diagnostic.

IIUC, this method will be translated to Objective-C as something like -(void) fooWithCompletion:(void(^)(void))completion. The documentation for NotificationCenter says:

The method that aSelector specifies must have one and only one argument (an instance of NSNotification).

So it passes an NSNotification instead of a block, the Swift code tries to use it the wrong way, and it crashes.

Perhaps I’m wrong, and this is expected to work. But at least to me, this crash isn’t surprising.

Well, it's a bug because I asked and got the notion that it should work.

If you don't consider this a bug, it's at least a target for improving the diagnostics.

@bbrk24 is correct about how this will be translated. The ObjC convention for async functions is that they take a block as a completion handler, which is not the convention that NSNotificationCenter uses, so you cannot use an async method for notifications this way. These dynamic ObjC technologies have always been dangerous since there's zero type enforcement associated with them.

It would be nice to diagnose this mismatch statically, but at the moment we'd have to hard-code this specific NSNotificationCenter API into the compiler to do it, because there's nothing in the ObjC header that tells the compiler what type of method the selector is expected to have.

1 Like

Are you aware of non selector based notification centre APIs?

NotificationCenter.default.addObserver(forName: .init("name"), object: nil, queue: nil) { note in
    ...
}

Bonus point - you can supply a specific queue there.

Out of curiosity, what would happen if the function were changed to func foo(_: NSNotification) async? That would be translated as -(void) foo:(NSNotification *)_, completion:(void(^)(void))completion, which I can imagine going a couple different ways:

  1. Objective-C passes nil for the second parameter, and…
    a. …Swift isn’t prepared, and it still crashes.
    b. …Swift is okay with that, and it works fine.
  2. Objective-C doesn’t specify a value for the second parameter, leading to a wild pointer and most likely (but not always) a crash.

Objective-C method sends effectively just look up the method pointer, cast it to a specific C function pointer type, and call it. (The mechanics are a bit different than that, but ultimately that's the behavior you get.) There's no mechanism in the language to fill in extra parameters that the caller didn't provide. C calling conventions are generally lax about providing extra arguments, which comes in handy with APIs like performSelector:withObject:afterDelay: that would otherwise need an overload to be used with nullary selectors, but missing arguments (or severely mis-typed arguments, e.g. float vs. int) are just going to misbehave.

It would be possible for an API to inspect either the selector or the target object's method metadata to decide how to call it, but that's uncommon, and I don't believe NSNotificationCenter does. So yeah, the method will get a bogus block pointer as a completion handler and blow up.

Just FYI, I believe Swift does not allow ObjC async methods to be called with a nil completion handler.

2 Likes