Does UnsafePointer create a weak reference to the referenced object?

Hi. Does a pointer object create a strong reference to an object for ARC to retain it, or is it a (unsafe) weak reference? I suspect it is the latter, but please confirm.

Also, does UnsafeMutablePointer.initialize(to:) remove an object from ARC and makes it somewhat 'unmanaged'? Or does it only create a pointer to an object, and the said object is still managed by ARC?

(i.e. the object will still be deallocated as soon as it is no longer referenced by anything, but the pointer will persist independently while pointing to the deallocated memory. Until ARC deallocates the pointer too, of course.)

Thanks.

Does a pointer object create a strong reference to an object
for ARC to retain it, or is it a (unsafe) weak reference?

Neither. That unsafe really means what it says.

The fact that you mention “object” suggests that you’re missing some key background about unsafe pointers. You can’t actually get an unsafe pointer to an object. Consider this:

class MyObject { }

func main() {
    let o = MyObject()
    print(ObjectIdentifier(o))
    withUnsafePointer(to: o) { oPtr in
        print(oPtr)
    }
}

main()

It prints:

ObjectIdentifier(0x00006000003b80c0)
0x000000016fdff2d0

Note how the two values aren’t the same. The first is the actual address of the object itself. The second is the address of the reference to the object. If you set a breakpoint after the second print(…) you’ll see this:

ObjectIdentifier(0x00006000003b80c0)
0x000000016fdff2d0
(lldb) mem read -f x -s 8 -c 1 0x000000016fdff2d0
0x16fdff2d0: 0x00006000003b80c0

If you want to manipulate objects unsafely, you might be looking for Unmanaged.


The other situation that might be leading you astray is interior pointers. Consider this:

class MyObject {
    var i: Int = 42
}

func main() {
    let o = MyObject()
    print(ObjectIdentifier(o))
    withUnsafePointer(to: o.i) { iPtr in
        … do something with `iPtr` …
    }
}

main()

There are two issues here:

  • iPtr is not valid outside of the scope of the closure being called by withUnsafePointer(…).

  • iPtr may not actually point to the bytes where i is stored within o.

I talk about that second point in some detail in .The Peril of the Ampersand.


If I’ve missed the point here, please post more details about the background to your question.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

9 Likes

OK, I understand.

Let's have a concrete example, then.

I have an Array<T?> where T: AnyObject. I keep objects in here to prevent them from being deallocated, as I need to pass them to a C function as pointers for further processing. Once the processing is done, the C code will call a provided callback where I can deallocate the object and its pointer.

Unfortunately, I used a rather unsafe function to get the pointer: return withUnsafeMutablePointer(to: &items[id]!) { $0 } (id is checked in advance to be a valid one). It works for as long as the backing array does not resize. As soon as it does, the pointers become invalid.

I am considering storing the UnsafeMutablePointer<T>? instead in the array. This will ensure that pointers will not change during the array resizing. However, I am now looking for a way to ensure that the referenced object will remain for as long as the pointer exists (until it is manually deallocated).

So, I was reading up on what is available, and found UnsafeMutablePointer.initialize(to:). Would that be what I need, or will it just create a pointer to the consumed object without retaining it?

Would you have any suggestions? Thanks a lot.

(Also, if I used some terms incorrectly from the Swift perspective, please feel free to correct me. I will watch the presentations from your post later today.)

If you need to pass a Swift class instance to C functions (commonly when used as opaque "context" objects), the canonical way to do this is the following:

let obj = MyClass()
let unmObj = Unmanaged<MyClass>.passRetained(obj)
// toOpaque() returns a C void pointer, essentially
my_c_func(unmObj.toOpaque(), callback: { ptr in 
    let sameObject = Unmanaged<MyClass>.fromOpaque(ptr)
    sameObject.release()
})

Note that the unmanaged instance is created as retained, and there's a balancing release after the function completes. This should be the default in the majority of cases[1], i.e. you should not rely on storing an object in an array (or even on the stack!) to keep it alive — having this guarantee locally is much stronger and helps with local reasoning.


  1. Note that the semantics may be different when interfacing with Core Foundation APIs, as those do participate in reference counting: see e.g. this example. ↩︎

6 Likes

