Why does copyMemory from ContiguousArray give different output than Array?

I'm trying to learn how unsafe memory works in Swift but confused by the following code. The code below works fine when data is an Array<Float> but prints garbage when data is declared a ContiguousArray<Float>. Why is this?

// When this is a ContiguousArray<Float> code prints garbage, but when it's simply an Array<Float> code works fine.
var data: ContiguousArray<Float> = [0.1,0.2,0.3] 
    
    func test() {
        let unsafeMutableRawPointer = UnsafeMutableRawPointer.allocate(byteCount: data.count*MemoryLayout<Float>.stride, alignment: MemoryLayout<Float>.alignment)
        unsafeMutableRawPointer.copyMemory(from: &data, byteCount: data.count * MemoryLayout<Float>.stride)
        
        let p = unsafeMutableRawPointer.bindMemory(to: Float.self, capacity: data.count)
        print(p.pointee) // Prints 0.1 when data is Array but garbage when data is ContiguousArray
        print(p.advanced(by: 1).pointee) // Prints 0.2 when data is Array but garbage when data is ContiguousArray
        print(p.advanced(by: 2).pointee) // Prints 0.3 when data is Array but garbage when data is ContiguousArray
    }

test()

I don’t know the answer, but you have the wrong value for capacity here:

The documentation says:

This is how it looks inside (use this information at your own risk!)

Array:

cdcccc3d cdcc4c3e 9a99993e

these are your three numbers (in little endian form)

ContiguousArray is more complicated, it contains a pointer inside (double indirection). If you dump enough bytes at the address that the pointer points to - you'd see your numbers at some offset:

a05e360d 01000000 03000000 00000000
03000000 00000000 06000000 00000000
cdcccc3d cdcc4c3e 9a99993e

I bet "3"s in there are for count and capacity.


A side note: you can pass &floats to a function that accepts UnsafePointer<Float> when floats are of type Array<Float>. But you can't do this when the type is ContiguousArray<Float>.

PS. worth mentioning that Array may also store instances of NSArray.

Oh oops I actually made that mistake when copying the code to a smaller test case, the problem still exists though even with that corrected.

That's interesting, I guess I just assumed (naively) that it would copy only the contiguous bytes of just the data in the array and not the extra stuff. It's funny though how array works fine, I would assume that arrays representation would be more complicated since it's not even guaranteed to be contiguous.

Regardless none of what I'm doing seems safe since I have no idea if at any point these representations could change.

Can you point me to more documentation about that side note? I find it strange that it doesn't work for ContiguousArray.

The bigger picture is that I was originally working on a project involving the graphics API Metal and I needed to copy data from an array into a UnsafeMutableRawPointer exposed by Metal. One of my arrays happened to be a ContiguousArray instead of a regular Array which led to this discovery. I still don't fully understand how safe it is to even use Array in this way since what if, for example, they end up changing the internal structure to be like ContiguousArray. I suppose maybe there is some documentation somewhere that mentions this. Maybe I need to roll my own solution with say UnsafeBufferPointer and never copy raw memory from Array/ContiguousArray to be safe.

Don't have a doc for that, found that by trial and error.

Array stores content continuously (except when it doesn't but that's not your case):

link1

link2

The method copyMemory(from:byteCount:) takes an UnsafeRawPointer as the first argument.

By using inout syntax, you're taking advantage of Swift's implicit bridging. This allows you to read the raw memory from an instance of any type.

Uniquely for Array, however, a temporary pointer to the elements of the array is created implicitly; there is no such magic for ContiguousArray. Your code relies on this magic implicit behavior to work as you'd expect, which is why it doesn't do what you want when you use ContiguousArray values.

In order to copy from the elements of a ContiguousArray, you should use withUnsafeBytes so that you have a temporary pointer to the elements rather than the container:

let unsafeMutableRawPointer = /* ... */
data.withUnsafeBytes {
  unsafeMutableRawPointer.copyMemory(from: $0.baseAddress!, byteCount: /* ... */)
}
let p = /* etc. */

You can, of course, use the same code when data is a plain Array, instead of relying on the special-cased implicit behavior.


@tera's answer is not quite correct as to how an Array value "looks on the inside" as compared to a ContiguousArray value, and you do not have to rely on knowledge of the internal representations in order to do what you want to do correctly. You simply need to be aware of when Swift is implicitly doing some of the work for you and how to achieve the same thing with public APIs.

For more on the use of UnsafeRawPointer's implicit casting and bridging behavior, see the documentation of that type.

13 Likes

Is there a way to opt-out of that implicit bridging? I'd like to see the raw bytes of Array (similar to how I can do this for ContinuousArray). For Educational/Debugging purposes only.

Aha, unsafeBitCast to the rescue :smiley:

Raw Array<Float> dump:

a06e1a01 01000000 03000000 00000000 
03000000 00000000 06000000 00000000 
cdcccc3d cdcc4c3e 9a99993e

This behavior is implicit to passing an inout value of type Array where an argument of type UnsafeRawPointer is expected. You don't get to opt out of this behavior if all three conditions apply, but you also don't get this behavior if these three conditions don't all apply.

Reading the source code would probably be a simpler way of figuring things out, but you can get your unsafe raw (buffer) pointer by calling withUnsafeBytes(of: data) if you'd prefer.

No, don't do that.

2 Likes

Thank you for the brilliant explanation, the "magic" bridging that is used for Arrays is what was naively expecting me to be able to just copy raw memory. I now see I must rely on the underling storage pointers using withUnsafeBytes and the like. I still find it strange that this behavior is implicit on the Array type but not the ContiguousArray, which ultimately led to some nasty bugs in my project. I suppose this implicit behavior was added to make it easier when working with C. All very interesting though, thanks again to you and @tera for assisting me with this issue.

1 Like

Thanks to @xwu we have a few takeaways here:

  • & is magical compared to C
  • and it is even more magical in special cases of Array and String (possibly others?)
  • "raw" may not mean what you think it does.
  • similar named floats.withUnsafeBytes and withUnsafeBytes(of: floats) give different results.

Learn something new every day.

1 Like