Questions about memory states in `Unsafe` APIs

I've revisited my Unsafe portions in my own codebase, and would like to verify a few things:

  1. Are the states (init-ed vs uninit-ed), and bindings (bound vs unbound) the properties of memory region, and not pointer? That is, the different pointers pointing to the same memory address will have the same state and binding.

  2. Say, I have ...Pointer<T>. If I rebind that region to U, the original pointer is invalidated.

    What happens if I bind the memory back to T, does it re-validate the first pointer? Is this what withMemoryRebound does?

  3. What's the bindings/states of these withUnsafe<Mutable>Bytes:

    Do they simply retain the binding of the original memory region? What about Data?

  4. If the memory pointed by Data.withUnsafeBytes is bound, how do I reinterpret it as my own struct? Do I just copy-out-copy-in and hope that optimiser understand me?

1 Like

Uh...both? Sort of?

For initialised/uninitialised, this is a property of the memory region. Two pointers pointing to the same region must not be deinitialised separately. This is easiest to see with pointers holding Swift classes, where deinitialisation decrements the reference count: naturally, you don't want to do this twice for the same buffer!

Binding is trickier. Strictly, binding is a property of the region, but in practice you cannot bind a region, so you must bind pointers. This is where Swift's version of C's strict aliasing rule comes into play. If you have bound a memory region to a type through any pointer, you must not access that memory region through a pointer bound to a different type at the same time. That is, the following is unacceptable:

let x: UnsafeRawBufferPointer = ???
let y = x.bindMemory(to: UInt8.self)
let z = x.bindMemory(to: UInt32.self)

Swift is entitled to assume (and indeed does assume) that y does not alias z, meaning that it may assume that any mutations that occur through y cannot affect reads of z, and vice-versa. This rule is why you must never use assumingMemoryBound unless you absolutely know you're right about what the binding actually is for this memory. Generally you only know that if you allocated the pointer.

It does, yes. Throughout the duration of withMemoryRebound you must not access that memory region through any of the old pointers.

These pointers are bound to Element and are all initialised to objects of type Element.

The binding of pointers from Data is extremely tricky because in practice it may contain pointers from basically anywhere due to the vagaries of Objective-C and bridging. NIO treats the pointers from Data as essentially radioactive: we only access them using the Raw pointer types. Basically the answer to this is "unknown". A well-implemented Data will always hand you initialized memory, however.

These are bound to the type of value, and are initialised.

Their bindings are carried in their types: if you didn't create the pointer, you must assume that the whomever handed you the pointer did not violate the aliasing rules. Initialization is not guaranteed.

Both binding and initialisation are unknown. Do not bind these pointers unless you are 100% confident of what the correct binding is.

If your struct is a plain data object, the easiest thing to do is to allocate a pointer to it and then use memcpy (or UnsafeMutableRawPointer.copyMemory) to do a bitwise-copy of the data into the appropriately typed pointer. If you know that the memory is aligned you can use UnsafeRawPointer.load to load the value directly.

If your struct can be nicely initialised to a temporary zero value, you can avoid the allocation of a new pointer and just stack-allocate the zero value, and then use withUnsafeBytes to copy directly onto the stack.


Thank you very much for the detailed explaination!

Do you mean on the C side? Cause I don’t think I can do that in Swift.

I imagine he means something like Core Graphics structs, such as CGRect or CGSize.

But how do I force stack-allocate them though, I don’t think even local var is guaranteed to do that (now that I think about it, maybe it’s best to let the compiler decide).

Correct, let the compiler decide. In practice a local var that is a trivial type will essentially always be stack allocated. It may then be copied to a heap allocation if it escapes, of course.