Can you explain the difference between OpaquePointers and UnsafeRawPointer? The docs say that the former is intended for pointers to the types that cannot be represented in swift.

Edit: Oops. My bad. toOpaque actually returns UnsafeMutableRawPointer.

Also, here is the link to what I was making: GitHub - RussBaz/mini-alloc: Small container for retaining Swift object references - aka mini allocator

If anyone is interested, please have a look and confirm if it is generally safe. (For as long as you never reuse the pointer after a release method is called)

As @eskimo has alluded to, you might be mistaken about what UnsafeMutablePointer<T> represents when T is a reference type (read "class").

When you have the following:

class MyClass { }

let instance = MyClass()

the value of instance is a reference (a non-nil pointer) to the storage of MyClass allocated by Swift somewhere on the heap. Whenever you form an UnsafeMutablePointer<MyClass>, you're getting a pointer to a pointer, not a pointer to the storage. @eskimo have provided a test above to confirm this via ObjectIdentifier; another way to see this is to consider differently sized classes:

// stores 16 bytes
class MySmallClass {
    var one: Int = 0
    var two: Int = 0
}

// stores 32 bytes
class MyLargeClass {
    var one: Int = 0
    var two: Int = 0
    var three: Int = 0
    var four: Int = 0
}

print(MemoryLayout<MySmallClass>.size) // 8
print(MemoryLayout<MyLargeClass>.size) // 8

— both statements print "8" on 64-bit platforms. This is because the value stored in variables of type MySmallClass or MyLargeClass or any other class is a pointer to the data. So if you take an UnsafeMutablePointer to it, you're getting a pointer to a pointer. If you're familiar with Rust, any instance of a Swift class is a Box<T>, not T itself.

Again, the canonical way to get a pointer to the actual block of memory on the heap is through Unmanaged<MyClass>.toOpaque(). However, even then you mustn't be assuming that the contents of that memory will follow the C layout logic: a Swift class stores type metadata in its first two fields (on Apple platforms):

class MySmallClass {
    var one: Int = 12345
    var two: Int = 0
}

let smol = MySmallClass()
let unmanaged = Unmanaged<MySmallClass>.passRetained(smol)
let opaquePtr = unmanaged.toOpaque()
let typed = opaquePtr.load(as: Int.self)

print(typed) // would expect to print "12345", but prints the address of class metadata table instead

let advanced = opaquePtr.advanced(by: 16).load(as: Int.self) // skip two words
print(advanced) // prints the actual "12345"

please have a look and confirm if it is generally safe

TL;DR: This is unfortunately absolutely unsafe :sweat_smile:. You're manipulating an array of pointers to pointers that can start to dangle almost immediately.

5 Likes

Sorry for just referencing this bit but why would they? Is initialize method really just copies the object address? How is it not failing then? The allocated space should not match or the compiler doesn’t care about it at all? This feels strange after all that safety involved with every aspect of pointers so far. Lastly, how come the tests do not fail?

Ok, how about just using an array of results of passRetained? Will that work?

Sorry for just referencing this bit but why would they?

You're taking a pointer to a pointer, but the inner pointer doesn't know anything about the fact that it has been (unsafely) referenced elsewhere: at some point the runtime is free to run the deinitializer and dealloc the class storage, since for what it cares the reference count hasn't been incremented.

This feels strange after all that safety involved with every aspect of pointers so far.

As long as you're not using UnsafeXYZ constructs :slight_smile:

Lastly, how come the tests do not fail?

Perhaps because the memory hasn't been reused (AFAIK the semantics there are still somewhat undefined).

Ok, how about just using an array of results of passRetained? Will that work?

Potentially — I'm not sure what you specifically have in mind. But the point of ARC is that you don't have to do any of this manual labour — if you just want to keep objects alive, you simply store them in an array:

class MyCache<T: AnyObject> {
    var storage: [T] = []
    
    func retain(_ object: T) {
        storage.append(object)
    }
}

but to me personally this too looks a bit off; you'd typically just pass these objects to whatever other places of your code that need them, and they will be retained automatically. The only use case I'd imagine for such a technique is some object pooling where the initalization is really expensive and is something you actively want to avoid.


