MainActor.assumeIsolated crashes on main thread

I am seeing the following crash in my software:

Crashed Thread:        0  Dispatch queue: com.apple.main-thread

Exception Type:        EXC_BREAKPOINT (SIGTRAP)
Exception Codes:       0x0000000000000001, 0x00000001a7d2e1f8

Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   libswiftCore.dylib            	       0x1a7d2e1f8 _assertionFailure(_:_:file:line:flags:) + 176
1   Light Table                   	       0x10423d788 specialized static MainActor.assumeIsolated<A>(_:file:line:) + 388
2   Light Table                   	       0x104087cf8 CommitDetailsTextView.awakeFromNib() + 68 (CommitDetailsTextView.swift:11) [inlined]
3   Light Table                   	       0x104087cf8 @objc CommitDetailsTextView.awakeFromNib() + 120 (/<compiler-generated>:8)
4   CoreFoundation                	       0x196659b08 -[NSSet makeObjectsPerformSelector:] + 176
...

So, I am on the main thread (Crashed Thread: 0 Dispatch queue: com.apple.main-thread), but the cause is MainActor.assumeIsolated? This is not something I expected to be possible.

This is happening when a NIB is loaded, with code like this:

class CommitDetailsTextView: NSTextView { // implicit @MainActor
	// ...
	
	override func awakeFromNib() { // implicit NOT @MainActor
		super.awakeFromNib()
		
		MainActor.assumeIsolated {
			self._awakeFromNib()
		}
	}
	
	private func _awakeFromNib() {
		self.font = Self.displayFont // must happen on main thread
		// ...
	}
}

The regular awakeFromNib is from NSObject and thus not main actor isolated. This is why I have this jump to a main actor isolated _awakeFromNib all over my project so I can access main actor isolated IBOutlet variables and such.

Just to try, I replaced the MainActor.assumeIsolated { … } above with a custom awakeFromNibOnMainThread { … } like so:

func awakeFromNibOnMainThread(_ body: @escaping @MainActor () -> ()) {
	if Thread.isMainThread {
		MainActor.assumeIsolated {
			body()
		}
	}
	else {
		Task { @MainActor in
			body()
		}
	}
}

But that still crashes:

Crashed Thread:        0  Dispatch queue: com.apple.main-thread

Exception Type:        EXC_BREAKPOINT (SIGTRAP)
Exception Codes:       0x0000000000000001, 0x00000001a7d2e1f8

Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   libswiftCore.dylib            	       0x1a7d2e1f8 _assertionFailure(_:_:file:line:flags:) + 176
1   Light Table                   	       0x126789058 specialized static MainActor.assumeIsolated<A>(_:file:line:) + 388
2   Light Table                   	       0x12674073c awakeFromNibOnMainThread(_:) + 232 (awakeFromNibOnMainThread.swift:5)
3   Light Table                   	       0x1265b3550 CommitDetailsTextView.awakeFromNib() + 16 (CommitDetailsTextView.swift:11) [inlined]
4   Light Table                   	       0x1265b3550 @objc CommitDetailsTextView.awakeFromNib() + 132 (/<compiler-generated>:8)
5   CoreFoundation                	       0x196659b08 -[NSSet makeObjectsPerformSelector:] + 176
...

Is this to be expected? MainActor.assumeIsolated crashing on the main thread? Or is this just a bug?

Tested with Swift 6.2 in Xcode 26 beta and Swift 6.1 in Xcode 16.4 on macOS 15.5.

3 Likes

Looks like a bug.

Would that work in the meantime?

override func awakeFromNib() { // implicit NOT @MainActor
	super.awakeFromNib()
	
	Task { @MainActor in
		self._awakeFromNib()
	}
}

That’s not a great workaround as some of my code expects to be run after the initial view setup has happened in awakeFromNib.

Is there a way to run @MainActor code without runtime checks? Not as a real solution, but to bridge the bug.

What might be worth confirming here is if "legacy" code that runs on the main thread without explicitly isolating to main thread with modern concurrency should not crash from assumeIsolated. I don't have an answer for that…

1 Like

I’m not quite sure what you mean by that. Legacy code as in AppKit or Objective-C in general outside the Swift compiler?

I wonder if Task.startSynchronously avoids a thread hop here?

override func awakeFromNib() {
	super.awakeFromNib()
	
	Task.startSynchronously { @MainActor in
		self._awakeFromNib()
	}
}

