- Proposal: SE-0210
- Authors: Joe Groff
- Review Manager: Doug Gregor
- Status: Implemented (Swift 4.2)
- Implementation: apple/swift#15519
This proposal introduces the ability for Swift code to query the in-memory
layout of stored properties in aggregates using key paths. Like the
offsetof
macro in C, MemoryLayout<T>.offset(of:)
returns the distance in
bytes between a pointer to a value and a pointer to one of its fields.
Swift-evolution thread: Pitch: “offsetof”-like functionality for stored property key paths
Many graphics and math libraries accept input data in arbitrary input formats,
which the user has to describe to the API when setting up their input buffers.
For example, OpenGL lets you describe the layout of vertex buffers using
series of calls to the glVertexAttribPointer
API. In C, you can use the
standard offsetof
macro to get the offset of fields within a struct, allowing
you to use the compiler's knowledge of a type's layout to fill out these
function calls:
// Layout of one of our vertex entries
struct MyVertex {
float position[4];
float normal[4];
uint16_t texcoord[2];
};
enum MyVertexAttribute { Position, Normal, TexCoord };
glVertexAttribPointer(Position, 4, GL_FLOAT, GL_FALSE,
sizeof(MyVertex), (void*)offsetof(MyVertex, position));
glVertexAttribPointer(Normal, 4, GL_FLOAT, GL_FALSE,
sizeof(MyVertex), (void*)offsetof(MyVertex, normal));
glVertexAttribPointer(TexCoord, 2, GL_UNSIGNED_BYTE, GL_TRUE,
sizeof(MyVertex), (void*)offsetof(MyVertex, texcoord));
There's currently no equivalent to offsetof
in Swift, so users of these kinds
of APIs must either write those parts of their code in C or else do Swift
memory layout in their heads, which is error-prone if they ever change their
data layout or the Swift compiler implementation changes its layout algorithm
(which it reserves the right to do).
Key paths now provide a natural way to refer to fields in Swift. We can add
an API to the MemoryLayout
type to ask for the offset of the field
represented by a key path.
A new API is added to MemoryLayout
:
extension MemoryLayout {
func offset(of key: PartialKeyPath<T>) -> Int?
}
If the given key
refers to inline storage within the
in-memory representation of T
, and the storage is directly
addressable (meaning that accessing it does not need to trigger any
didSet
or willSet
accessors, perform any representation changes
such as bridging or closure reabstraction, or mask the value out of
overlapping storage as for packed bitfields), then the return value
is a distance in bytes that can be added to a pointer of type T
to
get a pointer to the storage accessed by key
. In other words, if the return
value is non-nil, then these formulations are equivalent:
var root: T, value: U
var key: WritableKeyPath<T, U>
// Mutation through the key path...
root[keyPath: \.key] = value
// ...is exactly equivalent to mutation through the offset pointer...
withUnsafePointer(to: &root) {
(UnsafeMutableRawPointer($0) + MemoryLayout<T>.offset(of: \.key))
// ...which can be assumed to be bound to the target type
.assumingMemoryBound(to: U.self).pointee = value
}
One possible set of answers for a Swift struct might look like this:
struct Point {
var x, y: Double
}
struct Size {
var w, h: Double
var area: Double { return w*h }
}
struct Rect {
var origin: Point
var size: Size
}
MemoryLayout<Rect>.offset(of: \.origin.x) // => 0
MemoryLayout<Rect>.offset(of: \.origin.y) // => 8
MemoryLayout<Rect>.offset(of: \.size.w) // => 16
MemoryLayout<Rect>.offset(of: \.size.h) // => 24
MemoryLayout<Rect>.offset(of: \.size.area) // => nil
In Swift today, only key paths that refer to struct fields would support taking their offset, though if support for tuple elements in key paths were added in the future, tuple elements could as well. Class properties are always stored out-of-line, and require runtime exclusivity checking to access, so their offsets would not be available by this mechanism.
This is an additive change to the API of MemoryLayout
.
KeyPath
objects already encode the offset information for stored properties
necessary to implement this, so this has no additional demands from the ABI.
Clients of an API could potentially use this functionality to dynamically
observe whether a public property is implemented as a stored property from
outside of the module. If a client assumes that a property will always be
stored by force-unwrapping the optional result of offset(of:)
, that could
lead to compatibility problems if the library author changes the property to
computed in a future library version. Client code using offsets should be
careful not to rely on the stored-ness of properties in types they don't
control.
Instead of a new static method on MemoryLayout
, this functionality could also
be expressed as an offset
property on KeyPath
. All of the information
necessary to answer the offset question is in the KeyPath
value itself.
Nonetheless, MemoryLayout
seems like the natural place to put this API.
A related API that might be useful to build on top of this functionality would
be to add methods to UnsafePointer
and UnsafeMutablePointer
for projecting
a pointer to a field from a pointer to a base value, for example:
extension UnsafePointer {
subscript<Field>(field: KeyPath<Pointee, Field>) -> UnsafePointer<Field> {
return (UnsafeRawPointer(self) + MemoryLayout<Pointee>.offset(of: field))
.assumingMemoryBound(to: Field.self)
}
}
extension UnsafeMutablePointer {
subscript<Field>(field: KeyPath<Pointee, Field>) -> UnsafePointer<Field> {
return (UnsafeRawPointer(self) + MemoryLayout<Pointee>.offset(of: field))
.assumingMemoryBound(to: Field.self)
}
subscript<Field>(field: WritableKeyPath<Pointee, Field>) -> UnsafeMutablePointer<Field> {
return (UnsafeMutableRawPointer(self) + MemoryLayout<Pointee>.offset(of: field))
.assumingMemoryBound(to: Field.self)
}
}