When can one (safely) `unsafeBitCast` between function types?

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()
1 Like

That's what I love Swift forums for! Thanks @jamieQ for running an extra mile with this follow-up on the discussion :slight_smile:

2 Likes

Not sure if this is what you intended to demonstrate, but this does not assert because top level code is implicitly main actor isolated. If you‘d change ubc to be async it will assert.

So you removed @isolated(any) unsafely and it made it unsafe.

that's a good point, and that was perhaps not the best choice of example. but it is sort of the essence of the original discussion question – can you 'cast off' the @isolated(any) attribute from a synchronous, @isolated(any) function, by effectively taking over responsibility for the actor executor check yourself? this example shows it's at least a plausible thing to do (at least for global actor-isolated functions), but it's unclear to me if it's 'in practice safe'[1], or its appearing to 'work' is purely incidental.


  1. assuming you insert the preconditions yourself, or somehow only invoke it when known to be on the correct actor ↩︎

2 Likes