Skip to content

Conversation

@DougGregor
Copy link
Member

@DougGregor DougGregor commented Sep 5, 2025

Description

Introduce new types TrailingArray and TrailingPadding that consist of a fixed-sized header followed by variable-sized storage. The design of these types is inspired by the existing ManagedBuffer type in the standard library, but has been adjusted both to make them non-copyable and to eliminate the requirement for heap allocation.

Detailed Design

It's several new types. Here's a sketch of the API for the primary type, IntrusiveManagedBuffer:

public struct TrailingArray<Header: TrailingElements>: ~Copyable
    where Header: ~Copyable
{
    /// Allocate an intrusive managed buffer with the given header and calling
    /// the initializer to fill in the trailing elements.
    public init<E>(
        header: consuming Header,
        initializingTrailingElementsWith initializer: (inout OutputSpan<Element>) throws(E) -> Void
    ) throws(E)

    /// Allocate an intrusive managed buffer with the given header and
    /// initializing each trailing element with the given `element`.
    public init(header: consuming Header, repeating element: Element)

    /// Take ownership over a pointer to memory containing the header followed
    /// by the trailing elements.
    ///
    /// Once this managed buffer instance is no longer used, this memory will
    /// be freed.
    public init(consuming pointer: UnsafeMutablePointer<Header>)

    /// Return the pointer to the underlying memory, including ownership over
    /// that memory. The underlying storage will not be freed by this buffer;
    /// it is the responsibility of the caller.
    public consuming func takePointer() -> UnsafeMutablePointer<Header>

    /// Access the header portion of the buffer.
    public var header: Header { get set }

    /// The number of trailing elements in the value.
    public var count: Int { get }

    /// Starting index for accessing the trailing elements. Always 0
    public var startIndex: Int { get }

    /// Ending index for accessing the trailing elements. Always `count`.
    public var endIndex: Int { get }

    /// Indices covering all of the trailing elements. Always `0..<count`.
    public var indices: Range<Int> { get }

    /// Access the trailing element at the given index.
    public subscript(index: Int) -> Element { get set }

    /// Accesses the trailing elements following the header.
    public var elements: Span<Element> { get }

    /// Accesses the trailing elements following the header, allowing mutation
    /// of those elements.
    public var mutableElements: MutableSpan<Element> { mutating get }

    /// Create a temporary intrusive managed buffer for the given header, whose
    /// trailing elements are initialized to copies of `element`. That instance
    /// is provided to the given `body` to operate on for the duration of the
    /// call. The temporary is allocated on the stack, unless it is very
    /// large according to `withUnsafeTemporaryAllocation`.
    public static func withTemporaryValue<R: ~Copyable, E>(
        header: consuming Header,
        repeating element: Element,
        body: (inout IntrusiveManagedBuffer<Header>) throws(E) -> R
    ) throws(E) -> R

    /// Create a temporary intrusive managed buffer for the given header, whose
    /// trailing elements are initialized with the given `initializer` function.
    /// That instance is provided to the given `body` to operate on for the
    /// duration of the call. The temporary is allocated on the stack, unless
    /// it is very large according to `withUnsafeTemporaryAllocation`.
    public static func withTemporaryValue<R: ~Copyable, E>(
        header: consuming Header,
        initializingTrailingElementsWith initializer: (inout OutputSpan<Element>) throws(E) -> Void,
        body: (inout IntrusiveManagedBuffer<Header>) throws(E) -> R
    ) throws(E) -> R
}

Documentation

The new types and protocol have full documentation in the source. The module itself has tutorial-like overview to guide users to the functionality they need.

Testing

New tests of the major functionality of the feature.

Performance

I did not. Everything is @frozen/@_alwaysEmitIntoClient and uses noncopyable types so there should be no extraneous runtime overhead.

Source Impact

No source impact. This is a new module with new API.

Checklist

  • I've read the Contribution Guidelines
  • My contributions are licensed under the Swift license.
  • I've followed the coding style of the rest of the project.
  • I've added tests covering all new code paths my change adds to the project (to the extent possible).
  • I've added benchmarks covering new functionality (if appropriate).
  • I've verified that my change does not break any existing tests or introduce unexpected benchmark regressions.
  • I've updated the documentation (if appropriate).

