[Pitch] Box

You can use it to create recursive non-copyable types, or put the storage of a large non-copyable type on the heap so that you don't overflow the stack.

I would however like to second the recommendation that Unique<T> provide a stable address.

Edit: It's also not refcounted.

3 Likes

Unique is better than Box, but Pointer or SafePointer is more descriptive of what this type really is.

2 Likes

Even though promising that Unique has a stable address would improve things, I'm concerned that it would still be easy to accidentally break the exclusivity/aliasing rules when unsafely forming persistent pointers. Specifically, it'd be dangerous to non-instantaneously borrow or mutate the inner value if other Swift or C code has an unsafe pointer to the value. I mentioned earlier how mixing unsafe pointers and safe references like that has caused problems for Rust.

If we expect that, at some point, Unique will be primarily used to unsafely form persistent pointers (such as if the other use cases are subsumed by the indirect keyword), then I think it'd be a good idea to acknowledge that the primary use case for the type is unsafe. In C++, std::unique_ptr is still useful for preventing double-free errors and memory leaks, even if it's used to unsafely form persistent pointers.

To do that, we could name the type something like UnsafeUnique, and mark the .value property as @unsafe, to remind people to be careful about ensuring exclusivity. To remind people that the pointer isn't necessarily exclusive/non-aliased, I think it'd be even better to drop the "unique" part of the name and name it something like UnsafeOwned.

It would also be dangerous to send a Unique instance across isolation domains, if one forgets to send the unsafe pointers across isolation domains as well. So I think it'd also be a good idea to always make Unique non-Sendable. Then region-based isolation would know that the Unique instance and the unsafe pointers always need to be in the same isolation domain.

1 Like

Indulging my regrettable proclivity for bike-shedding…

It seems to me that the last several posts have been circling around a very interesting set of ideas. Integrating those points of view, but edging away a step, I'll throw in a new variant:

    UniqueValue

My reasoning is as follows:

  • This contains a great big signpost reassuring us that this is about values, not about some abstract concept of "uniqueness" alone.

  • It also emphasizes that this is not about reference types, and hence indicates why you wouldn't just use a class instance instead.

  • The word "unique" suggests something that has a "freestanding" (or kinda "persistent") identity, which is a gesture in the direction of "pointer".

  • By not using the word "pointer" in the name, I've punted on any promises that might be implied about pointer lifetimes if the word "pointer" were actually in the name.

  • For me, UniqueValue links loosely to "special value", which links loosely to "special place in memory", which links loosely to "heap", without actually committing to the concept of a heap at the language level. The "special place" is a sort of boarding school for values that need extra supervision — you know where the inmates sleep! :slight_smile:

Please correct me if I'm wrong here… but Span already is our "safe" Pointer?

There are actually very few references [womp womp…] of "pointers" in TSPL Language Guide. Pointers are mentioned a lot more in the Reference Manual. But if we assume that the Language Guide is what beginners are starting with then there's basically just this:

Which starts with the assumption the readers came from "C" languages. But I'm not really sure it's so easy to assume that working with pointers is something very common for entry-level engineers today. Those engineers can be very smart and build efficient data structures and algorithms without having experience with a language that requires us to "think" about pointers upfront.

The Unsafe[Mutable][Raw][Buffer]Pointer types are "unsafe" for a reason. I don't see any need for us to begin to ship a safe Pointer and also put that in the name itself. I think it's better to ask what a safe pointer would give us here that we don't already have… and then think of a name for that without explicitly calling it a Pointer. I believe Span and Unique both sort of fit that idea.

1 Like

Perhaps instead of marking the (actually-safe) Unique type as @unsafe, it would be sufficient to limit pointer conversions to being consuming? So instead of it offering var unsafeAddress: UnsafeMutablePointer<Value> { get } it would offer consuming func intoUnsafePointer() -> UnsafeMutablePointer<Value> and init(unsafeUniquePointer: UnsafeMutablePointer<Value>) with the requirement that the provided pointer have been obtained via the intoUnsafePointer method.

1 Like

