Wrapping C structs with @dynamicMemberLookup and friends

Continuing the discussion from Byte-sized Swift: Building Tiny Games for the Playdate:

Good question.

The following is mostly coded here in the Forums, i.e. may not actually compile or execute correctly. I'm just theorising through how one might do this.

Let's start with a specific example line from your code:

if let buffer = playdate.pointee.graphics.pointee.getDisplayFrame() {

Here's a quick adaptation of what I typically use as a starting point:

@dynamicMemberLookup
final class API: @unchecked Sendable {
    let contents: UnsafePointer<PlaydateAPI>

    init(_ contents: UnsafePointer<PlaydateAPI>) {
        self.contents = contents
    }

    subscript<T>(dynamicMember keyPath: KeyPath<PlaydateAPI, T>) -> T {
        contents.pointee[keyPath: keyPath]
    }

    deinit {
        // Here you could free the memory of the pointer,
        // in other circumstances where you take ownership
        // of it in `init`.
    }
}

// So now you have:
if let buffer = playdate.graphics.pointee.getDisplayFrame() {

That gets rid of the first layer of .pointee.. Xcode will even auto-complete the member namesÂą. It should optimise away completely in release builds.

But of course you have the problem that the returned values are all UnsafePointers themselves (based on what I assume is the correct API here - Panic don't appear to provide public access to their SDKs :pensive:).

But (I think) you could specialise that away with an additional overload:

subscript<T>(dynamicMember keyPath: KeyPath<PlaydateAPI, UnsafePointer<T>>) -> T {
    contents.pointee[keyPath: keyPath].pointee
}

// So now you have:
if let buffer = playdate.graphics.getDisplayFrame() {

The big potential problem is that the above is at least nominally making copies of the members. Probably the optimiser would eliminate those, but it'd be nice if there were some way to ensure that, in the way it's written. I don't know if there is, off-hand. Maybe heavy use of @inline(__always) might help?

The alternative would be to instead return a similar @dynamicMemberLookup wrapper for the sub-structs, which will definitely avoid any copies of them, but I'm not sure what happens when you use getDisplayFrame through dynamic member lookup. Arguably it should work, because it should be a lookup of the getDisplayFrame member which is typed as a function (() -> UnsafePointer<UInt8>) which you then simply invoke with (). But I'm not sure if that's how Swift imports the C APIs.

If it doesn't work that way, you could make your own wrappers utilising @dynamicCallable, perhaps.


¹ …sometimes. I'm not certain if it's any less reliable than for any other case, as Xcode definitely sometimes just refuses to auto-complete or offer correct suggestions in general. But I have seen it refuse to see through the dynamic member lookup at random times, seeing only the concrete contents member.

To me, explicit dereferencing of pointers is a feature, and so is the syntactic limitation of "lvalues" to only certain forms like properties and subscripts. If pointee is too long, how about an empty subscript, so you can write playdate[].graphics[].getDisplayFrame()?

1 Like

I'd prefer -> since it's at least well-established for that purpose. Invoking [] makes me think there's a collection / array of some kind involved.

Can you elaborate on why distinct syntax for 'pointers' is desirable? And why that wouldn't apply to other things that are actually pointers, like uses of reference types?

-> is a hack that's necessary in C to handle the mistake of using a prefix operator for dereference (so you'd need to write (*pointer).member without it). .pointee and [] are both postfix so we don't need it.

For class types, the reference itself has no members or operations beyond passing the reference around, and the instance itself can't be copied or moved around, so there's not really any possible overlap in operations on the instance and on the reference, and you can think of the reference "as the instance" to some degree the way Java, Python, and many other languages encourage people to. When working with an explicit pointer, which does have its own operations and a value that's meaningfully separable from the thing being pointed at, I think the distinction between operating on the pointer and on the pointed-to thing is important. If the distinction isn't really important to the API, like if a playdate always has one graphics associated it, then it seems like a better approach would be to make graphics be a computed property that projects through the pointer as an implementation detail, rather than some blanket automatic dereferencing through all pointers, or a hit-or-mess attempt to forward members.

1 Like

@jrose had a take on this operator if I recall correctly...

Highly in favor of empty postfix subscript, use it all the time in my personal projects, but hesitate to recommend it for any public projects unless everyone would recognize it, which would mean putting it in the standard library, and I don’t care about it myself enough to champion it. :sweat_smile: Maybe if I did more pointer chasing at work instead of just in personal projects.

2 Likes

Well, yeah, that's essentially what I proposed. Just, using @dynamicMemberLookup instead of having to manually hard-code every member.

The next level down is very similar - it's just a long list of function pointers, representing the actual API.

It's not entirely clear to me why they did it this way - as opposed to just having the functions be in the global namespace like for most platform APIs - but I assume it's some convenience for testing, or somesuch. But it does make it a lot uglier and more awkward to use, even in native C let-alone Swift.

What is it? Do you mean empty brackets or what?

1 Like

Yeah,

extension UnsafePointer {
  subscript() -> Pointee {
    self.pointee
  }
}

and similar for UnsafeMutablePointer, and maybe AutoreleasingUnsafeMutablePointer. But not Unsafe[Mutable]RawPointer (no default element type, assuming bytes would be weird); no buffer pointers (defaulting to the first element of a collection is weird); no OpaquePointer (no element type).

1 Like

Got you. Although it'd be probably the same as "pointer[0]" (that's available out of the box), no?

Yep. I just think it looks a little nicer, and doesn’t raise the question of “if 0 is valid, is 1 valid?” for fields where it usually isn’t.

This would be ideal:

// NOT SWIFT BELOW
postfix operator ^

struct S {
    struct R { var x = 0 }
    var rawValue = R()
    
    static postfix func ^ (_ this: S) -> R {
        get { this.rawValue }
        set { this.rawValue = newValue }
    }
}

var s = S()
print(s^.x) // âś…
s^.x = 2    // âś…

BTW, could we just provide parameter's default value to the existing one?

  @inlinable
  public subscript(i: Int = 0) -> Pointee {
    @_transparent
    unsafeAddress {
      return UnsafePointer(self + i)
    }
    @_transparent
    nonmutating unsafeMutableAddress {
      return self + i
    }
  }

Seems to work. Or is it a breaking change?

I think it works if you're the standard library, since subscripts can't be referred to directly as values. I tend to add the extension in my own projects outside the standard library.

Postfix ^ is fine, and even precedented in some languages (Pascal), but I personally prefer postfix [] because people are familiar with [i] on pointers already, and because it doesn't require a language change for use as an lvalue.

This reminds me of the Sprong operator ([0]->) which C programmers used on traditional Mac OS to deal with handles. Sensible programmers, of course, worked in Pascal (-: where the postfix indirection operator made accessing a field in a handle easy (^^.).

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

3 Likes

I mean to make this change (from "subscript(i: Int)" to "subscript(i: Int = 0)") in the standard library itself (assuming it's not a breaking change). The benefits:

  • those of us who need pointer[].foo don't have to add subscript() of our own.
  • gradually it becomes less a dialect and more a standard pattern, as it is built-in.
1 Like

@eskimo, thank you for the Sprong operator.

Reading it made me reminisce about my days coding in C in distant past, when I had to use similar methods. I did not know then that it was called the Sprong operator. :slight_smile:

1 Like