…rage

Introduce new types `IntrusiveManagedBuffer` and `PaddedStorage` that
consist of a fixed-sized header followed by variable-sized storage. The
design of these types is inspired by the existing `ManagedBuffer` type
in the standard library, but has been adjusted both to make them
non-copyable and to eliminate the requirement for heap allocation.
@DougGregor DougGregor requested a review from lorentey as a code owner September 5, 2025 23:27
Copy link
Member

@lorentey lorentey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

This may end up sharing the same module as Box and other ownership utility constructs, but we'll have plenty of opportunities to set that up after this lands.

{
/// The underlying storage.
@usableFromInline
var pointer: UnsafeMutablePointer<Header>
Copy link
Member

@lorentey lorentey Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: please underscore internal declarations, especially ones that are destined to eventually get exposed in ABI.

Suggested change
var pointer: UnsafeMutablePointer<Header>
var _pointer: UnsafeMutablePointer<Header>

/// the information in the header (via the `trailingCount` property), so that
/// it is not stored separately.
@frozen
public struct IntrusiveManagedBuffer<Header: TrailingElements>: ~Copyable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like Managed in the name here -- in the stdlib's existing use of this term, "managed" usually indicates reference counting. We've avoided using that term for constructs that are built on ownership features, like this one.

It also makes this uncomfortably close to ManagedBuffer, which is a far more general construct than this one -- in particular, ManagedBuffer supports (and encourages) uninitialized slots, whereas this one requires its storage to be fully initialized.

How about TrailingBuffer? Or perhaps TrailingArray, to avoid associating the word "buffer" with fixed-size, fully-initialized storage contexts? InlineArray has set a recent precedent of using "array" this way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I'm okay with TrailingArray. Does that mean it should go into the ArrayModule?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hrm. Now that I do this, I also want to rename PaddedStorage to TrailingPadding, which fits the scheme better.

/// that memory. The underlying storage will not be freed by this buffer;
/// it is the responsibility of the caller.
@_alwaysEmitIntoClient
public consuming func takePointer() -> UnsafeMutablePointer<Header> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like an unsafe version of Box.leak() -- would something like leakPointer be a better name? Cc @Azoy

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to go with that precedent, sure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I feel like leakStorage would be slightly better, because it's the storage that we're leaking and the type already has "pointer" into it.

Comment on lines 140 to 144
UnsafeMutableBufferPointer(
start: UnsafeMutableRawPointer(pointer.advanced(by: 1))
.assumingMemoryBound(to: Element.self),
count: header.trailingCount
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Element may have stronger alignment than Header here -- the advanced pointer has to be explicitly rounded up to a suitable boundary.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled this with the latest commit, thank you!

/// Determine the allocation size needed for the given header value.
@_alwaysEmitIntoClient
public static func allocationSize(header: borrowing Header) -> Int {
MemoryLayout<Header>.size + MemoryLayout<Element>.stride * header.trailingCount
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to make sure the allocation size is large enough to cover padding bytes if Element has stricter alignment than Header.

…alignment than the header

When this happens, we over-allocate and use the alignment of the
elements, and then move the start of the header after the point of
allocation such that the properly-aligned elements follow it
immediately.
@glessard
Copy link
Contributor

I force-pushed a mostly-identical copy of the latest commit to force the actions pick up the latest state of the main branch.

Copy link
Contributor

@glessard glessard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! We could improve the test coverage in followup work.

@_lifetime(self)
mutating get {
let elements = self.rawElements.mutableSpan
return _overrideLifetime(elements, mutating: &self)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could be a one-liner since _overrideLifetime consumes its 1st argument.

@glessard glessard merged commit 4cfab5d into apple:main Sep 12, 2025
30 checks passed
@DougGregor DougGregor deleted the trailing-elements branch September 15, 2025 15:37
@lorentey lorentey added this to the 1.3.0 milestone Sep 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants