2

I am working with a variety of structs in Swift that I need to be able to look at the memory of directly.

How can I look at a struct byte for byte?

For example:

struct AwesomeStructure {
  var index: Int32
  var id: UInt16
  var stuff: UInt8
  // etc.
}

The compiler will not allow me to do this:

func scopeOfAwesomeStruct() {
    withUnsafePointer(to: &self, { (ptr: UnsafePointer<Int8>) in
    })
}

Obviously because withUnsafePointer is a templated function that requires the UnsafePointer to be the same type as self.

So, how can I break down self (my structs) into 8 bit pieces? Yes, I want to be able to look at index in 4, 8-bit pieces, and so-on.

(In this case, I'm trying to port a CRC algorithm from C#, but I have been confounded by this problem for other reasons as well.)

Brandon M
  • 349
  • 2
  • 20
  • 3
    Natural warning: The layout algorithm the Swift compiler uses to determine struct memory layout is not stable. It can change between versions. Anything you write that "works" now, is subject to breakage at any time, without warning. – Alexander Sep 24 '18 at 22:01
  • I want to be able to iterate through a structure in 8-bit segments, regardless of the types of its properties. If I have a function that knows the length of the structure, this can be done safely. – Brandon M Sep 24 '18 at 22:03
  • What if the compiler decides to not zero out the padding bytes? – Alexander Sep 24 '18 at 22:28

3 Answers3

3

edit/update: Xcode 12.5 • Swift 5.4


extension ContiguousBytes {
    func object<T>() -> T { withUnsafeBytes { $0.load(as: T.self) } }
}

extension Data {
    func subdata<R: RangeExpression>(in range: R) -> Self where R.Bound == Index {
        subdata(in: range.relative(to: self) )
    }
    func object<T>(at offset: Int) -> T { subdata(in: offset...).object() }
}

extension Numeric {
    var data: Data {
        var source = self
        return Data(bytes: &source, count: MemoryLayout<Self>.size)
    }
}

struct AwesomeStructure {
    let index: Int32
    let id: UInt16
    let stuff: UInt8
}

extension AwesomeStructure {
    init(data: Data) {
        index = data.object()
        id = data.object(at: 4)
        stuff = data.object(at: 6)
    }
    var data: Data { index.data + id.data + stuff.data }
}

let awesomeStructure = AwesomeStructure(index: 1, id: 2, stuff: 3)
let data = awesomeStructure.data
print(data)  //  7 bytes

let structFromData = AwesomeStructure(data: data)
print(structFromData)   // "AwesomeStructure(index: 1, id: 2, stuff: 3)\n"
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
2

You can use withUnsafeBytes(_:) directly like this:

mutating func scopeOfAwesomeStruct() {
    withUnsafeBytes(of: &self) {rbp in
        let ptr = rbp.baseAddress!.assumingMemoryBound(to: UInt8.self)
        //...
    }
}

As already noted, do not export ptr outside of the closure.

And it is not safe even if you have a function that knows the length of the structure. Swift API stability is not declared yet. Any of the layout details of structs are not guaranteed, including the orders of the properties and how they put paddings. Which may be different than the C# structs and may generate the different result than that of C#.

I (and many other developers) believe and expect that the current layout strategy would not change in the near future, so I would write some code like yours. But I do not think it's safe. Remember Swift is not C.

(Though, it's all the same if you copy the contents of a struct into a Data.)

If you want a strictly exact layout with C, you can write a C struct and import it into your Swift project.

OOPer
  • 47,149
  • 6
  • 107
  • 142
  • Thank you! It seems the verdict is in. I should either just use straight C, or concede to using a Data object. – Brandon M Sep 25 '18 at 14:12
  • `.assumingMemoryBound` was very helpful. Elaborating on the stability of the Swift API was very helpful. This answer validates why making a Data copy seems to be the route I should take. Thank you. – Brandon M Sep 25 '18 at 14:13
1

Here's a decent first approximation. The trick is to use Swift.withUnsafeBytes(_:) to get a UnsafeRawBufferPointer, which can then be easily converted to Data using Data.init<SourceType>(buffer: UnsafeMutableBufferPointer<SourceType>).

This causes a copy of the memory, so you don't have to worry about any sort of dangling pointer issues.

import Foundation

struct AwesomeStructure {
    let index: Int32 = 0x56
    let id: UInt16 = 0x34
    let stuff: UInt8 = 0x12
}

func toData<T>(_ input: inout T) -> Data {
    var data = withUnsafeBytes(of: &input, Data.init)
    let alignment = MemoryLayout<T>.alignment
    let remainder = data.count % alignment

    if remainder == 0 {
        return data
    }
    else {
        let paddingByteCount = alignment - remainder
        return data + Data(count: paddingByteCount)
    }
}

extension Data {
    var prettyString: String {
        return self.enumerated()
            .lazy
            .map { byteNumber, byte in String(format:"/* %02i */ 0x%02X", byteNumber, byte) }
            .joined(separator: "\n")
    }
}

var x = AwesomeStructure()
let d = toData(&x)
print(d.prettyString)
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • Do I really have to make a `Data` copy? Can I not simply zone that memory somehow and recast it to pieces of `UInt8`? – Brandon M Sep 24 '18 at 22:33
  • Maybe I need `UnsafeBufferPointer`, but I have not yet put the pieces of the way Swift does unsafe pointers together quite yet. :( – Brandon M Sep 24 '18 at 22:34
  • Suppose the 'AwesomeStructure' was a C struct and we were using a C compiler. Essentially, I want to be able to cast something like: `char* cCast = (char*)awesomeStructInstance;` – Brandon M Sep 24 '18 at 22:36
  • Of course this assumes that the size of `awesomeStructInstance` is well known so as to ensure memory safety. – Brandon M Sep 24 '18 at 22:38
  • @BrandonMantzey You can, but you almost certainly wouldn't want to. The pointer provided to the closure you give when calling `withUnsafeBytes` is only guaranteed to be valid within the scope of that closure. If you really want to avoid the copy, all your memory-reading logic would have to occur within that closure. – Alexander Sep 24 '18 at 22:38
  • That's fine. I could make the function call occur within the scope of that closure. I would only escape the result, which would be an unsigned short. It's a CRC algorithm. So how would I go about doing that? – Brandon M Sep 24 '18 at 22:43
  • Using `UnsafeBufferPointer` directly *can* work, but as the name implies... it's unsafe. Unless you have strict performance reasons that prevent you from using `Data`, it's much preferred. The copy ensures memory safety, and guarantees you won't have dangling pointer issues, as you would have in that sample C code. – Alexander Sep 24 '18 at 22:43
  • @BrandonMantzey You just put your code inside the closure passed into `withUnsafeBytes(_:)`, and then you just return the result, and assign the whole expression to some `let checksum: UInt16`, or whatever – Alexander Sep 24 '18 at 22:44