Weird behavior using MainActor isolation across modules on iOS 17

Hey,

I am not sure if this is an iOS bug (should be reported to apple) or a Specific MainActor isolation (aka swift lang) issue that we are facing.

When running the entire project using the @MainActor isolation as the default isolation for the main app and some UI modules we get the following crash directly on any iOS device below 18.0.

    0x1028dd568 <+80>:  bl     0x1028e2ac0               ; symbol stub for: swift_getExtendedFunctionTypeMetadata

\->  0x1028dd56c <+84>:  adrp   x9, 15

    0x1028dd570 <+88>:  add    x9, x9, #0x400            ; lazy cache variable for type metadata for nonisolated(nonsending) @Sendable () async throws -> ()

Currently this can be reproduced using the following product

How to reproduce using the project:
1- Clone and build using Xcode 26.0.1
2- Using a simulator above iOS 18 the app would work.
3- Downloading a iOS 17.5 or below would lead to crash above.

parts of the stack trace:

Interesting sections of the crash:

TestApp.debug.dylib`protocol witness for ObservableObject.objectWillChange.getter in conformance LogoutManager:
    0x1028dd4b0 <+0>:  sub    sp, sp, #0x20
    0x1028dd4b4 <+4>:  stp    x29, x30, [sp, #0x10]
    0x1028dd4b8 <+8>:  add    x29, sp, #0x10
    0x1028dd4bc <+12>: str    x8, [sp, #0x8]
    0x1028dd4c0 <+16>: ldr    x0, [x20]
    0x1028dd4c4 <+20>: bl     0x1028e25bc               ; symbol stub for: Combine.ObservableObject< where τ_0_0.ObjectWillChangePublisher == Combine.ObservableObjectPublisher>.objectWillChange.getter : Combine.ObservableObjectPublisher
->  0x1028dd4c8 <+24>: ldr    x8, [sp, #0x8]
    0x1028dd4cc <+28>: str    x0, [x8]
    0x1028dd4d0 <+32>: ldp    x29, x30, [sp, #0x10]
    0x1028dd4d4 <+36>: add    sp, sp, #0x20
    0x1028dd4d8 <+40>: ret    
    0x1b87c4d54 <+224>: mov    x3, x20
    0x1b87c4d58 <+228>: bl     0x1b885ad18               ; symbol stub for: dispatch thunk of Swift.SetAlgebra.init<τ_0_0 where τ_1_0: Swift.Sequence, τ_0_0.Element == τ_1_0.Element>(__owned τ_1_0) -> τ_0_0
    0x1b87c4d5c <+232>: stp    x23, x22, [x29, #-0x80]
    0x1b87c4d60 <+236>: sub    x8, x29, #0x58
    0x1b87c4d64 <+240>: stp    x19, x8, [x29, #-0x70]
    0x1b87c4d68 <+244>: adrp   x2, 3
    0x1b87c4d6c <+248>: add    x2, x2, #0x328            ; partial apply forwarder for closure #1 (Swift.UnsafePointer<Swift.Int8>, Swift.Int, Any.Type, Swift._MetadataKind) -> Swift.Bool in Combine.ObservableObject< where τ_0_0.ObjectWillChangePublisher == Combine.ObservableObjectPublisher>.objectWillChange.getter : Combine.ObservableObjectPublisher
    0x1b87c4d70 <+252>: sub    x3, x29, #0x90
    0x1b87c4d74 <+256>: mov    x0, x24
    0x1b87c4d78 <+260>: mov    x1, x21
    0x1b87c4d7c <+264>: bl     0x1b885adb4               ; symbol stub for: Swift._forEachField(of: Any.Type, options: Swift._EachFieldOptions, body: (Swift.UnsafePointer<Swift.Int8>, Swift.Int, Any.Type, Swift._MetadataKind) -> Swift.Bool) -> Swift.Bool
->  0x1b87c4d80 <+268>: ldr    x8, [x27, #0x8]
    0x1b87c4d84 <+272>: mov    x0, x21
    0x1b87c4d88 <+276>: mov    x1, x20
    0x1b87c4d8c <+280>: blr    x8
    0x1b87c4d90 <+284>: ldur   x0, [x29, #-0x58]
    0x1b87c4d94 <+288>: cbnz   x0, 0x1b87c4da0           ; <+300>
    0x1b87c4d98 <+292>: mov    x0, x19
    0x1b87c4d9c <+296>: bl     0x1b87c48d4               ; static Combine.ObservableObjectTable.subscript.getter : <τ_0_0 where τ_0_0: Combine.ObservableObject>(τ_0_0) -> Combine.ObservableObjectPublisher
    0x1b87c4da0 <+300>: sub    sp, x29, #0x50
    0x1b87c4da4 <+304>: ldp    x29, x30, [sp, #0x50]
1 Like

I suspect the problem here is that the compiler is emitting a runtime call to instantiate metadata for a nonisolated(nonsending) function type, which is not supported on iOS 17. I don't know what the expected behavior is with backward deployment of such function types, perhaps @hborla or @John_McCall could chime in.

The question that comes immediately to mind is whether you’re setting your deployment target correctly. You do need to do this; code built for a minimum of iOS 26 is not guaranteed to run on older releases.

2 Likes

Sorry for the late reply,

Well the project is set to run on iOS 16.4, and we set the default isolation to main actor. I wonder if an error should be emitted then in that case to limit the user of the main actor isolation to iOS 26 and above instead of allowing it on iOS targets that are lower than that.

You’re right that Swift should either be rejecting this code or making it work. Have you filed a bug?

I haven't filed a bug, where can i do so? at developer.apple.com? or on github swiftlang?

Either is fine! Apple feedback could work if you had a project you didn’t want to share publicly, but since you’ve already got a reproducer, no reason not to use the public issue tracker.

I've submitted one to apple for now since it seems that I should do that first, since it related to Xcode, iOS, SwiftUI and Combine.

1 Like

It appears to be a bug in the Swift compiler. You should be able to trigger it without using SwiftUI and Combine by using the standard library's Mirror type, for example by calling dump() on a struct with a stored property that contains an nonisolated(nonsending) function type.

Oh okay! then i can definitely report it to swiftlang/swift, and reference this post too. if its not reported already

1 Like

Thanks for your info. I wonder what is the supposed way to use nonisolated(nonsending) by design? Should there be an OS runtime requirement? I find the evolution proposal a little bit vague on this.

2 Likes

I don’t think so. It looks like for backward deployment, we should strip off the new attribute when we emit reflection metadata. There’s no way to reflect over function types anyway. I suspect this was an oversight and not an intentional choice to limit the use of the new feature to newer runtimes.

4 Likes