"offsetof"-like functionality for stored property key paths

Swift currently lacks a way to ask the compiler for the offset of a stored property in a struct, an important thing for describing the layout of data buffers to graphics and accelerated math libraries. Key paths have to carry offset information when they refer to stored properties, and they seem to me like a natural mechanism for exposing the offset information as API. A couple possible skins for the API would be as a property on KeyPath:

extension PartialKeyPath {
  // Return the offset of a stored property in its struct, or nil if
  // this key path does not refer to a struct stored property
  var offset: Int? { get }
}

struct Point {
  var x, y, z: Double

  var hypotenuse: Double { return sqrt(x*x+y*y+z*z) }
}

(\Point.x).offset // => 0
(\Point.y).offset // => 8
(\Point.z).offset // => 16
(\Point.hypotenuse).offset // => nil

Or as a static method on MemoryLayout to go along with size, alignment, and friends:

extension MemoryLayout {
  func offset(of property: PartialKeyPath<T>) -> Int?
}

MemoryLayout<Point>.offset(of: \.z) // => 16

The offset result would need to optional in the current design since there’s no type-level distinction between stored and computed key paths, but I think the functionality is valuable. What do you all think?

12 Likes

My vote would be as an extension of MemoryLayout. Since in this case it’s already clear from the use of MemoryLayout that you’re talking about nitty-gritty internal representations. If it’s just a property of the keypath API I feel that information is lost.

18 Likes

I like the memory layout option

1 Like

Definitely a useful addition, and MemoryLayout seems the right fit.
Would it have to return nil if querying a non-fixed-layout struct in another module, or would this also work as a way to query the runtime layout of opaque types?

IMO, it should be possible to dynamically query the offsets of fields, since a key path should be the same value regardless of whether it’s formed inside or outside of the properties’ home module.

1 Like

+1 to MemoryLayout.offset(of: PartialKeyPath) -> Int.

How do you plan to confine its usage to a struct? custom diagnostic or return nil?

A non-stored-struct property would return nil.

If it were useful, there could be both inlineOffset and indirectOffset variants, the former for structs and struct-like entities (like tuples) and the latter for class or other boxed fields.

2 Likes

Should it take a full KeyPath<Base, Member> instead? You want to know statically that the MemoryLayout matches the KeyPath.

1 Like

It would be helpful to have more of a design for a reflection system before evaluating this. That said, it does seem to fit with the other things in MemoryLayout, so that seems like a good place for it.

We have layout constraints for guiding specialisation, don’t we? It would be cool if this could be limited to struct types in some way, which would also allow for all the other kinds of offsets you mentioned (indirect offsets for classes, etc).

I’ve wanted this in the past, but the problem I always had with it is, how do you distinguish between offset within a value and offset within a reference? inlineOffset and indirectOffset aren’t enough—\UIView.superview.tag requires a dereference, an offset, another dereference, and another offset.

Maybe you could return an array of offsets, and all but the last one are assumed to require a dereference?

I think it’s best to focus on the inline-offset case first, since I think that’s the most useful case, and a lot simpler than the general case where you’re chasing a path through indirections. Accessing indirected storage will also generally not be as simple as direct storage due to the need for exclusivity and copy-on-write checks, so I’m not sure there’s a clear non-computed path there at all.

PartialKeyPath<Base> is sufficient to ensure that the base type matches the type being offset.

Oops, right, sorry. I forgot that it was the base type preserved and not the member type, even though the latter wouldn’t make any sense.

Thanks everyone. I put up a draft proposal and implementation here: https://github.com/apple/swift-evolution/pull/818

6 Likes