Lifetime dependencies and pointers

I‘ve recently began trying out lifetime dependencies (currently being pitched here: [Pitch #2] Lifetime dependencies for non-`Escapable` values) and non escapable types and I came across an interesting use case I did not see much discussion about. I am not 100% sure though, if this is something that is safe, so I wanted to ask about it here.

struct Box<T>: ~Copyable, ~Escapable {

  private var pointer: UnsafeMutablePointer<T>

  @_lifetime(&value)
  init(value: inout T) {
    pointer = withUnsafeMutablePointer(to: &value) { $0 }
  }
}

Escaping the pointer from withUnsafeMutablePointer is explicitely disallowed in the docs. However, the pointer should have the same lifetime of the inout variable, so theoretically the compiler should prevent access to the pointer in a scope where the inout variable is already gone, and the pointer pointing to invalid memory, right?

The only thing missing is, how someone could safely access the value. Since we already have a pointer, the easiest way would be to expose the pointer via the unsafeAddress and unsafeMutableAddress accessors I‘ve seen being used similarly in InlineArray and Spans.

var value: T {
  unsafeAddress {
    UnsafePointer(pointer)
  }
  @_lifetime(&self) // I think, …
  unsafeMutableAddress {
    pointer
  }
}

The reason I even came to think about such a type was, that it allows to „store“ inout variables in other non escapable types, and allows them to be passed around as long as the lifetimes are correct.

Would someone mind, confirming or denying that this is something that is (or will be when lifetimes are accepted, or formalized) allowed and possible?

Thank you.

1 Like

Important

The pointer created through implicit bridging of an instance or of an array’s elements is only valid during the execution of the called function. Escaping the pointer to use after the execution of the function is undefined behavior. In particular, do not use implicit bridging when calling an UnsafeMutablePointer initializer.

Undefined behavior means that whatever behavior you are observing now is not guaranteed to be the same across different Swift versions or different platforms.

Like how Set/Dictionary uses a different seed for hashing every time your app is run (so iterating through keys/values will have different order), it is possible that there will be some intentional runtime check to avoid invalid uses of unsafe pointers. (To help with catching possible bugs related to passing pointers to C functions where the C function saves the pointer)

1 Like

AFAIU I am escaping the pointer but not beyond the execution of the function by using the lifetime annotations so this should not result in undefined behavior. The compiler simply wouldn‘t allow that, at least this is what I think my example achieves.

The lifetime annotations by themselves don't grant withUnsafe*Pointer any additional abilities. It is still undefined behavior to escape the pointer from the closure body. We don't yet have public API for capturing references to individual values like this. You might be able to use InlineArray<1, T> and MutableSpan<T> as a stand-in in the meantime.

6 Likes

What Joe said. Being inout doesn’t actually give value a stable address, so even if you could safely escape the pointer from the closure (you can’t), there compiler would be free to ignore any updates that happen through it.

This is something that we intend to make easier; @Alejandro has done some early drafts of what API for this might look like. CollectionOfOne and/or InlineArray give a somewhat hacky but viable workaround in the short term.

3 Likes

@Joe_Groff @scanon Thank you for explaining.

Would you mind explaining how InlineArray could be used as a workaround here? I‘m struggling to find a way to pass an inout to it.

You would put the referenced value inside of an InlineArray<1, T>, and use Span and MutableSpan to form references to the single value inside of the InlineArray, as in:

struct Box<T>: ~Copyable, ~Escapable {

  private var pointer: MutableSpan<T>

  @_lifetime(&value)
  init(value: inout InlineArray<1,T>) {
    pointer = value.mutableSpan
  }
}
4 Likes

Ah, I understand. Thank you.

Unfortunately, I don‘t think this lets me achieve what I wanted.

I was working on this PR: Retry & Backoff by ph1ps · Pull Request #364 · apple/swift-async-algorithms · GitHub

… and what bothered me was, that I could not do something like this:

struct FullJitterBackoffStrategy<Base: BackoffStrategy>: BackoffStrategy {
  var generator: inout some RandomNumberGenerator
  var base: Base
  mutating func duration(_ attempt: Int) -> Base.Duration {
    return .init(attoseconds: Int128.random(in: 0...base.duration(attempt).attoseconds, using: &generator)))
  }
}

… instead, I had to kind of extract the randomness into its own protocol conformance, so the user can supply their RNG on their own, per call, like this:

struct FullJitterBackoffStrategy<Base: BackoffStrategy>: BackoffStrategy {
  var base: Base
  mutating func duration(_ attempt: Int) -> Base.Duration {
    return .init(attoseconds: Int128.random(in: 0...base.duration(attempt).attoseconds))
  }
  mutating func duration(_ attempt: Int, using generator: inout some RandomNumberGenerator) -> Base.Duration {
    return .init(attoseconds: Int128.random(in: 0...base.duration(attempt, using: &generator).attoseconds, using: &generator))
  }
}

The only other solution I could think of, is to just take a copy of a RandomNumberGenerator and mutate it internally. But this doesn‘t match the signatures in stdlib, which all take inout RNGs, AFAIK.