An unexpected `Sendable` pitfall

I just want to share a pitfall related to Sendable: currently any object can bypass Sendable checking if you want (without using any unsafe language features of course).

Normally, it is impossible to freely send non-Sendables to another isolation domain:

class A {}

func foo(_ a: A) {
    Task { _ = a }   // error
}

However, we can rely on the fact that Sendable is a marker protocol, and it does not have any runtime effects.

func runtimeCast<T, U>(_ t: T, _ u: U.Type) -> U? {
    t as? U
}

func transformed(_ a: A) -> (any Sendable)? {
    runtimeCast(a, (any Sendable).self)   // always successfully
}

func bar(_ a: A) {
    guard let newA = transformed(a) else { return }
    Task { _ = newA }   // this line is now possible, because newA is Sendable...
}

I learned this trick after reading this discussion of Sendable casting checks related to isolated conformance.

3 Likes

And to make things even more confusing, function types with and without @Sendable are differentiated in the runtime.

Which in turn means that value of (@MainActor () -> Void).self will differ depending on if code was compiled with SE-0434 enabled or disabled. And if you mix code compiled with different compiler settings you can get very surprising runtime failures.

1 Like

why exactly does the runtime cast to Sendable succeed? do such casts work because the runtime has no knowledge of whether a type is or is not Sendable[1]?

per this comment from the referenced thread:

is this 'just' something that remains to be implemented?


  1. and presumably the casts should either always succeed or always fail in a deterministic manner if the true 'sendability' of the type cannot be recovered â†Šī¸Ž

IIUC, there are no plans to implement this.

Down the thread @Douglas_Gregor mentions that he was thinking of a limited solution that wouldn't actually work in general case:

2 Likes