/// alignment: 4
/// size.....: 5
/// stride...: 8
///
struct Foo {
let large: UInt32
let small: UInt8
}
/// alignment: 1
/// size.....: 3
/// stride...: 3
///
struct Bar {
let first: UInt8
let second: UInt8
let third: UInt8
}
Clearly, we may write [Foo][Bar] to any 8-byte region that is 4-byte aligned, but the memory binding APIs are adversarial:
let startOfFooBar: UnsafeRawPointer = // mmap or another source of raw bytes
let bar = startOfFooBar.advanced(by: 5).bindMemory(to: Bar.self, capacity: 1) // .....[Bar] (3 bytes)
let foo = startOfFooBar.advanced(by: 0).bindMemory(to: Foo.self, capacity: 1) // [Foo][Bar] (8 bytes) (!)
At this point, accessing bar as Bar is undefined because the memory region that contains bar is bound to Foo. This problem only occurs because the memory binding APIs are documented as overshooting when MemoryLayout<T>.size < MemoryLayout<T>.stride. While this specific example is obviously contrived (and there appears to be no runtime sanitizer for it), I think it would be fantastic if the documentation didn't invalidate what would otherwise be a perfectly valid program.
The secret 3rd option is adding an UnsafeRawPointer/bindMemory(to:) method that binds the size of one T. With this hypothetical API, we could keep using typed pointers happily ever after while preserving the stride semantics of UnsafeRawPointer/bindMemory(to:capacity:):
let startOfFooBar: UnsafeRawPointer = // mmap or another source of raw bytes
let bar = startOfFooBar.advanced(by: 5).bindMemory(to: Bar.self) // .....[Bar] (3 bytes)
let foo = startOfFooBar.advanced(by: 0).bindMemory(to: Foo.self) // [Foo]..... (5 bytes)
I'm asking for a memory binding API that binds the size of a type, as opposed to its stride. I'm also showing that the existing memory binding APIs are insufficient for this task. If such APIs existed (2nd post) or if Swift updated its current APIs (1st post), then the program would be valid by definition. I'm simply saying there's a design space that's waiting on the programming language's approval.
let startOfFooBar: UnsafeRawPointer = ...
let fooBarTyped = startOfFooBar.bindMemory(to: (Foo, Bar).self, capacity: 1)
let foo = fooBarTyped.pointer(to: \.0)!
let bar = fooBarTyped.pointer(to: \.1)!
I believe the tuple approach works in this specific case because the size of (Foo, Bar) is equal to its stride. But if Bar were to lose its third, then we would circle back to the case where size < stride and fooBarTyped over-binds by one byte into the following memory region.
Memory binding isn’t a permanent change to the memory; it simply dictates what the compiler believes may exist within a range of memory. “Overbinding” is therefore only a problem if you let the compiler believe invalid things beyond what is useful to you. What you should do is use temporary binding, withMemoryBound(to: T.self), or wrap the loadUnaligned or load functions.
The bindMemory() function uses very simple calculations because it exists in service of UnsafePointer<T>, which does bind N strides for N items. That forms part of the basis for Array. I don’t think we can change that.
Even without it forming the basis for Array, I don’t think we would be easily able to change it, because even the resulting UnsafeBufferPointer<T> binds n*stride bytes.
This reminds me of the mental gymnastics meme where one side simply moves from A to B while the other ends up flying through a burning city on a jetpack.
Memory binding isn’t a permanent change to the memory; it simply dictates what the compiler believes may exist within a range of memory. [...] What you should do is use temporary binding, withMemoryBound(to: T.self), or wrap the loadUnaligned or load functions.
Jokes aside, the laissez-faire approach to memory binding loses its luster when you realize that the language reserves the right to rug-pull whosoever dares to overbind, underbind, or misalign their memory bindings. So, naturally, you confront the rule-maker and say: "Sir, Mr. Malevolent Genie, what you ask of me can't be done. There's no way to bind the size of an arbitrary type using only stride-based memory binding APIs!" But, in response, the malevolent genie just hunches his back, rubs his hands like that infamous caricature, then answers you in the most triumphant voice his underhanded being can muster: "Exactly, what you think is yours is actually mine!". Applause echoes in his courtroom, as his jury of seals clap like they haven't yet realized that Mr. Malevolent Genie is malevolent and that they, too, are doomed. This is the plot of "Aladdin, Memory Binding in Swift".
Given that everbody has, so far, ignored the secret 3rd option, I'll reiterate that you can solve this problem properly while keeping the stride-based semantics of existing memory binding APIs. All you have to do is add a single method that binds the size of a one instance of T instead of its stride. That would solve the singular case and the plural case because a properly fitted collection can be represented as having a stride-based prefix binding followed by one properly fitted binding at the end.
Out of curiosity, if we were to change it, would it break anything?
And if we didn't have this already implemented and were pitching it today would we still prefer the stride based approach instead of the proposed stride + size for last component approach?
Considering this struct:
struct Combo {
var foo: Foo // size: 5, stride: 8
var bar: Bar // alignment: 1
}
and given I want to bind foo and bar independently (for whatever reason) it is indeed currently impossible to bind them simultaneously with the stride oriented API. I agree it could be better. Worth a pitch?
I'm all for pitching language improvements, but I would need some semblance of hope that a pitch wouldn't just be declared dead on arrival or stonewalled in perpetuity.
I was going to follow up on this thread by comparing the bindMemory(...) to initializeMemory(...) because surely the latter only binds the size of T (nope). I could then argue that the inconsistency is particularly unforgiving for my niche-but-cool use case, where the latter cannot be used. But, as it turns out, we don't have to venture that far into the weeds of memory management because the latter is also stride-based.
Why is this a problem? Your typical array is homogeneous, but let's say you want to write a heterogeneous array. Imagine a bump allocator for simplicity. When you append an element, you look ahead from the end of the last element, find the first address that is properly aligned, then write the bytes of the new element. You may also return a typed pointer to it in case you want to reference it later. Unrolled, it may look something like the following:
struct Foo { let large: UInt32; let small: UInt8 }
struct Bar { let first: UInt8 ; let second: UInt8 }
let mem = UnsafeMutableRawPointer.allocate(byteCount: 7, alignment: 4)
var end = mem
end = end.alignedUp(toMultipleOf: MemoryLayout<Foo>.alignment)
let foo = end.initializeMemory(as: Foo.self, to: Foo(large: 1, small: 2))
end = end.advanced(by: MemoryLayout<Foo>.size)
end = end.alignedUp(toMultipleOf: MemoryLayout<Bar>.alignment)
let bar = end.initializeMemory(as: Bar.self, to: Bar(first: 3, second: 4))
end = end.advanced(by: MemoryLayout<Bar>.size)
foo.pointee.large // 1
foo.pointee.small // 2
bar.pointee.first // 3
bar.pointee.second // 4
UnsafeRawPointer(mem).distance(to: foo) // 0
UnsafeRawPointer(mem).distance(to: bar) // 5
UnsafeRawPointer(mem).distance(to: end) // 7
I don't think this is a particularly niche use case. It definitely falls under the umbrella of things I could imagine using Swift for. But the point of this post is that the documentation forbids it. Swift reserves the right to flatten you with its gavel if you try. So, if you like your three-dimensional shape and you don't want to turn into a pancake, then you must ritualistically pad your allocations as if offering the wasted memory to a demon trapped in silicon. "Here, little guy, you can live in the space between Foo and Bar."
I get it. The rule is simple. The rule is clear. I can follow the rule. But I'm left wondering: why is this the rule? It seems so arbitrary to me. And if it is both arbitrary and unenforced, why can't we just change it?