Getting the size of Any value

This time I am drilling into Swift values without using objective-C APIs as in the previous case but by using the Mirror API. Good thing it works with structs as well as classes:

func printValue(label: String?, valuePtr: UnsafeRawPointer, value: Any, indentationLevel: Int = 0) {
    let m = Mirror(reflecting: value)
    print(indent(indentationLevel), terminator: "")
//    let size = MemoryLayout.size(ofValue: value) // always returns 32 🤔🤔🤔
    let size = size(of: value, display: m.displayStyle) // 🤔🤔🤔
    let sz = m.displayStyle == .class ? 8 : size
    let display = m.displayStyle != nil ? "\(m.displayStyle!) " : ""
    var nameAndType = "var " + label! + ": " + display + "\(m.subjectType)"
    while nameAndType.count < 32 - indentationLevel * 4 { nameAndType += " " }
    print("\(nameAndType) // \(sz) bytes (", terminator: "")
    printAnyBytes("", valuePtr, offset: 0, size: sz, terminator: ")")
    if m.displayStyle == .class {
        print(" -> \(size) bytes", terminator: "")
    }
    print()
    for child in m.children {
        var value = child.value
        printValue(label: child.label, valuePtr: &value, value: child.value, indentationLevel: indentationLevel + 1)
    }
}
Uninteresting bits
func indent(_ level: Int) -> String {
    (0 ..< level).reduce("") { r, e in r + "    " }
}

func printAnyBytes(_ title: String, _ ptr: UnsafeRawPointer, offset: Int, size: Int, terminator: String = "\n") {
    let p = unsafeBitCast(ptr, to: UnsafePointer<UInt8>.self)
    printBytes(title, p, offset: offset, size: size, terminator: terminator)
}

func printBytes(_ title: String, _ ptr: UnsafePointer<UInt8>, offset: Int, size: Int, terminator: String = "\n") {
    print("\(title)", terminator: "")
    for i in 0 ..< size {
        let ch = (ptr + offset + i).pointee
        if i != 0 {
            print(" ", terminator: "")
        }
        print(String(format: "%02x", ch), terminator: "")
    }
    print("", terminator: terminator)
}

With this implementation for the following example types:

class MyClass {
    var foo: Int16 = .max
    var bar: Double = .infinity
}

struct MyStruct {
    var foo: UInt8 = 0x11
    var myClass = MyClass()
    var bar: UInt16 = 0x3333
    let foo2: Int8 = 0x22
    var baz: UInt32 = 0x44444444
    let bar2: Int16 = -0x3333
    var qux: UInt64 = 0x5555555555555555
    var baz2: Int32 = -0x44444444
    let quux: (UInt8, Int) = (0x77, 0x6666666666666666)
    var qux2: Int64 = -0x5555555555555555
    let cgPoint = CGPoint(x: 1.2345, y: 3.1415)
}

var v = MyStruct()
printValue(label: "v", valuePtr: &v, value: v)

I am getting this autogenerated output:

var v: struct MyStruct           // 65 bytes 🛑 (11 c0 0e 05 01 00 00 00 00 f9 c1 02 00 60 00 00 33 33 22 05 44 44 44 44 cd cc 00 00 00 00 00 00 55 55 55 55 55 55 55 55 bc bb bb bb 01 00 00 00 66 66 66 66 66 66 66 66 77 60 f8 6a 01 00 00 00 ab) 🛑
    var foo: UInt8               // 1 bytes (11)
    var myClass: class MyClass   // 8 bytes (00 f9 c1 02 00 60 00 00) -> 10 bytes 🛑
        var foo: Int16           // 2 bytes (ff 7f)
        var bar: Double          // 8 bytes (00 00 00 00 00 00 f0 7f)
    var bar: UInt16              // 2 bytes (33 33)
    var foo2: Int8               // 1 bytes (22)
    var baz: UInt32              // 4 bytes (44 44 44 44)
    var bar2: Int16              // 2 bytes (cd cc)
    var qux: UInt64              // 8 bytes (55 55 55 55 55 55 55 55)
    var baz2: Int32              // 4 bytes (bc bb bb bb)
    var quux: tuple (UInt8, Int) // 9 bytes 🛑 (77 21 fc 00 00 60 00 00 66) 🛑
        var .0: UInt8            // 1 bytes (77)
        var .1: Int              // 8 bytes (66 66 66 66 66 66 66 66)
    var qux2: Int64              // 8 bytes (ab aa aa aa aa aa aa aa)
    var cgPoint: struct CGPoint  // 16 bytes (8d 97 6e 12 83 c0 f3 3f 6f 12 83 c0 ca 21 09 40)
        var x: Double            // 8 bytes (8d 97 6e 12 83 c0 f3 3f)
        var y: Double            // 8 bytes (6f 12 83 c0 ca 21 09 40)

which is almost perfect – I marked errors with :stop_sign:, they are related to padding bytes inserted to align field on their natural boundaries, and this implementation doesn't handle padding correctly (nor does it seem that Mirror API can return me values' offsets, just the values themselves).

The big question I had during implementing this is how to get the size of Any value correctly, specifically the value field of "Mirror.Child" which is typed as:

public typealias Child = (label: String?, value: Any)

NaĂŻvely using MemoryLayout.size(of: any) results into 32 (see the lines marked with :thinking::thinking::thinking:) hence I implemented my own code to get the size, the code I can't be less proud of :person_facepalming:

func size(of value: Any, display: Mirror.DisplayStyle?) -> Int {
    switch value {
    case is Bool: return MemoryLayout<Bool>.size
    case is Int8: return MemoryLayout<Int8>.size
    case is UInt8: return MemoryLayout<UInt8>.size
    case is Int16: return MemoryLayout<Int16>.size
    case is UInt16: return MemoryLayout<UInt16>.size
    case is Int32: return MemoryLayout<Int32>.size
    case is UInt32: return MemoryLayout<UInt32>.size
    case is Int64: return MemoryLayout<Int64>.size
    case is UInt64: return MemoryLayout<UInt64>.size
    case is Int: return MemoryLayout<Int>.size
    case is UInt: return MemoryLayout<UInt>.size
    case is Float: return MemoryLayout<Float>.size
    case is Double: return MemoryLayout<Double>.size
    case is CGFloat: return MemoryLayout<CGFloat>.size
    default:
        switch display {
        case .tuple, .class, .struct:
            return Mirror(reflecting: value).children.reduce(0) { r, e in
                let val = e.value
                return r + size(of: val, display: Mirror(reflecting: val).displayStyle)
            }
        default:
            fatalError("unhandled: \(value), type: \(type(of: value)), display: \(display)")
        }
    }
}

There must be a better way to get the size of Any value, but what is it? :thinking:

1 Like
func something<T>(_: T) -> Int {
  MemoryLayout<T>.size
}

func something2(_ x: Any) -> Int {
  something(x)
}

Is this what you're looking for?

this gives 32 :disappointed:

1 Like

Yeah, it does seem like the logic of implicitly opened existentials should extend to Any as well, but apparently here it doesn't (cc @Douglas_Gregor).

Using the underscored _openExistential API works though:

func something<T>(_: T) -> Int {
  MemoryLayout<T>.size
}
func something2(_ x: Any) -> Int {
  _openExistential(x, do: something)
}
something2(42) // 8
4 Likes

oh do we not implicitly unwrap the existential here? whoops try:

func something(_ x: Any) -> Int {
  func opened<T>(_: T) -> Int {
    MemoryLayout<T>.size
  }

  return _openExistential(x, do: opened(_:))
}

edit @xwu beat me

2 Likes

This will change in Swift 6; the current behavior is for backwards compatibility. See the section "Avoid opening when the existential type satisfies requirements (in Swift 5)" in the SE-0352 proposal.

7 Likes

I'm not sure whether I'm missing something in this thread, but if you want to use open existential, you need to use the keyword some in the signature.

#!/usr/bin/env swift

func foo<T>(_ t: T) {
    print(MemoryLayout<T>.size)
}
foo(1) // 8

func foo2(_ t: Any) {
    foo(t)
}
foo2(1) // 32

func foo3(_ t: any Any) {
    foo(t)
}
foo3(1) // 32

func foo4(_ t: some Any) {
    foo(t)
}
foo4(1) // 8

"some Any" :rofl:

For the record this method didn't work in this case as in the code it's more like in this example:

func foo<T>(_ t: T) {
    print(MemoryLayout<T>.size)
}
func foo4(_ t: some Any) {
    foo(t)
}
foo4(1) // 8 đź‘Ť
var x: Any = 1
foo4(x) // 32 đź‘Ž

