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.
- Motivation
- Proposed Solution
- Efficiency Requirements
- Detailed Design
- Source Compatibility
- Effect on ABI Stability
- Effect on API Resilience
- Alternatives Considered
The initial implementation of the MemoryLayout
API introduced in this document is available at the following URL: swiftlang/swift#28144
An up-to-date copy of this document (with a revision history) is available at https://gist.github.com/lorentey/71981897bb8637cb060255837730e5d8.
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, orManagedBuffer
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.
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.)
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.
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>?
}
This is an additive change to the Standard Library, with minimal source compatibility implications.
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.
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 direct ivar pointers should be careful not to rely on the stored-ness of properties in types they don't control.
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.
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.
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.