That would mean Unique effectively loses its stable address[1] guarantee, because we can only have a stable address if we make the Unique instance go away. It's a perfectly reasonable design for a type that lacks a stable address guarantee, such as the Box type in Rust. But we would lose the benefits of a type that provides a stable address guarantee, such as the std::unique_ptr type in C++: that we can use a stable address while still avoiding double-free errors and memory leaks.

I guess my point is that a type with a stable address guarantee ought to be different from a type without one. Using a stable address is fundamentally unsafe, because it creates aliasing that can't be tracked by the compiler or the runtime. This has far-reaching effects: whether the type should conform to Sendable, whether borrowing and mutating the inner value should be considered safe operations, whether the compiler should make no-aliasing optimizations, and (arguably) what the type should be named.

I also happen to think that any use case for a heap allocation that doesn't need a stable address guarantee would be better served by a language feature, such as indirect enum cases and stored properties.


  1. This is technically a misnomer, because what we're actually talking about is stable pointers. The difference is that pointers have provenance, which means a pointer can be invalid even if its memory address is a valid memory address. ↩︎

2 Likes

Rust's Box has a stable address. It's just not much of a footgun because you're going to reach for &*box and &mut *box before you reach for as_ptr() or as_mut_ptr(). The only reason it's even potentially an issue here is because we don't have first class references.

We should just document that Unique.withUnsafe{Mutable}Pointer provides a stable address, and then either move towards either first class local borrowing/inout bindings or non-escapable non-owning safe pointer types (like Spans, but for single elements).

On an unrelated bikeshedding note, I'd prefer UniqueBox over simply Unique, since otherwise it's not obvious that it stores its value on the heap type instead of inline.

Also, a consuming leak() -> UnsafeMutablePointer method could be useful regardless.

3 Likes

A Box in Rust does not have a stable address. For example, the following Rust code has undefined behavior (according to Miri, under both Stacked Borrows and Tree Borrows):

#![feature(box_as_ptr)]

fn main() {
    unsafe {
        let mut boxed = Box::new(42);
        let ptr = Box::as_mut_ptr(&mut boxed);
        *ptr += 1;
        boxed = boxed;
        *ptr += 1;
    }
}

I wrote earlier in detail about the problems things like this have caused. In short: this is extremely error-prone. In Rust, unsafe pointers can be invalidated according to the aliasing rules, which are extremely subtle. Swift usually avoids these problems because it only exposes unsafe pointers to existing values through closure-based APIs like withUnsafePointer, which make the lifetime of an unsafe pointer visible in the source code.

That's not because the address isn't stable, That error happens because the pointer is marked as noalias and that makes the self assignment logically invalidate it. This is purely a borrow checker issue. There's not even consensus that the pointer being noalias is correct, either: What are the uniqueness guarantees of Box and Vec? · Issue #326 · rust-lang/unsafe-code-guidelines · GitHub

I'm using "stable address" to mean "stable pointer", even though they are technically different concepts, because I think it aligns with how the term "stable address" has been used in this discussion. To be more precise, a Box in Rust doesn't provide a stable pointer, because according to the aliasing rules, pointers to the inner value are invalidated when the Box is moved. This means it's not useful in practice that the memory address itself doesn't technically change.[1]

The problem isn't that the noalias marker is potentially incorrect. It's consistent with Rust's aliasing rules, and there's nothing inherently wrong with the aliasing rules.[2] The problem is that the aliasing rules are not consistent with people's expectations, so the community has considered changing the aliasing rules.


  1. The discrepancy between pointers and memory addresses is rationalized using a concept called "pointer provenance". ↩︎

  2. Technically, the rules aren't officially decided, but the community usually uses the rules of Stacked Borrows and/or Tree Borrows. Miri uses those rules, and the compiler uses a subset of those rules to justify some optimizations, such as the noalias marker. The specific aliasing rules of Box are documented. ↩︎

I think it's better in the context of Swift for the pointer to remain valid if it is moved. This also falls out naturally from the obvious implementation as a safe wrapper over UnsafeMutablePointer.

1 Like

