Exposing the Memory Locations of Class Instance Variables

Dear Swift folks,

Here is a pitch to plug a small hole in the stdlib API by providing a reliable method to retrieve the address for any directly addressable stored property inside a class instance. The need to work with such addresses comes up particularly often when we try to use synchronization tools to manage concurrency, and it's especially desirable when working with atomic primitives.

This pitch is the first in a series, hopefully culminating in a draft for a memory model for Swift programs that would allow people to start building low-level synchronization tools and some concurrent data structures in pure Swift code. (People interested in peeking at what lies ahead are welcome to check out the work-in-progress atomics implementation at https://github.com/apple/swift/pull/27229. That PR is mostly concentrating on API design work -- drafting a memory model will happen elsewhere, most likely in a separate topic on this forum.)


Exposing the Memory Location of Class Instance Variables

Introduction

We propose to enable Swift code to retrieve the memory location of any directly addressable stored property in a class instance as an UnsafeMutablePointer value.

The initial implementation of the MemoryLayout API introduced in this document is available at the following URL: https://github.com/apple/swift/pull/28144

An up-to-date copy of this document (with a revision history) is available at https://gist.github.com/lorentey/71981897bb8637cb060255837730e5d8.

Motivation

For Swift to be successful as a systems programming language, it needs to allow efficient use of the synchronization facilities provided by the underlying computer architecture and operating system, such as primitive atomic types or higher-level synchronization tools like pthread_mutex_t or os_unfair_lock. Such constructs typically require us to provide a stable memory location for the values they operate on.

Swift provides a number of language and runtime constructs that are guaranteed to have a stable memory location. (Incidentally, concurrent access to shared mutable state is only possible through memory locations such as these.) For example:

  • Dynamic variables manually created by Swift code (such as through allocate/initialize methods on unsafe pointer types, or ManagedBuffer APIs) inherently have a stable memory location.

  • Class instances get allocated a stable memory location during initialization; the location gets deallocated when the instance is deinitialized. Individual instance variables (ivars) get specific locations within this storage, using Swift's class layout algorithms.

  • Global variables and static variables always get associated with a stable memory location to implement their storage. This storage may be lazily initialized on first access, but once that's done, it remains valid for the entire duration of the Swift program.

  • Variables captured by an (escaping) closure get moved to a stable memory location as part of the capture. The location remains stable until the closure value is destroyed.

  • The stdlib provides APIs (such as withUnsafePointer(to:_:)) to pin specific values to some known memory location for the duration of a closure call. This sometimes reuses the existing storage location for the value (e.g., if these APIs are called on a directly accessible global variable), but this isn't guaranteed -- if the existing storage happens to not be directly available, these APIs silently fall back to creating a temporary location, typically on the stack.

However, Swift does not currently provide ways to reliably retrieve the address of the memory location backing these variables -- with the exception of dynamic variables, where all access is done through an explicit unsafe pointer whose value is (obviously) known to the code that performs the access.

Therefore, in current Swift, constructs that need to be backed by a known memory location can only be stored in dynamically allocated memory. For example, here is a simple "thread-safe" integer counter that uses the Darwin-provided os_unfair_lock construct to synchronize access:

(We can wrap POSIX Thread mutexes in a similar manner; we chose os_unfair_lock for this demonstration to minimize the need for error handling.)

class Counter {
    private var _lock: UnsafeMutablePointer<os_unfair_lock_s>
    private var _value: Int = 0
    
    init() {
        _lock = .allocate(capacity: 1)
        _lock.initialize(to: os_unfair_lock_s())
    }
    
    deinit {
        _lock.deinitialize(count: 1)
        _lock.deallocate()
    }
    
    private func synchronized<R>(_ body: () throws -> R) rethrows -> R {
        os_unfair_lock_lock(_lock)
        defer { os_unfair_lock_unlock(_lock) }
        return try body()
    }
    
    func increment() {
        synchronized { _value += 1}
    }
    
    func load() -> Int {
        synchronized { _value }
    }
}

Having to manually allocate/deallocate memory for such constructs is cumbersome, error-prone and inefficient. We should rather allow Swift code to use inline instance storage for this purpose.

To enable interoperability with C, the Swift Standard Library already provides APIs to retrieve the memory location of a class instance as an untyped UnsafeRawPointer value:

class Foo {
    var value = 42
}

let foo = Foo()
let unmanaged = Unmanaged.passRetained(foo)
let address = unmanaged.toOpaque()
print("foo is located at \(address)") // ⟹ foo is located at 0x0000000100500340
unmanaged.release()

However, there is currently no way to reliably retrieve the memory location for individual stored variables within this storage.

SE-0210 introduced a MemoryLayout.offset(of:) method that can be used to determine the layout offset of a stored variable inside a struct value. While this method doesn't work for classes, we can use it to guide the design of new API that does.

Proposed Solution

We propose to add an API that returns an UnsafeMutablePointer to the storage behind a directly addressable, mutable stored property within a class instance:

extension MemoryLayout where T: AnyObject {
  static func unsafeAddress<Value>(
    of key: ReferenceWritableKeyPath<T, Value>,
    in root: T
  ) -> UnsafeMutablePointer<Value>?
}

If the given key refers to a stored property within the in-memory representation of root, and the property is directly addressable (in the sense of SE-0210), then the return value is a direct pointer to the memory location implementing its storage.

Accessing the pointee property on the returned pointer is equivalent to the same access of the instance property itself (which is in turn equivalent to access through the corresponding key path):

class Foo {
    var value = 42
}

let foo = Foo()

// The following groups of statements all perform the same accesses
// on `foo`'s instance variable:

print(foo.value) // read
foo.value = 23   // assignment
foo.value += 1   // modification

print(foo[keyPath: \.value]) // read
foo[keyPath: \.value] = 23   // assignment
foo[keyPath: \.value] += 1   // modification

withExtendedLifetime(foo) {
  let p = MemoryLayout.unsafeAddress(of: \.value, in: foo)!
  print(p.pointee)  // read
  p.pointee = 23    // assignment
  p.pointee += 1    // modification
}

Note the use of withExtendedLifetime to make sure foo is kept alive while we're accessing its storage. To rule out use-after-free errors, it is crucial to prevent the surrounding object from being deallocated while we're working with the returned pointer. (This is why this new API needs to be explicitly tainted with the unsafe prefix.)

Note also that the Law of Exclusivity still applies to accesses through the pointer returned from unsafeAddress(of:in:). Accesses to the same instance variable (no matter how they're implemented) aren't allowed to overlap unless all overlapping accesses are reads. (This includes concurrent access from different threads of execution as well as overlapping access within the same thread -- see SE-0176 for details. The compiler and runtime environment may not always be able to diagnose conflicting access through a direct pointer; however, it is still an error to perform such access.)

We can use this new API to simplify the implementation of the previous Counter class:

final class Counter {
    private var _lock = os_unfair_lock_s()
    private var _value: Int = 0
    
    init() {}
    
    private func synchronized<R>(_ body: () throws -> R) rethrows -> R {
        let lock = MemoryLayout<Counter>.unsafeAddress(of: \._lock, in: self)!
        os_unfair_lock_lock(lock)
        defer { os_unfair_lock_unlock(lock) }
        return withExtendedLifetime(self) { try body() }
    }
    
    func increment() {
        synchronized { value += 1}
    }
    
    func load() -> Int {
        synchronized { value }
    }
}

(Note that the functions os_unfair_lock_lock/os_unfair_lock_unlock cannot currently be implemented in Swift, because we haven't formally adopted a memory model yet. Concurrent mutating access to _lock within Swift code will run afoul of the Law of Exclusivity. Carving out a memory model that assigns well-defined semantics for certain kinds of concurrent access is a separate task, deferred for future proposals. For now, we can state that Swift's memory model must be compatible with that of C/C++, because Swift code is already heavily relying on the ability to use synchronization primitives implemented in these languages.)

Efficiency Requirements

To make practical use of this new API, we need to ensure that the unsafeAddress(of:in:) invocation is guaranteed to compile down to a direct call to the builtin primitive that retrieves the address of the corresponding ivar whenever the supplied key is a constant-evaluable key path expression. (Ideally this should work even in unoptimized -Onone builds.) Creating a full key path object and iterating through its components at runtime on each and every access would be prohibitively expensive for the high-performance synchronization constructs that we expect to be the primary use case for this new API.

This optimization isn't currently implemented in our prototype PR, but we expect it will be done as part of the process of integrating the new API into an actual Swift release.

For now, to enable performance testing, we recommend caching the ivar pointers in (e.g.) lazy stored properties:

final class Counter {
  private var _lockStorage = os_unfair_lock_s()
  private var _value: Int = 0

  private lazy var _lock: UnsafeMutablePointer<os_unfair_lock_s> =
    MemoryLayout<Counter>.unsafeAddress(of: \._lockStorage, in: self)!

  private func synchronized<R>(_ body: () throws -> R) rethrows -> R {
    os_unfair_lock_lock(_lock)
    defer { os_unfair_lock_unlock(_lock) }
    return try withExtendedLifetime(self) { try body() }
  }

  func increment() {
    synchronized { _value += 1}
  }

  func load() -> Int {
    synchronized { _value }
  }
}

This wastes some memory, but has performance comparable to the eventual implementation.

Detailed Design

extension MemoryLayout where T: AnyObject {
  /// Return an unsafe mutable pointer to the memory location of
  /// the stored property referred to by `key` within a class instance.
  ///
  /// The memory location is available only if the given key refers to directly
  /// addressable storage within the in-memory representation of `T`, which must
  /// be a class type.
  ///
  /// A class instance property has directly addressable storage when it is a
  /// stored property for which no additional work is required to extract or set
  /// the value. Properties are not directly accessible if they are potentially
  /// overridable, 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.
  ///
  /// For example, in the `ProductCategory` class defined here, only
  /// `\.updateCounter`, `\.identifier`, and `\.identifier.name` refer to
  /// properties with inline, directly addressable storage:
  ///
  ///     final class ProductCategory {
  ///         struct Identifier {
  ///             var name: String              // addressable
  ///         }
  ///
  ///         var identifier: Identifier        // addressable
  ///         var updateCounter: Int            // addressable
  ///         var products: [Product] {         // not addressable: didSet handler
  ///             didSet { updateCounter += 1 }
  ///         }
  ///         var productCount: Int {           // not addressable: computed property
  ///             return products.count
  ///         }
  ///         var parent: ProductCategory?  // addressable
  ///     }
  ///
  /// When the return value of this method is non-`nil`, then accessing the
  /// value by key path or via the returned pointer are equivalent. For example:
  ///
  ///     let category: ProductCategory = ...
  ///     category[keyPath: \.identifier.name] = "Cereal"
  ///
  ///     withExtendedLifetime(category) {
  ///       let p = MemoryLayout.unsafeAddress(of: \.identifier.name, in: category)!
  ///       p.pointee = "Cereal"
  ///     }
  ///
  /// `unsafeAddress(of:in:)` returns nil if the supplied key path has directly
  /// accessible storage but it's outside of the instance storage of the
  /// specified `root` object. For example, this can happen with key paths that
  /// have components with reference semantics, such as the `parent` field
  /// above:
  ///
  ///     MemoryLayout.unsafeAddress(of: \.parent, in: category) // non-nil
  ///     MemoryLayout.unsafeAddress(of: \.parent.name, in: category) // nil
  ///
  /// - Warning: The returned pointer is only valid until the root object gets
  ///   deallocated. It is the responsibility of the caller to ensure that the
  ///   object stays alive while it is using the pointer. (The
  ///   `withExtendedLifetime` call above is one example of how this can be
  ///   done.)
  ///
  ///   Additionally, the Law of Exclusivity still applies: the caller must
  ///   ensure that any access of the instance variable through the returned
  ///   pointer will not overlap with any other access to the same variable,
  ///   unless both accesses are reads.
  @available(/* to be determined */)
  public static func unsafeAddress<Value>(
    of key: ReferenceWritableKeyPath<T, Value>,
    in root: T
  ) -> UnsafeMutablePointer<Value>?
}

Source Compatibility

This is an additive change to the Standard Library, with minimal source compatibility implications.

Effect on ABI Stability

Key path objects already encode the information necessary to implement the new API. However, this information isn't exposed through the ABI of the stdlib as currently defined. This implies that the new runtime functionality defined here needs to rely on newly exported entry points, so it won't be back-deployable to any previous stdlib release.

Effect on API Resilience

As in SE-0210, 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 unsafeAddress(of:in:), 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

While we could have added the new API as an extension of ReferenceWritableKeyPath, the addition of the root parameter makes that option even less obvious than it was for offset(of:). We agree with SE-0210 that MemoryLayout is the natural place for this sort of API.

Generalizing offset(of:)

We considered extending the existing offset(of:) method to allow it to return an offset within class instances. However, the means of converting an offset to an actual pointer differ between struct and class values, and using the same API for both is likely to lead to confusion.

let foo = Foo()
let offset = MemoryLayout<Foo>.offset(of: \.value)!

// if Foo is a struct:
withUnsafeBytes(of: foo) { buffer in 
    let raw = buffer.baseAddress! + offset
    let p = raw.assumingMemoryBound(to: Int.self)
    print(p.pointee) 
}

// if Foo is a class:
withExtendedLifetime(foo) { 
    let raw = Unmanaged.passUnretained(foo).toOpaque() + offset
    let p = raw.assumingMemoryBound(to: Int.self)
    print(p.pointee)
}

We also do not like the idea of requiring developers to perform fragile raw pointer arithmetic just to access ivar pointers. The proposed API abstracts away these details.

Closure-Based API

Elsewhere in the Standard Library, we prefer to expose unsafe "inner" pointers through APIs that take a closure. For example, SE-0237 added the following customization point to Sequence:

protocol Sequence {
  public mutating func withContiguousMutableStorageIfAvailable<R>(
    _ body: (inout UnsafeMutableBufferPointer<Element>) throws -> R
  ) rethrows -> R?
}

At first glance, this looks very similar to the ivar case. In both cases, we're exposing a pointer that has limited lifetime, and arranging the client code into a closure helps avoiding lifetime issues.

It would definitely be possible to define a similar API here, too:

extension MemoryLayout where T: AnyObject {
  public static func withUnsafeMutablePointer<Value, Result>(
    to key: ReferenceWritableKeyPath<T, Value>,
    in root: T,
    _ body: (UnsafeMutablePointer<Value>) throws -> Result
  ) rethrows -> Result?
}

However, the ivar usecase is something of a special case that makes this approach suboptimal.

In the Sequence.withContiguousMutableStorageIfAvailable case, it is strictly illegal to save the pointer for later access outside the closure -- Sequence implementations are allowed to e.g. create a temporary memory location for the duration of the closure, then immediately destroy it when the closure returns. There is no way to (reliably) hold on to the storage buffer.

In contrast, the memory location (if any) of a class ivar is guaranteed to remain valid as long as there is at least one strong reference to the surrounding object. There is no need to artifically restrict the use of the ivar pointer outside the duration of a closure -- indeed, we believe that the guaranteed ability to "escape" the pointer will be crucially important when we start building abstractions on top of this API.

In the primary usecase we foresee, the pointer would get packaged up with a strong reference to its object into a standalone construct that can fully guarantee the validity of the pointer. Given that this usecase is deliberately escaping the pointer, it seems counter-productive to force access through a closure-based API that discourages such things.

48 Likes

This is fan-tas-tic, Karoy! :clap:

Unlocks all the future use cases (that you nicely preview in 27229, can't wait for those :slight_smile:) we have in mind... Needless to say, great addition to the language and esp. for server use cases once the follow-ups land.

I like the design, spelling as well as putting it onto MemoryLayout as well, very natural fit and reads very well.

I'd like to +1 that it would indeed be nice to always have the keypath dance avoided in runtime. For the types of structures we'd like to build with these things the keypaths are likely (though would need to verify) painful overhead even in debug builds.

3 Likes

Yay! :slight_smile:

4 Likes

Great presentation of the concepts.

Well-conceived substance.

Well-considered alternatives.

I like that the API differs between classes and structs. At first, I thought unification would be better, but they are different animals.

Thank you.

4 Likes

This is really awesome, I love the approach - thank you for tackling this!

6 Likes

This is great, @lorentey, I love this proposal. I have one question:

I have a vague recollection that lazy ivars are not thread safe. It seems like storing the pointer to the storage in such an ivar is not good practice, even though in this context it may behave as intended. Perhaps the recommendation should be to compute this in the initializer?

It's exciting to see work in this direction happening! Thank you @lorentey!

+1

I’ve thought we needed something in this space for a long, long time. Bravo.

But...

Why not implement this safely-packaged version of the functionality ourselves? Or implement the with* version of it while documenting that it’s okay to escape the pointer as long as you keep the base alive? An unsafe-by-default API seems like it’s inviting mistakes.

1 Like

Thanks for addressing this important problem! I don't think this is quite the right approach, though, because fundamentally the storage you want to allocate isn't a property from Swift's perspective. The exclusivity expectations are fundamentally at odds with concurrency primitives like locks. And in what little memory model Swift does have, it is not allowed to mix formal accesses with accesses through pointers; our existing escapes like withUnsafe*Pointer must be used mutually exclusively with the property that's "locked" by the with. This means that, if you need raw memory inline in a class instance, and you allocated that storage using a property, it would pretty much always always a bug to access that property directly. In other words, in:

final class Counter {
    private var _lock = os_unfair_lock_s()
}

It would never be correct to access self._lock, because it might copy, and it would assert exclusive access on the storage and interleave formal accesses with raw pointer accesses.

I think it would be better to expose raw storage inside a class instance as just that, raw storage, and make the pointer the primary interface to the storage, since that's what you want anyway. We could express this as something that looks like a property wrapper, maybe:

final class Counter {
    @RawStorage private var _lock: UnsafeMutablePointer<os_unfair_lock_s>
}

That makes it much harder to misuse, because there's no way to accidentally access the storage as a property, and also means we don't have to expose similarly brittle APIs for getting pointers to arbitrary class properties. We can also relax the requirements on backing storage allocated this way so that it isn't subject to interference by exclusivity semantics, making it more appropriate for building low-level primitives or interfacing with existing ones.

13 Likes

Thanks so much @lorentey, I'm very strongly +1 on this. And for me, often doing some low-level, performance focussed programming, this is one of the most important proposals ever.

That's exactly the plan -- but I felt important to first introduce the primitive on which the safe(r) construct would be built on. The @Anchoring property wrapper is going to be running into some pretty hard limits on what's reasonably achievable in this area on the library level; I expect (hope!) it will spur people to design something better.

This pitch highlights some lower-level issues that deserve serious consideration, and I don't want to distract attention from those by introducing them at the same time as a brand new, somewhat shaky abstraction.

If the followup abstraction is successful enough, we may well end up not needing to expose this API as public. (I think that would in fact be the outcome I prefer.)

I was wrestling with this very question for days. What put me over the edge is that I think it would be unwise to teach people that it's sometimes okay to escape pointers out of closure-scoped pointer APIs. I prefer to add one unsafe API instead of slightly weakening a general pattern.


We could in theory have the new MemoryLayout API return a "safe" pointer:

struct AnchoredPointer<Pointee> {
  private let _anchor: AnyObject
  private let _pointer: UnsafeMutablePointer<Pointee>

  init(_ pointer: UnsafeMutablePointer<Pointee>, in anchor: AnyObject) {
    self._anchor = anchor
    self._pointer = pointer
  }

  func withScopedAddress<Result>(
    _ body: (UnsafeMutablePointer<Pointee>) throws -> Result
  ) rethrows -> Result {
    try withExtendedLifetime { 
      try body(_pointer)
    }
  }
}

extension MemoryLayout where T: AnyObject {
  func anchoredPointer<Value>(
    to key: ReferenceWritableKeyPath<T, Value>,
    in root: AnyObject
  ) -> AnchoredPointer<Value> { ... }

But this doesn't seem like particularly useful API on its own. (It would need to be wrapped into constructs that give it the high-level operations that are actually useful.) I'm especially worried about the retain/release traffic around _anchor -- which would clearly be unacceptable for atomic operations. I have a hare-brained scheme to try eliminating this overhead in the upcoming @Anchoring pitch, but I don't think we can reasonably do that if the underlying primitive insists on returning a strong reference.

This is the sort of thing that it'd be nice to have explicit invocation of accessor coroutines for. If you were forced/strongly encouraged to access the property in a scoped access, using strawman syntax like this:

with lockAddr = self.anchoredProperty {
  use(lockAddr)
}

where anchoredProperty would be implemented using a read coroutine, and with would force the body of the with statement to be run during the coroutine access, then that should naturally establish a lifetime relationship between self and the pointer provided by anchoredProperty without explicit retain/release traffic.

Without coroutines, it seems like a with*-style higher order function could still manage this, maybe exploiting coroutines under the surface.

SIL does have an instruction for introducing ad-hoc lifetime dependencies between values. Another possibility could be that we expose that to the standard library as a builtin, to increase the likelihood that naive uses of the storage pointer are bound to the lifetime of the containing object. The effectiveness of that approach would be limited by how far the optimizer inlines, though, so you'd still need withExtendedLifetime if you're returning the pointer up the callstack.

4 Likes

@lorentey Thanks so much for putting this together. Very happy to see the development in this area and very much looking forward to the nice things we can build on top.

1 Like

Agreed. However, the Law of Exclusivity, as introduced in SE-0176, doesn't care what form an access takes, or what sort of variable it touches -- overlapping mutating access is currently strictly verboten in Swift. Restricting such access to dynamic variables doesn't keep us out of jail at all -- they too are illegal under the Law.

Obviously, this cannot stand; we need to be able to deal with concurrently mutable shared state in Swift, even if we'd only use this to provide a higher-level concurrency model (such as actors) to Swift applications. In order to do this, we need to punch carefully shaped holes into the Law of Exclusivity; specifically, we need to seriously start talking about variables, memory locations, atomic operations and memory orderings.

Swift code is already using instance/global/static/captured/dynamic variables to represent mutable state shared across concurrent threads of execution -- it calls out to C & C++ code (such as Dispatch) to try escaping the Law, but this practice arguably still violates it. To wit, it implicitly assumes that C++'s memory orderings also apply to Swift code. (We need to explicitly state that this is indeed the case. The first step to doing that is that we need to admit that certain language constructs are mapped to specific memory locations, so that we can understand how to apply memory orderings to them. This pitch is really just an excuse to start talking about this.)

The holes we need to punch into the Law of Exclusivity will need to include allowances for overlapping calls to certain primitive atomic operations (i.e., we'll need to add a new "atomic" access to go with the current "read"/"assign"/"modify" set). I would find it weirdly asymmetric if this new access would be artificially restricted to dynamic variables, while the existing ones are defined on all sorts.

I may be missing something here, but as I understand it, overlapping read accesses are explicitly okay, no matter what form they take. So this code is perfectly fine:

class Foo { var value: Int = 42}

let foo = Foo()

// thread A
print(foo.value)

// thread B
print(foo[keyPath: \.value])

// thread C
withUnsafePointer(to: foo.value) { print($0.pointee) }

(Even assuming withUnsafePointer would provide the actual address of the ivar.)

Objection, your honor! There are two places in particular where access through the property syntax is not just correct, but also highly desirable: during init and deinit.

These are guaranteed to be outside of any overlapping access, and in fact going through the usual exclusivity checks may provide an extra layer of protection against lifetime issues. (If nothing else, TSAN could use the exclusivity assertion as an input signal.)

I did seriously consider this approach, but I found it raises more questions than it answers.

  • How would this storage get initialized? We don't want to lose the statically enforced guarantee that all ivars must get created before init returns.
  • How would this storage get deinitialized? Some synchronization constructs will definitely include strong references, and these should really just behave like regular strong references when it comes to deinitialization. (We'll introduce an atomically initializable lazy reference in the first batch, and I'm pretty sure we'll also build a full AtomicReference type when we add double-wide atomics. )
  • How would @RawStorage compose into other language features? How would it enable us to provide an ergonomic but lightweight UnsafeLock construct in the stdlib (or some concurrency module)?

The last point is essential -- we do not want to force developers to carefully manage initialization/deinitialization of every single synchronization construct, or to litter unsafe pointers throughout their code.

APIs like pthread_mutex_destroy will cause problems anyway, since property wrappers cannot currently hook into class deinitialization. However, there is a large swath of constructs that can be modeled with a general-purpose property wrapper built around unsafeAddress(of:in:).

Yes yes yes! If I understand correctly, my hare-brained scheme is going to try doing something very much like that.

class Foo {
  @Anchoring var counter: AtomicInt
}

let foo = Foo()

foo.counter.wrappingIncrement()
print("Current value: \(foo.counter.load())")

The @Anchoring property wrapper would yield (not return) the AtomicInt struct (which would effectively be a wrapper around AnchoredPointer), and this would hopefully be a constrained enough setup that we could add an optimization that gets rid of the retain/release in the common case where the yielded value doesn't escape the coroutine, and the operation doesn't call isKnownUniquelyReferenced(). (As long as everything is fully @inlinable and @frozen).

We'd still rely on the strong reference inside the AtomicInt struct (in its full retain/release glory) in case someone copies the value into, say, a local variable (which we cannot practically prevent in today's Swift).

let myQuestionableCopy = foo.counter

myQuestionableCopy.wrappingIncrement() // This needs to be safe

I'm working on formalizing the @Anchoring pitch as quickly as I can. (As always, writing these things is exquisite agony.)

2 Likes

The Law of Exclusivity only governs operations that fall under its formal model, like properties and variables. Things like raw memory accesses are mostly outside of the model, and don't rest on the semantics of any language-managed storage, which is why you can get away with putting locks in raw allocated memory, doing atomic operations on them, etc., and why things like withUnsafePointer are so restricted in how they allow managed things to be temporarily treated like raw memory. I think we should avoid involving the mechanics of regular stored properties in this mechanism as much as we possibly can, because it leads to a much more understandable semantic model—you just have the "usual" pitfalls of raw memory access, without an additional layer of higher-level semantic guarantees.

This is of a different nature to what you're proposing, though; the sequencing semantics we're getting by calling out to C happen around Swift formal accesses, and synchronize on entities that are not managed by Swift's semantics such as queues or locks stored on raw memory.

We try really hard not to saddle regular high-level code with unnecessary low-level semantics that are irrelevant to most people by default. We don't want to fall into the trap C and C++ have where they try to have their low-level cake and eat their high-level optimizations too with rules that are overly complex and satisfy nobody. By all means, objects are already effectively tied to a memory address, and it makes sense to be able to allocate unmanaged storage with a similarly-fixed address inline inside objects, but I think getting that sort of raw storage ought to be something opt-in on the storage and not something that potentially any property can be turned into.

When we get move-only types, atomic accesses should slot into the existing model naturally like they do in Rust, where atomic mutation operations apply to nonexclusively read-borrowed references.

Well-typed nonatomic reads are probably practically fine, but you would need more than that for anything where you'd want to use this feature. It's a bit tricky, though, because the compiler can generally copy its way out of situations where formal accesses might interfere with reads. We're more than likely going to copy foo.value to a temporary in this example so that we don't block off writes to foo.value during the withUnsafePointer block.

I think there are valid arguments in the other direction, that you want to be able to initialize/deinitialize the storage on your own terms in init/deinit without the usual restrictions on initializing properties. There could be other use cases for this feature where the object wants to allocate some raw storage that it leaves uninitialized in some conditions. Ideally we could obsolete at least some uses of ManagedBuffer for that purpose with this feature.

If you have a pointer to the storage, you can still use UnsafeMutablePointer's .initialize()/.destroy() methods to initialize and destroy the storage as if it were a regular property.

I don't think you can get there until we get move-only types. But it seems like you could associate RawStorage with a protocol for initializing types from pointers, like:

protocol PointerToStorage {
  associatedtype Storage

  init(storage: UnsafeMutablePointer<Storage>)
}

struct UnsafeLock: PointerToStorage {
  var storage: UnsafeMutablePointer<pthread_mutex_t>
}

It would also make sense to me to allow RawStorage inside structs, and have the same guarantees about the raw storage inside the struct when the struct itself is transitively allocated in RawStorage, which would allow you to compose other wrappers around it.

4 Likes

I argue that a dynamic variable created by allocating and initializing storage through an UnsafeMutablePointer isn't any less a variable than an ivar -- it must have just as well-defined semantics. The Law of Exclusivity must necessarily apply to it, like it does to all variables:

UnsafeMutablePointer.pointee is mutable memory. Array.subscript's addressor returns mutable memory. There is nothing truly raw about these -- it's true they have their own unique flow for creation/destruction, but so does every other form of variable.

(It's also true that exclusivity violations through unsafe pointers typically won't get diagnosed in regular production use. I don't see how this is a problem: these pointers are marked unsafe for a reason.)

There is a distinction to be made between variables that are backed with a stable memory location and variables that aren't. (E.g., the latter inherently cannot participate in any concurrent access.)

My point is that sadly this ship has already sailed. Swift encourages people to synchronize access on shared mutable state using C and C++ constructs, so Swift has de facto adopted the C++ concurrency model, including the scary bits about dependency-ordered-before this and inter-thread-happens-before that. We just neglected to specify exactly what this means -- which is a standing invitation to concurrency bugs.

How can os_unfair_locks or dispatch queues (or indeed, the existing internal atomic operations in the stdlib) be safe to use in Swift code without specifying how (say) a regular assignment to an everyday Swift closure capture variable interacts with a subsequent releasing store to an os_unfair_lock?

I don't think it's viable for us to keep being coy about how sharable variables (such as class ivars) work. By merely observing that Swift apps have been able to successfully use locks like os_unfair_lock to synchronize access to shared data stored in these language constructs, and knowing nothing whatsoever about the inner workings of the Swift compiler, we can already deduct that

  1. Shared variables must be associated with a memory location (because addressable memory is the only way to share mutable state across threads).
  2. All participating threads agree on the location of this memory (or it wouldn't behave like a shared variable at all).
  3. The last value assigned to a shared variable that precedes an unlock operation on the lock is guaranteed to get translated to a corresponding store to its known memory location, and that store is guaranteed to get executed before the unlock operation issues its releasing atomic store on the mutex variable. (Otherwise locks wouldn't work.)
  4. The first read of any shared variable that follows a lock operation (which we know issues an acquiring load on the mutex variable) must be translated into a regular load operation on the variable's memory location. (Otherwise locks wouldn't work.)

So class ivars, closure captures, global variables, etc. evidently do all have stable addresses and their accesses must be following a sensible memory model that is compatible with the memory orderings defined by all those smart people in C++ land. Again, all this follows from the mere fact that locks imported from C APIs demonstrably work in Swift.

I sincerely hope none of this is surprising or controversial!

(...is it?) :worried:

I wholeheartedly agree with this last part. Let's make the ivar thing opt-in, as long as we can agree on a nice way to implement it. (The API in this pitch is not critical to make public -- I have no objection to limiting its (direct) use to the stdlib, as long as we provide safer abstractions that provide the same benefits. We do badly need to access specific ivar locations within the stdlib.)

But I really really wouldn't want to treat synchronization constructs as unmanaged raw storage. These things aren't built out of special artisanal bits that are only meaningful to C++ code: they contain regular everyday types that we already model in the stdlib. They should be (and already are!) initialized/destroyed as regular Swift values. os_unfair_lock_s is imported as a boring struct holding an integer ivar; it's no more special than, say, NSRange is.

The only thing that makes these types special is the operations they expose for use between their init/deinit. Given that Swift is a "high-performance system programming language", my expectation is that I should be able to implement these operations directly in Swift, and if anything, this should be less difficult than it is in C++. Requiring people to mess around with raw pointers is definitely not the right direction for that.

I agree that non-copyable types would be the appropriate abstraction to represent these constructs. Unfortunately, we aren't currently modeling them in Swift. On the other hand, just as unfortunately, we're badly overdue for adding usable atomics, and I think it would be a mistake to delay it further.

4 Likes

One benefit of having this opt-in and explicit is that it would be possible to clearly define its "API resilience", and not relying of having the user of such field guess if it will work and have to check for every usages.

If this pitch is only about workaround current Swift limitation, I prefer this API to remain private, so we can replace it by the proper construct later.

I have assumed for a couple years now that native atomics would only happen after move-only types arrived. @lorentey and @Joe_Groff appear to agree that move-only types would be the better model.

This raises the question: do we have an idea how far away are move-only types?

What are other uses for the functionality pitched here? I personally have only wanted/needed this for atomics.

Another question, especially to Karol and Joe: if this is "only" about a workaround and it were to go through as an API private to the standard library, would it prevent a public atomics implementation from landing in the standard library? If not, would such an implementation be hamstrung later on?

Terms of Service

Privacy Policy

Cookie Policy