Thank you @xwu and @Alejandro, your version works. Corrected output:

var v: struct MyStruct           // 88 bytes (11 80 83 04 01 00 00 00 e0 5d 45 01 00 60 00 00 33 33 22 04 44 44 44 44 cd cc 00 00 00 00 00 00 55 55 55 55 55 55 55 55 bc bb bb bb 01 00 00 00 77 6c 8f db 01 00 00 00 66 66 66 66 66 66 66 66 ab aa aa aa aa aa aa aa 8d 97 6e 12 83 c0 f3 3f 6f 12 83 c0 ca 21 09 40)
	var foo: UInt8               // 1 bytes (11)
	var myClass: class MyClass   // 8 bytes (e0 5d 45 01 00 60 00 00) -> 32 bytes
		var foo: Int16           // 2 bytes (ff 7f)
		var bar: Double          // 8 bytes (00 00 00 00 00 00 f0 7f)
	var bar: UInt16              // 2 bytes (33 33)
	var foo2: Int8               // 1 bytes (22)
	var baz: UInt32              // 4 bytes (44 44 44 44)
	var bar2: Int16              // 2 bytes (cd cc)
	var qux: UInt64              // 8 bytes (55 55 55 55 55 55 55 55)
	var baz2: Int32              // 4 bytes (bc bb bb bb)
	var quux: tuple (UInt8, Int) // 16 bytes (77 73 15 00 00 60 00 00 66 66 66 66 66 66 66 66)
		var .0: UInt8            // 1 bytes (77)
		var .1: Int              // 8 bytes (66 66 66 66 66 66 66 66)
	var qux2: Int64              // 8 bytes (ab aa aa aa aa aa aa aa)
	var cgPoint: struct CGPoint  // 16 bytes (8d 97 6e 12 83 c0 f3 3f 6f 12 83 c0 ca 21 09 40)
		var x: Double            // 8 bytes (8d 97 6e 12 83 c0 f3 3f)
		var y: Double            // 8 bytes (6f 12 83 c0 ca 21 09 40)

Is there a way to get field offset information somehow †? Desired output:

var v: struct MyStruct               // 88 bytes (11 40 65 02 01 00 00 00 40 c0 70 01 00 60 00 00 33 33 22 02 44 44 44 44 cd cc 00 00 00 00 00 00 55 55 55 55 55 55 55 55 bc bb bb bb 01 00 00 00 77 6c 8f db 01 00 00 00 66 66 66 66 66 66 66 66 ab aa aa aa aa aa aa aa 8d 97 6e 12 83 c0 f3 3f 6f 12 83 c0 ca 21 09 40)
	@0  var foo: UInt8               // 1 bytes (11)
	@1  padding                      // 7 bytes (40 65 02 01 00 00 00)
	@8  var myClass: class MyClass   // 8 bytes (40 c0 70 01 00 60 00 00) -> 32 bytes
		@0  padding                  // 16 bytes (xx xx xx xx xx xx xx xx xx xx xx xx xx xx xx xx)
		@16 var foo: Int16           // 2 bytes (ff 7f)
		@18 padding                  // 6 bytes (xx xx xx xx xx xx)
		@24 var bar: Double          // 8 bytes (00 00 00 00 00 00 f0 7f)
	@16 var bar: UInt16              // 2 bytes (33 33)
	@18 var foo2: Int8               // 1 bytes (22)
	@19 padding                      // 1 bytes (02)
	@20 var baz: UInt32              // 4 bytes (44 44 44 44)
	@24 var bar2: Int16              // 2 bytes (cd cc)
	@26 padding                      // 6 bytes (00 00 00 00 00 00)
	@32 var qux: UInt64              // 8 bytes (55 55 55 55 55 55 55 55)
	@40 var baz2: Int32              // 4 bytes (bc bb bb bb)
	@44 padding                      // 4 bytes (01 00 00 00)
	@48 var quux: tuple (UInt8, Int) // 16 bytes (77 6c 8f db 01 00 00 00 66 66 66 66 66 66 66 66)
		@0  var .0: UInt8            // 1 bytes (77)
		@1  padding                  // 7 bytes (6c 8f db 01 00 00 00)
		@8  var .1: Int              // 8 bytes (66 66 66 66 66 66 66 66)
	@64 var qux2: Int64              // 8 bytes (ab aa aa aa aa aa aa aa)
	@72 var cgPoint: struct CGPoint  // 16 bytes (8d 97 6e 12 83 c0 f3 3f 6f 12 83 c0 ca 21 09 40)
		@0  var x: Double            // 8 bytes (8d 97 6e 12 83 c0 f3 3f)
		@8  var y: Double            // 8 bytes (6f 12 83 c0 ca 21 09 40)