I was thinking legacy code as in code that might use something like GCD dispatchMain to send work to main thread. But code that is not compiled with Swift Strict Concurrency and does not put its types on MainActor directly.

1 Like

I think that is the case here. This is the full stack trace, which is mostly AppKit loading a storyboard and its individual NIB files triggered by a menu item action:

Crashed Thread:        0  Dispatch queue: com.apple.main-thread

Exception Type:        EXC_BREAKPOINT (SIGTRAP)
Exception Codes:       0x0000000000000001, 0x00000001a7d2e1f8

Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   libswiftCore.dylib            	       0x1a7d2e1f8 _assertionFailure(_:_:file:line:flags:) + 176
1   Light Table                   	       0x10423d788 specialized static MainActor.assumeIsolated<A>(_:file:line:) + 388
2   Light Table                   	       0x104087cf8 CommitDetailsTextView.awakeFromNib() + 68 (CommitDetailsTextView.swift:11) [inlined]
3   Light Table                   	       0x104087cf8 @objc CommitDetailsTextView.awakeFromNib() + 120 (/<compiler-generated>:8)
4   CoreFoundation                	       0x196659b08 -[NSSet makeObjectsPerformSelector:] + 176
a6   AppKit                        	       0x19a5d3dbc -[NSNib _instantiateNibWithExternalNameTable:options:] + 332
7   AppKit                        	       0x19a5d3ba8 -[NSNib _instantiateWithOwner:options:topLevelObjects:] + 132
8   AppKit                        	       0x19a5d3508 -[NSViewController loadView] + 296
9   AppKit                        	       0x19a5d3304 -[NSViewController _loadViewIfRequired] + 72
10  AppKit                        	       0x19ac16e58 __24-[NSViewController view]_block_invoke + 28
11  AppKit                        	       0x19a5cd070 NSPerformVisuallyAtomicChange + 108
12  AppKit                        	       0x19a5d3274 -[NSViewController view] + 160
13  AppKit                        	       0x19a5f8774 -[_NSSplitViewItemViewWrapper wrapView] + 72
14  AppKit                        	       0x19a5f7838 -[NSSplitViewController _setupSplitView] + 312
15  AppKit                        	       0x19a5f74e4 -[NSSplitViewController viewDidLoad] + 164
16  AppKit                        	       0x19a5e5650 -[NSViewController _sendViewDidLoad] + 84
17  AppKit                        	       0x19a5d33a8 -[NSViewController _loadViewIfRequired] + 236
18  AppKit                        	       0x19ac16e58 __24-[NSViewController view]_block_invoke + 28
19  AppKit                        	       0x19a5cd070 NSPerformVisuallyAtomicChange + 108
20  AppKit                        	       0x19a5d3274 -[NSViewController view] + 160
21  AppKit                        	       0x19a7040b4 -[NSTabViewController _goodTabViewContentSize] + 120
22  AppKit                        	       0x19a703ec8 -[NSTabViewController viewDidLoad] + 88
23  AppKit                        	       0x19a5e5650 -[NSViewController _sendViewDidLoad] + 84
24  AppKit                        	       0x19a5d33a8 -[NSViewController _loadViewIfRequired] + 236
25  AppKit                        	       0x19ac16e58 __24-[NSViewController view]_block_invoke + 28
26  AppKit                        	       0x19a5cd070 NSPerformVisuallyAtomicChange + 108
27  AppKit                        	       0x19a5d3274 -[NSViewController view] + 160
28  AppKit                        	       0x19a5f8774 -[_NSSplitViewItemViewWrapper wrapView] + 72
29  AppKit                        	       0x19a5f7838 -[NSSplitViewController _setupSplitView] + 312
30  AppKit                        	       0x19a5f74e4 -[NSSplitViewController viewDidLoad] + 164
31  AppKit                        	       0x19a5e5650 -[NSViewController _sendViewDidLoad] + 84
32  AppKit                        	       0x19a5d33a8 -[NSViewController _loadViewIfRequired] + 236
33  AppKit                        	       0x19ac16e58 __24-[NSViewController view]_block_invoke + 28
34  AppKit                        	       0x19a5cd070 NSPerformVisuallyAtomicChange + 108
35  AppKit                        	       0x19a5d3274 -[NSViewController view] + 160
36  AppKit                        	       0x19a7569a4 -[NSWindow _contentViewControllerChanged] + 80
37  AppKit                        	       0x19a5cd070 NSPerformVisuallyAtomicChange + 108
38  AppKit                        	       0x19a7568e0 -[NSWindow setContentViewController:] + 132
39  Foundation                    	       0x197c08a1c -[NSObject(NSKeyValueCoding) setValue:forKey:] + 324
40  AppKit                        	       0x19a781f34 -[NSWindow setValue:forKey:] + 144
41  AppKit                        	       0x19a604354 -[NSIBUserDefinedRuntimeAttributesConnector establishConnection] + 168
42  AppKit                        	       0x19a56da6c -[NSIBObjectData nibInstantiateWithOwner:options:topLevelObjects:] + 732
43  AppKit                        	       0x19a5d3dbc -[NSNib _instantiateNibWithExternalNameTable:options:] + 332
44  AppKit                        	       0x19a5d3ba8 -[NSNib _instantiateWithOwner:options:topLevelObjects:] + 132
45  AppKit                        	       0x19afebbd8 -[NSStoryboard _instantiateControllerWithIdentifier:creator:storyboardSegueTemplate:sender:] + 644
46  AppKit                        	       0x19afeb900 -[NSStoryboard instantiateControllerWithIdentifier:creator:] + 24
47  AppKit                        	       0x19afeb934 -[NSStoryboard instantiateControllerWithIdentifier:] + 20
48  Light Table                   	       0x1040b6a78 specialized static Dashboard.dashboard(for:) + 248 (Dashboard.swift:24)
49  Light Table                   	       0x1041f1544 static Dashboard.dashboard(for:) + 8 [inlined]
50  Light Table                   	       0x1041f1544 specialized Plugin.showRepository(_:) + 264 (Plugin.swift:349)
51  Light Table                   	       0x1041f0d78 @objc Plugin.showRepository(_:) + 148
52  Glyphs 3                      	       0x1008a722c 0x10082c000 + 504364
53  AppKit                        	       0x19a7f32d8 -[NSMenuItem _corePerformAction] + 372
54  AppKit                        	       0x19af56510 _NSMenuPerformActionWithHighlighting + 152
55  AppKit                        	       0x19adaa7f0 -[NSMenu _performKeyEquivalentForItemAtIndex:] + 172
56  AppKit                        	       0x19a7f232c -[NSMenu performKeyEquivalent:] + 356
57  AppKit                        	       0x19af2b108 routeKeyEquivalent + 444
58  AppKit                        	       0x19af28fdc -[NSApplication(NSEventRouting) sendEvent:] + 652
59  Glyphs 3                      	       0x1008a6d8c 0x10082c000 + 503180
60  AppKit                        	       0x19ab2842c -[NSApplication _handleEvent:] + 60
61  AppKit                        	       0x19a57ec8c -[NSApplication run] + 520
62  AppKit                        	       0x19a55535c NSApplicationMain + 880
63  Glyphs 3                      	       0x1008329b4 0x10082c000 + 27060
64  dyld                          	       0x1961dab98 start + 6076