Edit: explanation on the runtime behaviour potentially incorrect, I've missed the UnsafeMutablePointer<T>.initialize call and what it does — see below.

What your code is doing is allocating an 8-byte chunk of memory (UnsafeMutablePointer<T>.allocate) that contains a strong reference to the class instance. Then, later, you deinitialize and deallocate the pointer which results in the strong reference being removed and the reference count being decremented. This is basically what you can do with Unamanged but with extra steps (and an extra allocation). But +1 to what @nkbelov said about just storing the references in the array directly (instead of using pointers or Unmanaged yourself).

4 Likes

Oh, I guess it indeed could actually be retaining it, since UnsafeMutablePointer<T>.initialize should know about the semantics. That might explain it better, my bad if that's the case.

I’m afraid it is not so simple. The C library I work with runs its own event loop. And the only way to pass my own data to the callback that is executed on the event loop after a certain operation is complete, is to pass a void pointer to it when scheduling the operation. Only this void pointer will be available inside the callback.

Is this roughly your setup?

void c_call(void* parameter) {
	rememberedParameter = parameter;
}
void c_call_at_some_later_point() {
	swift_call(rememberedParameter);
}
class C {}
func swift_call(_ c: C) { ... }
var c: C? = C()
let something = ..... based on c
c_call(something)
c = nil
...
c_call_at_some_later_point()

What should happen if you call c_call_at_some_later_point() more than once? Or less than once?

Very roughly but yes.

The C event loops guarantees that once it accepts my operation, it will always run the callback once (and only once - no less and no more than that, without a guarantee when that will happen). The docs for the C library also say that the callback is where one should normally free the memory referenced by the void pointer.

So, if it is called more than once or not at all, then it is a serious bug in the C code (which I do not own, btw) and I would prefer it resulting in a crash as there is almost no way to recover from that. The only other possibility is that the C library allows for cancellation of scheduled but not yet run operations.

More accurate description of how it is scheduled: I create a custom class and initialise a C provided structure associated with an operation I want to perform inside that class. This structure has a mutable void pointer field. I assign a pointer to the containing custom class to that field. Then I pass a pointer to the structure and a function pointer to the callback to the event loop to schedule the operation on it. If this function call results in a success, then it guarantees that it will eventually call the callback and pass the processed structure into it. Otherwise, I am free to free the memory right away.

I am also allowed to request a cancellation of the scheduled operation, but I need to provide a pointer to the original structure and a pointer to a cancellation callback (where it will be safe to free my own memory). It can only be done if the operation has not yet run. It is not a big problem because the event loop is synchronous and single threaded, and all the scheduling tasks must happen on the same thread as the event loop itself. (I simplified the event loop slightly as it is a very complex beast)

You’d need to call CFBridgingRetain and CFBridgingRelease in C, those convert between id and void* adjusting the ref count accordingly. The relevant class in Swift will have to be NSObject based, IIRC.

That’s exactly what Unmanaged is for! You’d do Unmanaged.passRetained(foo).toOpaque() to create a void * while performing an extra retain to keep the object alive. Then you’d call Unmanaged<Foo>.fromOpaque(ptr).takeRetainedValue() to consume the extra retain you did earlier (so the object doesn’t leak) and get the object you referenced back.

6 Likes

Yep, I like this better than CFBridgingRetain/CFBridgingRelease approach as it doesn't require the class being based on NSObject.

Expanding on using Unmanaged, you can write a wrapper that bundles up a Swift closure as your context object and executes it on the event loop, which will let you use normal closure capture rules with the C API:

extension EventLoop {
    func addTask(task: @escaping () -> Void) {
        class Context {
            let task: () -> Void
            
            init(task: @escaping () -> Void) {
                self.task = task
            }
        }
        
        let context = Context(task: task)
        
        EventLoopAddTask(self, Unmanaged.passRetained(context).toOpaque()) { ptr in
            let local = Unmanaged<Context>.fromOpaque(ptr!).takeRetainedValue()
            local.task()
        }
    }
}

You can use it with normal captures like this:

let name = "Jayson"
eventLoop.addTask {
    print("ran task with \(name) via Swift closure")
}

I uploaded a demo here:

(Don't judge the EventLoop implementation, I just wanted to quickly throw together a representative example)

2 Likes