Recently, I found yet another way to bypass Sendable check. The trick relies on the following behavior:
protocol A {
func take<T>(_ t: T)
}
struct B: A {
func take<T: Sendable>(_ t: T) { }
}
As shown above, when implementing a generic protocol requirement, the conformer can strengthen the generic constraint by adding T: Sendable. Normally, this is illegal if we just add a random protocol constraint, because theoretically it does not satisfy the Liskov substitution principle.
I'm not sure why Sendable is special, and I don't know if this is a bug or is by design. I certainly can come up with some situation where this can be helpful, for example when the protocol we want to conform to comes from a pre-concurrency library.
Based on this behavior, we can transform any non-Sendable value to be sendable:
protocol A {
func asSendable<T>(_ t: T) -> any Sendable
}
struct B: A {
func asSendable<T: Sendable>(_ t: T) -> any Sendable { t }
}
func foo<T>(_ input: T) {
let transformed = (B() as A).asSendable(input)
Task {
_ = transformed
}
}