Search code examples
swiftpointers

Difference between Unmanaged and withUnsafePointer APIs?


Below is the sample code I use:

class Dummy {
    let uuid = UUID()
}

func test() {
    
    let dummy = Dummy()
    let unmangedOpaquePointer = Unmanaged.passUnretained(dummy).toOpaque()
    let fromWithUnsafeAPIPointer = withUnsafePointer(to: dummy, { UnsafeMutableRawPointer(mutating: $0) })
    print(unmangedOpaquePointer == fromWithUnsafeAPIPointer) // false
    
    let dummy1 = Unmanaged<Dummy>.fromOpaque(unmangedOpaquePointer).takeUnretainedValue()
    let dummy2 = fromWithUnsafeAPIPointer.assumingMemoryBound(to: Dummy.self).pointee
    let dummy3 = Unmanaged<Dummy>.fromOpaque(fromWithUnsafeAPIPointer).takeUnretainedValue()
    let dummy4 = unmangedOpaquePointer.assumingMemoryBound(to: Dummy.self).pointee
    
    print(dummy1 === dummy2) // true
    print(dummy1 === dummy3) // EXC_BAD_ACCESS
    print(dummy2 === dummy4) // EXC_BAD_ACCESS
}

test()
  • different UnsafeMutableRawPointer instances
print(unmangedOpaquePointer == fromWithUnsafeAPIPointer) // false

The result of the above code snippet is "false". It's kind of expected. unmangedOpaquePointer and fromWithUnsafeAPIPointer are different instances of UnsafeMutableRawPointer, so they are not equal. However, they both point to the same object dummy here. How can I test this piece of fact, if unmangedOpaquePointer == fromWithUnsafeAPIPointer is not the right way to achieve it?

  • API calls should be paired? Why? What's the difference between the UnsafeMutableRawPointer returned from Unmanaged APIs and the withUnsafePointer APIs?
print(dummy1 === dummy2) // true

The print call says dummy1 and dummy2 retrieved from the two pointers are identical (the same object instance). However dummy1 === dummy3 and dummy2 === dummy4 are not runtime valid expressions (cause they both crash). So it seems the pointer returned by calling an Unmanaged API should only be used with an Unmanaged API to retrieve the value that the pointer points to. The same goes for withUnsafePointer APIs. Why?

With the help of @Sweeper, I tested another version of the sample code:

class Dummy {
    let uuid = UUID()
}

func test() {

    var dummy = Dummy()
    let unmangedOpaquePointer = Unmanaged.passUnretained(dummy).toOpaque()
    withUnsafeMutablePointer(to: &dummy) {
        let fromWithUnsafeAPIPointer = UnsafeMutableRawPointer($0)
        print(unmangedOpaquePointer == fromWithUnsafeAPIPointer) // false
        
        let dummy1 = Unmanaged<Dummy>.fromOpaque(unmangedOpaquePointer).takeUnretainedValue()
        let dummy2 = fromWithUnsafeAPIPointer.assumingMemoryBound(to: Dummy.self).pointee
        let dummy3 = Unmanaged<Dummy>.fromOpaque(fromWithUnsafeAPIPointer).takeUnretainedValue()
        let dummy4 = unmangedOpaquePointer.assumingMemoryBound(to: Dummy.self).pointee

        print(dummy1 === dummy2) // true
        print(dummy1 === dummy3) // EXC_BAD_ACCESS
        print(dummy2 === dummy4) // EXC_BAD_ACCESS
    }
}

test()

This corrects the wrong way to use withUnsafePointer APIs. But the result is the same.


Solution

  • Dummy is a reference type. When you do var dummy = Dummy(), the actual UUID in the class instance is stored somewhere else in memory (the heap), not in the dummy variable, which is on the stack. dummy only stores a reference to the class instance. As an analogy, the Swift type Dummy would be similar to Dummy * in C, where Dummy is a C struct. Note that you cannot express the C struct Dummy in Swift.

    The pointer you get with withUnsafeMutablePointer is of type UnsafeMutablePointer<Dummy>. As we have established, Dummy itself is just "a reference to a class instance on the heap", so a UnsafeMutablePointer<Dummy> is a pointer to "a reference to a class instance on the heap". In C terms, this would be like a Dummy **.

    The raw pointer you get with Unmanaged.toOpaque however, is just a pointer to the class instance - the same kind of thing that the dummy variable stores. This is the fundamental difference.

    This explains why the two raw pointers are not equal. They point to very different things.

    Using fromOpaque and takeUnretainedValue effectively converts the raw pointer to a Dummy. These are different Swift types, but they are really the same thing - a reference to the class instance. In the C analogy, this would look like casting a void * to Dummy *. In dummy3, you are trying to cast a Dummy ** to a Dummy *, which obviously goes very badly. Crucially, there is no dereferencing.

    Using pointee, however, is dereferencing. Again, using a C notation, you can dereference a Dummy ** to get the Dummy * that it is pointing to (like you do in dummy2). If you dereference a Dummy * like in dummy4, the value you get is the Dummy struct (not expressible in Swift), not Dummy * (Dummy in Swift).

    To summarise this in a table:

    Swift Analogous to C
    Dummy Dummy *
    UnsafeMutablePointer<Dummy> Dummy **
    UnsafeMutableRawPointer void *
    fromOpaque then takeUnretained Casting to Dummy *
    assumingMemoryBound(to: Dummy.self).pointee Casting to Dummy ** then dereference

    Here is some analogous C pseudocode to illustrate what's wrong with dummy3 and dummy4.

    Dummy *dummy = ...;
    
    // dummy is a Dummy *, a pointer pointing to a Dummy
    void *unmangedOpaquePointer = dummy;
    
    // &dummy is a Dummy **, a pointer pointing to a Dummy *
    void *fromWithUnsafeAPIPointer = &dummy; 
    
    // OK, unmangedOpaquePointer is indeed pointing to a Dummy
    Dummy *dummy1 = (Dummy *)unmangedOpaquePointer;
    
    // OK, fromWithUnsafeAPIPointer is a Dummy **, pointing to a Dummy* and we dereference get the Dummy * it is pointing to
    Dummy *dummy2 = *(Dummy **)fromWithUnsafeAPIPointer;
    
    // BAD! fromWithUnsafeAPIPointer is not a Dummy *
    Dummy *dummy3 = (Dummy *)fromWithUnsafeAPIPointer;
    
    // BAD! unmangedOpaquePointer is not a Dummy **
    Dummy *dummy4 = *(Dummy **)unmangedOpaquePointer;