(† - In specific cases I can decipher offsets searching for byte sub strings, but that doesn't work in general).

2 Likes

It looks like a bug in Swift to me. Other protocols behave correctly :thinking:

#!/usr/bin/env swift

func foo<T>(_ t: T) {
    print(MemoryLayout<T>.size)
}

func foo4(_ t: some Any) {
    foo(t)
}

protocol A {}
extension Int: A {}

func foo5<T: A>(_ t: T) {
    print(MemoryLayout<T>.size)
}

func foo6(_ t: some A) {
    foo5(t)
}

let x: any Any = 1
foo4(x) // 32
let y: any A = 1
foo6(y) // 8

No, it's not a bug. It's a phased change outlined below. The reason foo5 and foo6 work is because the have the conformance requirement <T: A>. Since any A does not conform to protocol A, it can't stand that any A == T, so any A needs to be opened in order to be passed to these functions.

2 Likes

Oh I wasn't aware of that, thank you.

Still, it doesn't make sense to me, that even when i specifically type val: some Any, the existentioal isn't opened. I thought, that the description "arg: some P is just another way of typing arg: T where T: P" was a way to explain the concept, rather then actual description of how it really works. :thinking:

1 Like

I guess I can try "second guess" it:

  • let's consider there was Int8 at offset 0
  • the next field is Int32. It's alignment should be 4
  • but current offset is 1, so there must be 3 bytes of padding

or will it be too fragile?

Every Swift type (including existengials) is a subtype of Any, so T: Any isn’t an actual constraint.

Reading the documentation on Type Layout, I was left with the impression, that Any is a special case of existential with NUM_WITNESS_TABLES = 0.

If that's correct, I don't see why opening Any should not be a thing. From what you told me it looks like, that without _openExistential we won't be able to open Any even past Swift 6. This just feels weird to me...

Sorry if I was unclear. Yes, any Any is an existential and it can be opened through _openExistential. My point was that the type system considers every type T as conforming to the protocol Any, i.e. T: Any. Consider the following example.

func f(_: some Any) {} // Same as f<T>(_: T) {}
f(1 as any Any) // T inferred as `any Any` in all versions of Swift 5

The above code worked before SE-0352 (for implicitly opening existentials). Thus, to preserve compatibility any Any is not opened to the underlying Int type in Swift 5 mode. In Swift 6, all existentials will be implicitly opened, so T will be inferred as Int. Hope this helps!

4 Likes

I gave it a spin, and got promising results. For the following value:

class MyClass {
    var foo: Int16 = 0x5555
    var bar: Double = Double(bitPattern: 0xFFFFFFFFFFFFFFFF)
}

struct MyStruct {
    var foo: UInt8 = 0x11
    var myClass = MyClass()
    var bar: UInt16 = 0x3333
    let foo2: Int8 = 0x22
    var baz: UInt32 = 0x44444444
    let bar2: Int16 = -0x3333
    var qux: UInt64 = 0x5555555555555555
    var baz2: Int32 = -0x44444444
    let quux: (UInt8, Int) = (0x77, 0x6666666666666666)
    var qux2: Int64 = -0x5555555555555555
    let cgPoint = CGPoint(x: Double(bitPattern: 0x6666666666666666), y: Double(bitPattern: 0x7777777777777777))
}

now getting this autogenerated output:

@0 var v: struct MyStruct            // 88 bytes (11 00 ae 00 01 00 00 00 c0 5a f6 02 00 60 00 00 33 33 22 00 44 44 44 44 cd cc 00 00 00 00 00 00 55 55 55 55 55 55 55 55 bc bb bb bb 01 00 00 00 77 6c 8f db 01 00 00 00 66 66 66 66 66 66 66 66 ab aa aa aa aa aa aa aa 66 66 66 66 66 66 66 66 77 77 77 77 77 77 77 77)
    @0 var foo: UInt8                // 1 bytes (11)
    @1 padding                       // 7 bytes (00 ae 00 01 00 00 00)
    @8 var myClass: class MyClass    // 8 bytes (c0 5a f6 02 00 60 00 00) -> 32 bytes
        @0 padding                   // 16 bytes (d8 02 9c 00 01 00 00 00 03 00 00 00 08 00 00 00)
        @16 var foo: Int16           // 2 bytes (55 55)
        @18 padding                  // 6 bytes (00 00 00 00 00 80)
        @24 var bar: Double          // 8 bytes (ff ff ff ff ff ff ff ff)
    @16 var bar: UInt16              // 2 bytes (33 33)
    @18 var foo2: Int8               // 1 bytes (22)
    @19 padding                      // 1 bytes (00)
    @20 var baz: UInt32              // 4 bytes (44 44 44 44)
    @24 var bar2: Int16              // 2 bytes (cd cc)
    @26 padding                      // 6 bytes (00 00 00 00 00 00)
    @32 var qux: UInt64              // 8 bytes (55 55 55 55 55 55 55 55)
    @40 var baz2: Int32              // 4 bytes (bc bb bb bb)
    @44 padding                      // 4 bytes (01 00 00 00)
    @48 var quux: tuple (UInt8, Int) // 16 bytes (77 6c 8f db 01 00 00 00 66 66 66 66 66 66 66 66)
        @0 var .0: UInt8             // 1 bytes (77)
        @1 padding                   // 7 bytes (6c 8f db 01 00 00 00)
        @8 var .1: Int               // 8 bytes (66 66 66 66 66 66 66 66)
    @64 var qux2: Int64              // 8 bytes (ab aa aa aa aa aa aa aa)
    @72 var cgPoint: struct CGPoint  // 16 bytes (66 66 66 66 66 66 66 66 77 77 77 77 77 77 77 77)
        @0 var x: Double             // 8 bytes (66 66 66 66 66 66 66 66)
        @8 var y: Double             // 8 bytes (77 77 77 77 77 77 77 77)

So I'm starting with offset = 0, adding the first field size to it, and checking the next field alignment to know if there are padding bytes and how many. When "stepping" into a class (offset @8 above) I've hardcoded the first field offset to be 16 (and thus the first 16 bytes where isa / RC fields reside are now treated as padding). So, all in all, it works... Although it feels very fragile and the fear is my algorithm of calculating field offsets getting out of sync with Swift's.

Should we enhance Mirroring API (the current or the future) to include value offset information?

1 Like

Is there a reason you can’t use MemoryLayout.offset(of:)?

I probably could if I knew how to get the relevant key path!

This is an oversimplified version of the current code (print statements and class support removed)
func printValue(_ value: Any, valuePtr: UnsafeRawPointer, offset: Int = 0) -> Int {
    let m = Mirror(reflecting: value)
    let size = size(of: value)
    let alignment = alignment(of: value)
    
    var offset = offset
    var delta = (alignment - (offset % alignment)) % alignment
    if delta != 0 {
        offset += delta
    }
    var subOffset = 0
    let p = valuePtr + offset
    for child in m.children {
        subOffset = printValue(child.value, valuePtr: p, offset: subOffset)
    }
    offset += size
    return offset
}

func size(of x: Any) -> Int {
    func opened<T>(_: T) -> Int {
        MemoryLayout<T>.size
    }
    return _openExistential(x, do: opened)
}

func alignment(of x: Any) -> Int {
    func opened<T>(_ x: T) -> Int {
        MemoryLayout<T>.alignment
    }
    return _openExistential(x, do: opened)
}

struct TestStruct {
    var foo: UInt8 = 0x11
    var bar: UInt16 = 0x3333
    let foo2: Int8 = 0x22
    var baz: UInt32 = 0x44444444
    let bar2: Int16 = -0x3333
    var qux: UInt64 = 0x5555555555555555
    var baz2: Int32 = -0x44444444
    let quux: (UInt8, Int) = (0x77, 0x6666666666666666)
    var qux2: Int64 = -0x5555555555555555
    let cgPoint = CGPoint(x: Double(bitPattern: 0x6666666666666666), y: Double(bitPattern: 0x7777777777777777))
}

var v = TestStruct()
printValue(v, valuePtr: &v)

1 Like

Yeah, for non-frozen types Swift does not promise any particular layout, and I think it is likely to change in the future because the current one wastes space!

1 Like

:crossed_fingers: by then there'll be a sanctioned way to get fields offsets (via Mirror api or something else).

2 Likes