If the goal is to build something similar to std::unique_ptr, the type should also have a mechanism for consuming an existing pointer and specifying a custom deallocator. That would allow patterns like:

import Darwin // Glibc, Musl, ucrt, etc.
let safeCString = unsafe Unique(consuming: strdup(...), deallocatingWith: free)

At which point Swift has taken ownership of the C string (a C pointer) and is able to correctly deinitialize and deallocate it at the end of the Unique instance's lifetime.

Otherwise, it is constrained to providing its own allocation/deallocation which, while still useful, will force developers to incur a deep copy if the value they want to use comes from a non-Swift source such as a C/POSIX function.

7 Likes

If the goal is that:

We should design this to separate the case that requires a delegated destructor from the one that uses the default allocator. This ensures the standard allocating implementation doesn't pay for extra space (closure storage) or dynamic dispatch calls when they aren't required.

For example like in C++:

protocol Deallocator<Pointee>: ~Copyable {
  associatedtype Pointee: ~Copyable
  func dealloc(_ pointer: UnsafeMutablePointer<Pointee>)
}
struct Box<D: Deallocator>: ~Copyable {
  typealias Value = D.Pointee
  let pointer: UnsafeMutablePointer<Value>
  let deallocator: D
}

However, that makes the type awkward to use:

func f(_ box: consuming Box<some Deallocator<Int>>)

Or introduce it as a protocol:

protocol Box<T>: ~Copyable {
  associatedtype T: ~Copyable
  var value: T { borrow mutate }
  consuming func consume() -> T
  // ...
}

func f(_ box: consuming some Box<T>)

This allows it to be used as a currency type while providing two implementations: one optimized for standard allocation, and another capable of delegating destruction to a closure (and be used for erasure).
This would also allow users to implement their own zero-overhead versions when the destruction function is statically known.

1 Like

My understanding is that the expectation is that Span is the currency type, not Box. Whether the Span came from a Box would be an implementation detail. If you need to manage the lifetime of the allocation, you would use a different type than Box to vend the Span.

Because the type will be @frozen, this could be done with an additional generic parameter:

struct Unique<Value, Deallocator>: ... {
  var _address: UnsafeMutablePointer<Value>
  var _deallocator: Deallocator

  typealias _CustomDeallocator = (UnsafeMutablePointer<Value>) -> Void

  init(...) where Deallocator == Void {
    ...
  }

  init(..., deallocatingWith deallocator: @escaping Deallocator) where Deallocator == _CustomDeallocator {
    ...
    _deallocator = deallocator
  }

  deinit {
    // Ugly, but the compiler can optimize it away
    if Deallocator.self == _CustomDeallocator .self {
      (_deallocator as! _CustomDeallocator)(_address)
    } else {
      _address.deinitialize()
      _address.deallocate()
    }
  }
}

Of course, this then requires the caller to track the deallocator as part of the type, which is ungainly (and a problem for std::unique_ptr too.) Using two separate types has the same problem and an existential box around any UniqueProtocol or some such would take even more space.

(I'd be inclined to just eat the cost here, honestly.)

1 Like

One more naming option to consider: following the precedent of InlineArray, call it Outline, or OutlineBox.

As it was noted by others, calling it Unique suggests semantics that isn't really there; calling it Pointer would be misleading since it has little to do with pointers as a concept; and calling it Box is nice but fails to communicate what is it actually for. Calling it Outline covers the last part: it hints on the main use case, which is untying actual value storage from its immediate inline storage slot.

1 Like

I might be wrong, but I believe the opposite of inline as an adjective is "out of line", not "outline".

5 Likes

The Swift compiler does use the adjective "outlined" quite a bit to describe code that's been lifted out of a context and into its own function, which is sort of the same concept, but regardless the term "outlined/out of line" is orders of magnitude less commonly encountered in programming (compared to "inline") and would be far too jargony for a standard library API.

9 Likes

I think that Box as a suffix adequately conveys that the storage is out-of-line, and Unique as a prefix adequately conveys non-copyability. I just think that having the name be just one or the other would be misleading.

2 Likes