Questions about raw and typed pointers and generic

I wrote some functions for Swift pointers for learning and couldn’t understand the results I am getting.
The code: Questions about raw and typed pointers and generic - #3 by Luminaron
My experiments revolve around the following array:

var myIntArray = Array<Int>(repeating: 10, count: 10)

The first function gets a raw pointer, prints it and possibly does something with it:

func mutableRawPointer(to pointer: UnsafeMutableRawPointer,
                       _ body: (UnsafeMutableRawPointer) -> Void)
{
    print("Acquired raw pointer: \(pointer)")
    body(pointer)
    print("Raw pointer END\n")
}

mutableRawPointer(to: &myIntArray){_ in}

Output:

Acquired raw pointer: 0x000060000390c020 // Raw Pointer Address
Raw pointer END

The second one gets a typed pointer and everything else is the same:

func arrayIntPointer(to pointer: UnsafeMutablePointer<Array<Int>>,
                     _ body: (UnsafeMutablePointer<Array<Int>>) -> Void)
{
    print("Acquired Array<Int> typed pointer: \(pointer)")
    body(pointer)
    print("Array Int pointer END\n")
}

arrayIntPointer(to: &myIntArray){_ in}

Output:

Acquired Array<Int> typed pointer: 0x0000000100008038 // Typed Pointer Address
Array Int pointer END

The addresses are different!

This just confuses me. However, before I can think of a reason why. The third function brings more questions to the table. Instead of taking a specific type, the third function takes a generic type:

func genericTypedPointer<T>(to pointer: UnsafeMutablePointer<T>,
                            _ body: (UnsafeMutablePointer<T>) -> Void)
{
    print(“Acquired generic typed pointer: \(pointer)")
    body(pointer)
    print("Typed generic pointer END\n")
}

genericTypedPointer(to: &myIntArray){_ in}

Output:

Acquired generic typed pointer: 0x000060000390c020 // Raw Pointer Address
Typed generic pointer END

So, I thought to myself that the issue might stem from type inference, and thus I wrote a variant of the third function with a "forced type":

func forcedTypedPointer<FT>(_ _: FT,
                            to pointer: UnsafeMutablePointer<FT>,
                            _ body: (UnsafeMutablePointer<FT>) -> Void)
{
    print("Acquired forced generic pointer: \(pointer)")
    body(pointer)
    print("Forced generic pointer END\n")
}

forcedTypedPointer(myIntArray, to: &myIntArray){_ in}

Output:

Acquired forced generic pointer: 0x0000000100008038 // Typed Pointer Address
Forced generic pointer END

Despite these experiments, I'm left with several questions:

  • Why do the memory addresses differ between typed and raw pointers for the same variable?
  • Why does the generic typed pointer function behave similarly to a raw pointer function?
  • In the third function, what is the type T if it's not inferred as Array<Int>, and why is it inferred in such a manner?
1 Like

Forgot to add the callers:

mutableRawPointer(to: &myIntArray){_ in}
arrayIntPointer(to: &myIntArray){_ in}
genericTypedPointer(to: &myIntArray){_ in}
forcedTypedPointer(myIntArray, to: &myIntArray){_ in}

The whole thing looks like this:

var myIntArray = Array<Int>(repeating: 10, count: 10)

func mutableRawPointer(to pointer: UnsafeMutableRawPointer,
                       _ body: (UnsafeMutableRawPointer) -> Void)
{
    print("Acquired raw pointer: \(pointer)")
    body(pointer)
    print("Raw pointer END\n")
}

func arrayIntPointer(to pointer: UnsafeMutablePointer<Array<Int>>,
                     _ body: (UnsafeMutablePointer<Array<Int>>) -> Void)
{
    print("Accquired Array<Int> typed pointer: \(pointer)")
    body(pointer)
    print("Array Int pointer END\n")
}

func genericTypedPointer<T>(to pointer: UnsafeMutablePointer<T>,
                            _ body: (UnsafeMutablePointer<T>) -> Void)
{
    print("Accquired generic typed pointer: \(pointer)")
    body(pointer)
    print("Typed generic pointer END\n")
}

func forcedTypedPointer<FT>(_ _: FT,
                            to pointer: UnsafeMutablePointer<FT>,
                            _ body: (UnsafeMutablePointer<FT>) -> Void)
{
    print("Acquired forced generic pointer: \(pointer)")
    body(pointer)
    print("Forced generic pointer END\n")
}

mutableRawPointer(to: &myIntArray){_ in}
arrayIntPointer(to: &myIntArray){_ in}
genericTypedPointer(to: &myIntArray){_ in}
forcedTypedPointer(myIntArray, to: &myIntArray){_ in}
1 Like

Have a look at this refactored version (I'm using int32 elements to keep output compact):

Some infrastructure
typealias ElementType = Int32
typealias ArrayType = Array<ElementType>

let element: ElementType = 0x11223344
let headerSize = 32

func isStackMemory(_ ptr: UnsafeRawPointer) -> Bool {
    let stackBase = pthread_get_stackaddr_np(pthread_self())
    let stackSize = pthread_get_stacksize_np(pthread_self())
    return ptr <= stackBase && ptr >= stackBase - stackSize
}

enum MemoryType: String {
    case stack = "on stack", heap = "on heap", other = "other memory (global?)"
    
    init(_ ptr: UnsafeRawPointer) {
        // TODO: correct heap memory determination
        if isStackMemory(ptr) {
            self = .stack
        } else {
            self = .other
        }
    }
}

func dumpHex(_ title: String, _ ptr: UnsafeRawPointer, _ size: Int, _ headerSize: Int = 0) {
    var size = size
    var ptr = ptr
    let mallocSize = malloc_size(ptr - headerSize)
    if mallocSize != 0 {
        let mallocBlock = ptr - headerSize
        print("\(title), \(mallocBlock) + \(headerSize) = \(ptr) (\(MemoryType.heap.rawValue)), size: \(headerSize) + \(mallocSize - headerSize) = \(mallocSize) -> ")
        size = mallocSize
        ptr = mallocBlock
    } else {
        print("\(title), \(ptr) (\(MemoryType(ptr).rawValue)), showing size: \(size) -> ")
    }
    let v = ptr.assumingMemoryBound(to: UInt8.self)
    for i in 0 ..< size {
        if i != 0 && (i % 32) == 0 {
            print()
        }
        print(String(format: "%02x ", v[i]), terminator: "")
    }
    print()
}

Your code changed a bit:

func mutableRawPointer(_ p: UnsafeMutableRawPointer) {
    dumpHex("mutableRawPointer", p, 8, headerSize)
}
func arrayPointer(_ p: UnsafeMutablePointer<ArrayType>) {
    dumpHex("arrayPointer", p, 8, headerSize)
}
func genericTypedPointer<T>(_ p: UnsafeMutablePointer<T>) {
    dumpHex("genericTypedPointer", p, 8, headerSize)
}
func forcedTypedPointer<FT>(_ title: String, _ : FT, _ p: UnsafeMutablePointer<FT>, _ headerSize: Int = 0) {
    dumpHex(title, p, 8, headerSize)
}
func test() {
    var array = ArrayType(repeating: element, count: 3)
    
    dumpHex("duming array", &array, 8, headerSize)
    // duming array, 0x0000600000c102a0 + 32 = 0x0000600000c102c0 (on heap), size: 32 + 16 = 48 ->
    // 68 d4 3b f5 01 00 00 00 03 00 00 00 04 00 00 00 03 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00
    // 44 33 22 11 44 33 22 11 44 33 22 11 00 00 00 00
    
    mutableRawPointer(&array)
    // mutableRawPointer, 0x0000600000c102a0 + 32 = 0x0000600000c102c0 (on heap), size: 32 + 16 = 48 ->
    // 68 d4 3b f5 01 00 00 00 03 00 00 00 02 00 00 00 03 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00
    // 44 33 22 11 44 33 22 11 44 33 22 11 00 00 00 00

    arrayPointer(&array)
    // arrayPointer, 0x000000016fdfe940 (on stack), showing size: 8 ->
    // a0 02 c1 00 00 60 00 00

    genericTypedPointer(&array)
    // genericTypedPointer, 0x0000600000c102a0 + 32 = 0x0000600000c102c0 (on heap), size: 32 + 16 = 48 ->
    // 68 d4 3b f5 01 00 00 00 03 00 00 00 02 00 00 00 03 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00
    // 44 33 22 11 44 33 22 11 44 33 22 11 00 00 00 00

    forcedTypedPointer("forcedTypedPointer element", ElementType(), &array, headerSize)
    // forcedTypedPointer element, 0x0000600000c102a0 + 32 = 0x0000600000c102c0 (on heap), size: 32 + 16 = 48 ->
    // 68 d4 3b f5 01 00 00 00 03 00 00 00 02 00 00 00 03 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00
    // 44 33 22 11 44 33 22 11 44 33 22 11 00 00 00 00

    forcedTypedPointer("forcedTypedPointer array", array, &array)
    // forcedTypedPointer array, 0x000000016fdfe940 (on stack), showing size: 8 ->
    // a0 02 c1 00 00 60 00 00
}
test()

This drilles into the memory and shows the whole memory block (if available) along with some details (whether it's on heap or stack, etc).

Note how in some cases you are viewing the array contents, while in other cases you are viewing the memory corresponding to the Array struct (8 bytes holding the pointer to array block). The memory is layout out in little endian form, so this:

// a0 02 c1 00 00 60 00 00

treated as 0000600000c102a0 which matches the memory address of array block above.

 68 d4 3b f5 01 00 00 00 03 00 00 00 02 00 00 00 03 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 
|-ISA-------------------|-REF-COUNTS------------|-CURRENT-SIZE----------|-CAPACITY?-------------|
 44 33 22 11 44 33 22 11 44 33 22 11 00 00 00 00 
|-ELEMENT-0-|-ELEMENT-1-|-ELEMENT-2-|-FREE-SPACE|

In practical terms, if you are after the pointer to the contents (I assume this is the one you want) – pick mutableRawPointer, genericTypedPointer or forcedTypedPointer+element, while if you are after the pointer to array struct itself (which is very rarely needed) then choose arrayPointer, or forcedTypedPointer+array.

3 Likes

Thank you for this very detailed and informative reply! Do you happen to know what drives swift to make the different decisions of which memory location to return? Also, is it possible to for my own custom struct to have this behavior?
So like this:

struct MyHeader { /* Contains info for actual data on heap */ }

Can I write it so that with raw pointers swift gives the address for the data on heap and with typed pointers swift gives me the address on stack/struct itself (not sure why this is needed but just wondering).

I believe Swift is just doing you a courtesy by being able to convert from a pointer to array to a pointer to its elements, because the pointer to array elements is what's needed in 99.9% cases:

    func foo_constElementPtr(_ v: UnsafePointer<ElementType>) {}
    func foo_constArrayPtr(_ v: UnsafePointer<ArrayType>) {}
    func foo_constOtherPtr(_ v: UnsafePointer<Double>) {}
    func foo_mutableElementPtr(_ v: UnsafeMutablePointer<ElementType>) {}
    func foo_mutableArrayPtr(_ v: UnsafeMutablePointer<ArrayType>) {}
    func foo_mutableOtherPtr(_ v: UnsafeMutablePointer<Double>) {}

    foo_constElementPtr(&array)
    foo_constArrayPtr(&array)
    // foo_constOtherPtr(&array) // 🛑 Cannot convert value of type 'UnsafePointer<ElementType>' (aka 'UnsafePointer<Int32>') to expected argument type 'UnsafePointer<Double>'
    foo_mutableElementPtr(&array)
    foo_mutableArrayPtr(&array)
    // foo_mutableOtherPtr(&array) // 🛑 Cannot convert value of type 'UnsafeMutablePointer<ElementType>' (aka 'UnsafeMutablePointer<Int32>') to expected argument type 'UnsafeMutablePointer<Double>'

Note that you could even do this (without ampersand in this case):

    foo_constElementPtr(array) // ✅

I don't think this is possible for your custom struct.

2 Likes