Since that code is not using any await, I think it’s the same as without Task.startSynchronously { … }. Also, the docs read

Create and immediately start running a new task in the context of the calling thread/task.

So, I think this would not accept a @MainActor closure anyway.

1 Like
extension MainActor {
    
    /// - Note: https://github.com/swiftlang/swift/blob/dbf7fe6aa01c958fa6711df30423e88ff22c0b75/stdlib/public/Concurrency/MainActor.swift#L128
    @available(*, noasync)
    nonisolated
    static func myAssumeIsolated<T: Sendable>(_ operation: @MainActor () throws -> T) rethrows -> T {
        
        precondition(Thread.isMainThread)
        
        return try withoutActuallyEscaping(operation) {
            return try unsafeBitCast($0, to: (() throws -> T).self)()
        }
        
    }
    
}

class CommitDetailsTextView: NSTextView {
    
    override func awakeFromNib() {
        super.awakeFromNib()
        
        MainActor.myAssumeIsolated {
            self._awakeFromNib()
        }
        
    }
    
    private func _awakeFromNib() {
        self.font = Self.displayFont
    }
    
}
1 Like

Thanks, that works for now.

There is still the issue that a MainActor.assumeIsolated call nested inside _awakeFromNib() { … } can cause a crash, so I would like to understand if this is something I can fix in my code or if this is a bug in Swift.

I would definitely suggest filing a bug report about it. Either it's a bug in Swift in that it shouldn't happen, or it's a bug in Swift in that we should make it more clear what's going on and what to do about it.

4 Likes