Skip to content

Latest commit

 

History

History
173 lines (137 loc) · 6.41 KB

0210-key-path-offset.md

File metadata and controls

173 lines (137 loc) · 6.41 KB

Add an offset(of:) method to MemoryLayout

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<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

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:

// 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:

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.

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>(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)
  }
}