A lifetime-dependent property (or "dependent property") is a struct or class property whose value can only be accessed within an enclosing scope, during which the parent object cannot be deinitialized. A dependent property can provide a lightweight "view" of its parent object's underlying storage through an alternate type. This proposal focuses on the problem of returning the view object as a property of its "container". The author of the view type still bears responsibility for other aspects of safety. The Future Directions sections cover additional features needed to support development of safe view types.
A container data type often needs to provide limited access to its underlying storage to a more general purpose API that consumes the data but is otherwise independent of the data type. In Swift, this is typically done by having the container provide a "slice" that conforms to the Collection protocol. Since slices depend on their container's storage, they need to keep their container alive by retaining a hidden reference. This incurs reference counting traffic, makes it impossible to reason about the container's lifetime independent of its slices, and is fundamentally incompatible with unmanaged storage. Developers who want to efficiently abstract over arbitrary storage need to drop down to unsafe pointer types:
func decode(buffer: UnsafeRawBufferPointer) {
// ... decode buffer
}
let array: [UInt8] = ...
array.withUnsafeBytes {
decode(buffer: $0)
}
let tuple: (UInt8, UInt8) = ...
withUnsafeBytes(of: tuple) {
decode(buffer: $0)
}
Views can be used instead to avoid multiple dangers inherent to unsafe pointers: use-after-free, out-of-bounds, type confusion, and simultaneous modification.
The [BufferView proposal (TBD)] provides specific examples of putting views into practice. BufferViews have a concrete representation that supports efficient access without requiring specialization on the parent container. This allows storage to be passed safely and efficiently across module boundaries.
In summary, views
- allow existing unsafe APIs to be replaced with safe and efficient APIs
- enable more efficient storage techniques, such as stack allocation
- support generalized, safe, efficient communication across module boundaries
A new language feature is needed to ensure that when a container provides a view as a property, that view does escape the corresponding access to the container.
A container type can safely provide a view of its underlying storage through a dependent property. The dependent property can refer back to its parent's underlying storage. The view returned by a dependent property is limited to a lexical scope, so the affect on its container's lifetime is locally, statically predictable. The view may be copied, passed to another function, and stored in an aggregate, but none of the copies can extend the container's lifetime beyond it's original scope. Although views abstractly reference their container, there's no need for them to retain references. The parent need not even be a reference counted object. In fact, views imposes no particular form of memory management on their storage.
A dependent property provides a consistent "view" of its parent's storage by enforcing exclusive access over the scope of the view. A mutable view requires exclusive access, so the storage is only modified by the view itself or a copy of that view. An immutable view prevents exclusive access, so the storage cannot be modified in the view's scope. SE-0176: Enforce Exclusive Access to Memory explains exclusivity in detail.
A dependent property requires introducing exclusivity and lifetime constraints on the value it returns. We propose adding a single @dependent
attribute to property declarations that provides both constraints and ties them to the same "access scope". In "Future directions", we discuss inferring this attribute from the property's type. This attribute is only valid for properties implemented using read or modify accessor coroutines.
class Storage { ... }
struct Container {
private var _storage: Storage
var view: @dependent View {
_read {
yield View(_storage)
}
}
var mutableView: @dependent MutableView {
_modify {
var view = MutableView(_storage)
yield &view
}
}
}
Accessors already establish an access scope in the caller. This scope normally only covers the expression that computes the property's value. A copy of that value is then passed along without exclusivity:
let v = container.view
// exclusive access to container.view is released here, before 'v' goes out of scope
lookAt(view: v)
The @dependent
attribute tells the compiler to extend the access scope across the lexical scope that immediately encloses the access:
let v = container.view
lookAt(view: v)
// exclusive access to container.view is released here, after 'v' is no longer used.
If a dependent property's value is assigned to a variable (such as 'v' above), then, from that point onward, that variable is only available within the lexical scope of the access:
let v: View?
if let container = container {
v = container.view
lookAt(view: v) // this use is ok
}
lookAt(view: v) // this use raises a compiler error
This lexical scope rule is enforced by the "nonescaping values" feature described below. The compiler may optimize the variable's lifetime using the usual lifetime rules. The access scope, however, must always extend beyond the last use of the variable. If destroying the variable results in deinitialization, then the deinitializer runs within the access scope.
If the dependent accessor is used in a subexpression, its scope now covers the entire expression. Here, the exclusivity scope covers all transformations applied by map
:
container.view.map { ... }
View types may provide their own properties that expose the same underlying storage. To avoid escaping that storage, the author of the view type must transitively declare all such properties @dependent
. Here, the iterator's exclusivity scope is nested within the view's scope. Both extend past the last use of i
:
let i = container.view.iterator
By extending the scope of the accessor, dependent properties naturally extend the lifetime of the parent object. This means that the container's storage can neither be modified nor destroyed while the view exists.
In addition to creating a lifetime dependence on the parent object, dependent properties also restrict the lifetime of the value returned by that property to the same scope. In other words, the property returns a "nonescaping value" associated with its access scope. A new compiler feature supports this, which we refer as "nonescaping values". In broad terms, the compiler ensures that a nonescaping value does not outlive its designated scope.
Every copy of a nonescaping value inherits its lifetime restriction. So when a dependent property is assigned to a variable, the variable is restricted to the property's access scope. Here, the view's access scope is limited to the 'if let' block:
let v: View?
if let container = container {
v = container.view
lookAt(view: v) // ok
}
lookAt(view: v) // nonescaping property 'view' is used outside its access scope
v
is a copy of view
's nonescaping value embedded in an Optional. It therefore inherits a nonescaping scope corresponding to view
s access scope. The first call to lookAt
receives another copy as an argument, which cannot escape the function call. The second call to lookAt
is a violation because it uses v
outside of its nonescaping scope.
Within a function, the compiler enforces nonescaping values using a dataflow-driven diagnostic. Passing a nonescaping value across function calls raises interesting issues described in the "Nonescaping parameters" section and further explored in "Future directions".
We propose adding the @nonescaping
attribute to parameter types to allow passing a nonescaping value across an API boundary:
func lookAt(view: @nonescaping View)
A nonescaping parameter is simply a nonescaping value whose nonescaping scope is the function body. Plainly put, the argument value cannot escape the function. In "Future directions", however, we describe a "lifetime-dependent results" feature that will allow nonescaping values to be both passed as an argument and returned by the same function such that the return value inherits the argument's nonescaping scope.
A method's 'self' parameter can be marked nonescaping by adding the attribute before the function declaration:
struct MyView {
@nonescaping func lookAtSelf()
}
Computed properties can also delcare 'self' as nonescaping. Note that the attribute occurs here before the property declaration rather than in type position:
struct MyView {
@nonescaping var value: V { get {...} }
}
For convenience, accessors that yield dependent values also imply nonescaping self. There is no need to add @nonescaping
to a @dependent
property:
struct MyView {
/*@nonescaping*/ var value: @dependent V { _read {...} }
}
In the future, the nonescaping parameter attribute may sometimes be inferred from the parameter type. Implementing safe view types, however, merits a separate discussion; so "Concrete nonescaping types" and "Generic nonescaping types" are discussed in "Future directions".
Other techniques for using nonescaping values across API boundaries are discussed under Future Directions.
When a view is accessed in a mutating context, the modify accessor yields an inout
parameter:
struct MyView {
@nonescaping mutating func update()
}
container.mutableView.update()
As with immutable views, mutable views can only be passed to inout parameters that have a @nonescaping
attribute:
func mutate(view: @nonescaping inout View) {
view.update()
}
mutate(&container.mutableView)
Unlike other nonescaping values, nonescaping inout parameters are noncopyable because allowing copies would break the exclusivity requirement. The compiler enforces noncopyable values using the same mechanism as for @noncopyable struct or enums.
A nonescaping value supports all standard operations on values that an escapable value supports. Therefore, the unsafe withoutActuallyEscaping
escape hatch can be extended to handle non-function types:
let c: Collection = container.view
withoutActuallyEscaping(c) { c in // v is now allowed to escape inside the closure
// ...
let iter = c.makeIterator()
// I promise not to escape 'iter'
// ...
}
New @dependent
properties and new @nonescaping
parameters are additive relative to existing Swift code.
Adding @nonescaping
attributes to existing parameters is not source breaking as long as the implementation cannot escape its parameter:
container.view.map {...} // ok after adding '@nonescaping' to 'map'
container.view.sort() // ok after adding '@nonescaping' to 'sort'
But protocols requirements will either be incompatible with views or will require potentially breaking existing conformances:
container.view.index(after: i) // ok
container.view.swapAt(i, j) // ok
let v: Collection = container.view
v.index(after: i) // error: 'Collection.index()' may escape 'self'
v.swapAt(i, j) // error: 'Collection.swapAt()' may escape 'self'
v.nonEscapingSwapAt(i, j) // ok: additive '@nonescaping' Collection requirement
Adding or removing @dependent
on an existing property would break ABI because it is the caller's responsibility to enforce the attribute.
The @nonescaping
parameter attribute does not affect ABI.
Introducing new properties with a @dependent
attribute is additive. For an existing property, adding or removing @dependent
breaks API.
Adding a @nonescaping
parameter attribute is a resilient change. Removing a @nonescaping
parameter is not resilient because caller's make correctness assumptions based on the attribute.
Rather than using an explicit attribute, the compiler can deduce that a property is dependent from its type. This approach will be preferable once nonescaping types are supported, which is covered in "Future directions". An attribute is simply a convenient way to introduce dependent properties.
A number of techniques can be used to allow nonescaping values to cross API boundaries:
-
A
@nonescaping
parameter attribute -
Automatic compiler analysis of always-emit-into-client code
-
An unsafe
@_effects(nonescaping)
parameter annotation
Only the first technique, @nonescaping
parameter attributes, is formally supported by this proposal.
A @nonescaping
attribute could be applied to local variable declarations. Once initialized, the variable could be used within its lexical scope. Copies of the variable cannot escape its scope. Compiler support for "nonescaping values" is sufficient to implement this.
A nonescaping variable is only lifetime constrained by a lexical scope. It does not require exclusive access to a different storage object and does create a lifetime dependence on the storage object:
let x
do {
@nonescaping let y = object.property
// exclusive access to object.property is released here
// object itself may be destroyed here
x = y
}
// any use of x after this point is illegal
As such, this feature has potential for misuse.
Returning a lifetime-dependent value from a function requires lifetime-dependent results. This could be expressed with a @resultDependsOn
parameter attribute and a @resultDependsOnSelf
function attribute. A dependent result is a nonescaping value relative to the lexical scope that contains the call, and it creates a lifetime dependence on the the value that was passed into the annotated parameter. Here, the returned View object will depend on the self
Container's lifetime, but exclusivity would need to be enforced manually:
struct Container {
@resultDependsOnSelf
func lockContainerAndGetView() -> View { ... }
}
If, however, the dependent parameter is borrowed (on the caller side) or passed inout
, then the result's nonescaping scope is the argument's exclusive access scope:
func getExclusiveMember(a: @resultDependsOn inout A) -> B {
return a.b
}
let parent = ...
// The exclusive modification of parent begins here...
let child = getExclusiveMember(&parent)
use(child)
// The exclusive modification of parent ends after child's lifetime ends
This allows the behavior of dependent properties to be emulated using normal function calls. We can then add a @dependent
attribute to regular method declarations:
@dependent someMethod() -> T
This would imply @resultDependsOnSelf borrowing
:
@resultDependsOnSelf borrowing someMethod() -> T
(borrowing
tells the callee to borrow self
as explained in borrow
and consume
parameter ownership modifiers.) On the caller side, the @dependent
function attribute would ensure that the lifetime of the return value is confined to the borrow scope of self
.
Without nonescaping types, passing a nonescaping value across an API boundary requires a parameter annotation:
func lookAt(view: @nonescaping View)
Using view types, however, should not require API annotation. Instead, the view type should carry the annotation itself. This can be done with a @nonescaping
struct, enum, or class type attribute:
@nonescaping
struct View { ... }
A nonescaping type requires all instances to be nonescaping values. As a consequence, the initializer must produce a nonescaping result. This can be done using a lifetime-dependent result as described above. View initializers will, in practice, take the view's container as the argument that the result depends on, even if that argument is unused within the initializer. This creates an immediate lifetime dependency from the view to its container:
@nonescaping
struct View {
init(internals: Internals, @resultDependsOn opaqueContainer: Any) {...}
}
Nonescaping values can never be type erased such that they lose their nonescaping constraint. They can neither be passed to an generic API nor assigned to an existential unless the destination has a nonescaping constraint. As explained in "Nonescaping parameters", nonescaping values can, however, be borrowed as noncopyable types. Thus, nonescaping values are compatible with any API designed for noncopyable types.
If @nonescaping
annotations become too onerous, then generic nonescaping types can be introduced. This can be done using exactly the same technique as Generics support for noncopyable types. In addition to a default Copyable
capability, types would now have a default Escapable
capability, which generic parameter types can explicitly opt-out of.
Because mutable views can't be copied, their usability is severely restricted relative to immutable views. mutating
variables will eventually address this shortcoming...
Mutating references, a.k.a. inout reference bindings, will allow mutable views to be used locally with a lightweight variable declarations syntax:
mutating view = &container.mutableView
view.sort() // assuming 'sort' is annotated @nonescaping
mutating properties will allow mutable views to be wrapped in adapters for convenenience:
struct MyMutableAdapter : Adapter {
mutating view: View
func apply() {
mutate(&view)
}
}
MyAdapter(view: &container.mutableView)
Read and modify accessor coroutines
borrow
and consume
parameter ownership modifiers
borrow and inout reference bindings