related to the discussion here, i was wondering more generally: what does it mean to unsafeBitCast between two function types? are there cases in which doing so is ever 'actually safe' (and others where it never is)? if so, what are they?
my intuition is that any cast that adds/removes arguments or changes a function's parameters or return type in incompatible ways would be suspect. but what about doing things like adding/removing certain parameter attributes?
here is a compiler explorer example with some experiments i've tried (source also reproduced below).
source code
class A {
var value: Int = 0
}
class B: A {}
func ubc() {
typealias FnOne = (Int) -> Void
let intFn: FnOne = { i in print("i: \(i)") }
intFn(42) // i: 42
do {
typealias NewFn = () -> Void
let newFn: NewFn = unsafeBitCast(intFn, to: NewFn.self)
newFn() // i: 72057602407836425
}
do {
typealias NewFn = (String) -> Void
let newFn: NewFn = unsafeBitCast(intFn, to: NewFn.self)
newFn("hello") // i: 478560413032
}
typealias FnStr = (String) -> Void
let strFn: FnStr = { s in print("s: \(s)") }
strFn("hello world") // hello world
do {
typealias NewFn = (Int) -> Void
let newFn: NewFn = unsafeBitCast(strFn, to: NewFn.self)
//newFn(42) // 💥 Fatal error: UnsafeBufferPointer has a nil start and nonzero count
}
do {
typealias NewFn = () -> Void
let twoStr: NewFn = unsafeBitCast(strFn, to: NewFn.self)
// twoStr() // 💥
}
typealias ClassFn = (A) -> Void
let classFn: ClassFn = { a in print("a: \(String(describing: a))") }
classFn(A()) // a: A
do {
typealias NewFn = () -> Void
let newFn = unsafeBitCast(classFn, to: NewFn.self)
// newFn() // 💥
}
do {
typealias NewFn = (B) -> Void
let newFn: NewFn = unsafeBitCast(classFn, to: NewFn.self)
newFn(B()) // a: B
}
typealias IsoAnyFn = @isolated(any) (Int) -> Void
let isoAnyFn: IsoAnyFn = { @MainActor int in
MainActor.preconditionIsolated()
print("main actor: \(int)")
}
do {
typealias NewFn = (Int) -> Void
let newFn: NewFn = unsafeBitCast(isoAnyFn, to: NewFn.self)
newFn(42) // main actor: 42
}
}
ubc()