From 3659ab1a873a3f00addacd9d18a87dd29f5c1923 Mon Sep 17 00:00:00 2001 From: Joe Groff Date: Mon, 26 Mar 2018 14:38:03 -0700 Subject: [PATCH] Proposal to add offset(of:) to MemoryLayout. --- proposals/NNNN-key-path-offset.md | 173 ++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 proposals/NNNN-key-path-offset.md diff --git a/proposals/NNNN-key-path-offset.md b/proposals/NNNN-key-path-offset.md new file mode 100644 index 0000000000..0542f681d4 --- /dev/null +++ b/proposals/NNNN-key-path-offset.md @@ -0,0 +1,173 @@ +# Add an `offset(of:)` method to `MemoryLayout` + +* Proposal: [SE-NNNN](NNNN-key-path-offset.md) +* Authors: [Joe Groff](https://github.com/jckarter) +* Review Manager: TBD +* Status: **Awaiting review** +* Implementation: [apple/swift#15519](https://github.com/apple/swift/pull/15519) + +## Introduction + +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.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](https://forums.swift.org/t/pitch-offsetof-like-functionality-for-stored-property-key-paths/11309/13) + +## Motivation + +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: + +```c +// 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). + +## Proposed solution + +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. + +## Detailed design + +A new API is added to `MemoryLayout`: + +```swift +extension MemoryLayout { + func offset(of key: PartialKeyPath) -> 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: + +```swift +var root: T, value: U +var key: WritableKeyPath +// Mutation through the key path... +root[keyPath: \.key] = value +// ...is exactly equivalent to mutation through the offset pointer... +withUnsafePointer(to: &root) { + (UnsafeMutableRawPointer($0) + MemoryLayout.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: + +```swift +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.offset(of: \.origin.x) // => 0 +MemoryLayout.offset(of: \.origin.y) // => 8 +MemoryLayout.offset(of: \.size.w) // => 16 +MemoryLayout.offset(of: \.size.h) // => 24 +MemoryLayout.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. + +## Source compatibility + +This is an additive change to the API of `MemoryLayout`. + +## Effect on ABI stability + +`KeyPath` objects already encode the offset information for stored properties +necessary to implement this, so this has no additional demands from the ABI. + +## Effect on API resilience + +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. + +## Alternatives considered + +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: KeyPath) -> UnsafePointer { + return (UnsafeRawPointer(self) + MemoryLayout.offset(of: field)) + .assumingMemoryBound(to: Field.self) + } +} + +extension UnsafeMutablePointer { + subscript(field: KeyPath) -> UnsafePointer { + return (UnsafeRawPointer(self) + MemoryLayout.offset(of: field)) + .assumingMemoryBound(to: Field.self) + } + + subscript(field: WritableKeyPath) -> UnsafeMutablePointer { + return (UnsafeMutableRawPointer(self) + MemoryLayout.offset(of: field)) + .assumingMemoryBound(to: Field.self) + } +} +```