@Lantua thank you so much, since we are using Int (on both solutions, like load(as: Int.self) and assumingMemoryBound(to: Int.self)) is it less implementation specific or working with unsafe raw pointer always is reliant on implementation details of the types and might break when implementation details change ?
It's largely about semantic, the memory layout for simple types should be pretty solid by now.
I'm more concerned about memory binding. In an ideal world, each part of the memory would be tagged, as Int or UInt8, and we're trying to read Int out of UInt8 memory, which is not allowed in Swift. (There's also an implication that using memory with incorrect binding could lead to undefined behavior though I'm not sure how that works.)
assumingMemoryBound, well, assumes that you already bind that region of memory to Int at some point (which we didn't) and binding memory to a type would cause it to lose binding of the previous type (we would lose UInt8 binding) so we don't want that either.
So I'm trying to find a variant that doesn't change/assume the binding of the memory, and load(fromByteOffset:as:) seems to be what I want.
If you're curious, the docs as well as SE-0107 would be useful.
I do this a lot because I’m always parsing network packets and other weirdo data structures, and in my experience the best way to do this is byte-wise. For example, for little endian:
let bytes: [UInt8] = [0x01, 02, 03, 04]
let u32 = bytes.reversed().reduce(0) { soFar, byte in
return soFar << 8 | UInt32(byte)
}
print(String(u32, radix: 16)) // 4030201
Drop the reversed for big endian.
Doing this using unsafe pointers is a pain for a number of reasons:
It’s… well… unsafe. A simple mistake can ruin your whole day.
And it’s easy to make a mistake. As an example, my code above converts to UInt32 because it’s pretty darned rare that you find a network protocol that uses Int (remember that Int changes size depending on your architecture).
You have to worry about type punning.
You have to worry about memory alignment.
You have to worry about big and little endian. Admittedly the code above deals with little and big endian, but when you assemble multi-byte integers byte-by-byte, it’s hard to forget about that problem (-:
Now, don’t get me wrong, there are clearly cases where dealing with unsafe pointers is the right choice, but IMO it shouldn’t be your first choice.
We can generalize @eskimo's solution a bit if we want (perhaps too much) to work with any iterator or collection of UInt8 and any FixedWidthInteger type:
extension FixedWidthInteger {
init<I>(littleEndianBytes iterator: inout I)
where I: IteratorProtocol, I.Element == UInt8 {
self = stride(from: 0, to: Self.bitWidth, by: 8).reduce(into: 0) {
$0 |= Self(truncatingIfNeeded: iterator.next()!) &<< $1
}
}
init<C>(littleEndianBytes bytes: C) where C: Collection, C.Element == UInt8 {
precondition(bytes.count == (Self.bitWidth+7)/8)
var iter = bytes.makeIterator()
self.init(littleEndianBytes: &iter)
}
}
Even in this ridiculous generality, Swift generates basically optimal code with optimization enabled, which I think is pretty cool:
I think it's quite common to parse a byte array into some logical structure, and handle e.g. endianness and different widths.
I usually use an ArraySlice and an extension to the ArraySlice and a throwing method to extract values with the correct endianness.
E.g. (quite pseudo):
var slice = buffer[buffer.startIndex..<buffer.endIndex]
let value = try slice.consume(type: UInt32.self, endian: .big)
let count = try slice.consume(type: Int16.self)
let string = try slice.consumeString(encoding: .utf8, length: count)
which takes care of e.g. reading out-of-bounds and type conversion.
IMHO something like this is missing from the standard library, as we otherwise have to drop down to using UnsafePointers or a the, little hacky, solution of reducing the buffer - and we just want to get the data parsed and get on with it. :)
load(as:) requires that the pointer be suitably-aligned for the type being loaded (this is something that @Joe_Groff, @Andrew_Trick and others have been chatting about improving recently). From the documentation of load(as:):
The buffer pointer plus offset must be properly aligned for accessing an instance of type T.
Your example happens to work some of the time, because there's no opportunity for the compiler to abuse the undefined behavior you're invoking, and you're running on a CPU that silently supports unaligned access to memory (and your slice is likely to be four-byte aligned by virtue of how it was created). However, on other architectures this might trap or produce unspecified results, and even on x86, it may allow the compiler to generate vectorized code that traps when the pointer is unaligned, or optimize on the assumption that it is and produce undefined behavior.
In debug builds at least, UnsafeRawPointer.load(as:) will assert on misaligned data. Clearly, it would be really nice to be able to UnsafeRawPointer.load(as:) on unaligned data. I think we've seen enough justification by now to loosen this restriction, which won't affect any existing code. If we really want aligned raw pointer loads for performance in the future then we could introduce an alignedLoad API later.
Just for those who want a stable follow-up for Steve's example, here's a compiler explorer link to the code. This is a good example of why it's important to push on the Swift optimiser: it should always be possible to write very general safe code that generates optimal (or nearly optimal) output in the easy case and good output in the complex case.
Interestingly, in -Ounchecked, the assembly for foo is quite a bit longer. I would have expected it to be the same sans the comparison and jump. Is there a missed optimization opportunity in -Ounchecked, or is this just my very basic understanding of assembly shining through?
Similarly, the -Osize version does not inline, which sounds right at first, but the resulting foo is actually longer than in -O as well, so inlining would actually bring code-size benefits as well iiuc
This maybe a newbie question, but would you please explain why do you do the precondition this way?
Why do you add 7 first then divide the sum by 8? What is the problem with using (Self.bitWidth / 8) directly?
Is it valid and safe if I use bytes.count = MemoryLayout<Self>.size / MemoryLayout<UInt8>.size as precondition here?
Why do you add 7 first then divide the sum by 8? What is the problem with using (Self.bitWidth / 8) directly?
It's just me being extra-paranoid and making it work correctly for hypothetical integer types with bitWidth that is not a multiple of 8. No public standard library type would ever be in that boat, and it would be slightly weird for a custom type as well, but it is possible.
Is it valid and safe if I use bytes.count = MemoryLayout<Self>.size / MemoryLayout<UInt8>.size as precondition here?
I don't think that there's a guarantee that a type conforming to FixedWidthInteger doesn't have some additional fields other than its numerical value, which would make this do the wrong thing. This would be extremely odd, however.