Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?

Improved Swift pointers

  • Proposal: SE-1989
  • Authors: Kelvin Ma
  • Review Manager: TBD
  • Status: Awaiting review

Introduction

Swift currently offers two sets of pointer types — singular pointers such as UnsafeMutablePointer, and vector (buffer) pointers such as UnsafeMutableBufferPointer. This implies a natural separation of tasks the two kinds of pointers are meant to do. For example, buffer pointers implement Collection conformance, while singular pointers do not.

However, some aspects of the pointer design contradict these implied roles. It is possible to allocate an arbitrary number of instances from a type method on a singular pointer, but not from a buffer pointer. The result of such an operation returns a singular pointer, even though a buffer pointer would be more appropriate to capture the information about the number of instances allocated. It’s possible to subscript into a singular pointer, even though they are not real Collections. Many of the memorystate methods on UnsafeMutablePointer work on vectors of items, while others, such as move() return a single instance. Some parts of the current design turn UnsafePointers into downright DangerousPointers, leading users to believe that they have allocated or freed memory when in fact, they have not.

This proposal seeks to iron out these inconsistencies, and offer a more convenient, more sensible, and less bug-prone API for Swift pointers.

Swift-evolution thread: Pitch: Improved Swift pointers

Motivation

The behavior of Swift pointers regarding memory allocation and deallocation is inconsistent. Currently, memory is allocated by a type method on UnsafeMutablePointer<T>, UnsafeMutablePointer<T>.allocate(capacity:), and an arbitrary number of instances of T can be allocated. This is a problem because UnsafeMutableBufferPointer exists to represent vectors of things in memory, while UnsafeMutablePointer is designed around dealing with singular instances of things. When capacity != 1, the UnsafeMutablePointer instance’s purpose is almost always as a temporary to be passed as the start argument of UnsafeMutableBufferPointer.init(start:count:). This means that the following “idiom” is very common in Swift code:

let buffer = UnsafeMutableBufferPointer<UInt8>(start: UnsafeMutablePointer<UInt8>.allocate(capacity: byteCount), count: byteCount)

Aside from being extremely long and unwieldy, and requiring the creation of a temporary, byteCount must appear twice.

Pointer deallocation is even more troublesome. Currently, memory is deallocated by an instance method on UnsafeMutablePointer, deallocate(count:). This requires the extraction of the buffer pointer’s baseAddress, and count property. It is very common for the above code to be immediately followed by:

defer
{
    buffer.baseAddress?.deallocate(capacity: buffer.count)
}

The ? is sometimes exchanged with an ! depending on the personality of the author, as neither operator is meaningful here — the baseAddress is never nil if the buffer pointer was created around an instance of UnsafeMutablePointer.

Nearly all users, on first seeing the signature of deallocate(capacity:), will naturally conclude from the capacity label that deallocate(capacity:) is equivalent to some kind of realloc() that can only shrink the buffer. However this is not the actual behavior — deallocate(capacity:) actually ignores the capacity argument and just calls free() on self. The current API is not only awkward and suboptimal, it is misleading. Users who are aware of this behavior may also choose to disregard the capacity argument and write things like this:

defer
{
    buffer.baseAddress?.deallocate(capacity: 42)
}

which is functionally equivalent. However this will lead to disastrous source breakage if the implementation of deallocate(capacity:) is ever “corrected”. Since the API would not change, such code would still compile, but suddenly start failing at runtime.

Problems exist in the design of the Swift memorystate functions. assign(from:count:), moveAssign(from:count:), moveInitialize(from:count:), initialize(from:count:), deinitialize(count:), and withMemoryRebound<T, Result>(to:capacity:_:) are all instance methods on UnsafeMutablePointer, even though the count argument is a natural fit for the count property on UnsafeMutableBufferPointer, or UnsafeBufferPointer for the non-vacating memorystate functions.

The initialize(from:count:) method is a curious case — it has a repeating variant, initialize(to:count:), but assign(from:count:) has no such variant, even though it would make just as much sense for it to have one.

UnsafeMutablePointer has a move() method which vacates and returns the singular instance at self. Strangely, none of the other memorystate functions have singular variants, nor does move() have a vector variant. There is no assign(from:), moveAssign(from:), moveInitialize(from:), initialize(from:), deinitialize(), or withMemoryRebound<T, Result>(to:_:) method, nor is there a move(count:) method which could perhaps allocate and return a buffer pointer or an Array of the pointer’s contents.

UnsafeMutableBufferPointer does not implement most of the memorystate functions. This means that copying, moving, or vacating anything into or out of them requires extracting baseAddresses, just like with deallocate(capacity:).

buffer1.baseAddress?.moveInitialize(from: buffer2.baseAddress!)

Because one of the pointers is an argument, even authors of the conservative disposition are forced to use ! instead of ? optional chaining.

There is one exception — UnsafeMutableBufferPointer features an initialize<S>(from:) method which takes a Sequence, the only memorystate function in UnsafeMutableBufferPointer. This method was originally an inhabitant of UnsafeMutablePointer before it was supposedly move-initialized to UnsafeMutableBufferPointer by SE-147. SE-147 must not have deinitialized the member on UnsafeMutablePointer, as the type still features an active initialize<C>(from:) method which takes a Collection. (The original SE-147 method took a Collection.)

Finally, the naming and design of some UnsafeMutableRawPointer members deserves to be looked at. The usage of capacity, bytes, and count as argument labels is wildly inconsistent and confusing. In copyBytes(from:count:), count refers to the number of bytes, while in initializeMemory<T>(as:at:count:to:) and initializeMemory<T>(as:from:count:), count refers to the number of strides. Meanwhile bindMemory<T>(to:capacity:) uses capacity to refer to this quantity. The always-problematic deallocate(bytes:alignedTo) method and allocate(bytes:alignedTo:) type methods use bytes to refer to byte-quantities. Adding to the confusion, UnsafeMutableRawBufferPointer offers an allocate(count:) type method (the same signature method we’re trying to add to UnsafeMutableBufferPointer), except the count in this method refers to bytes. This kind of API naming begets stride bugs and makes Swift needlessly difficult to learn.

Proposed solution

  • drop the capacity parameters from UnsafeMutablePointer<T>.allocate(capacity:) and deallocate(capacity:).

UnsafeMutablePointer<T> will now only allocate memory for singular instances of T, more in-line with what it was designed for. Similarly, there will no longer be any confusion over what deallocate() does, it will just free the memory for that single instance. When the pointer comes from somewhere else besides UnsafeMutablePointer<T>.allocate(), for example, the baseAddress! of a buffer pointer, deallocate() will free the entire memory block at self, just as if free() were called on it.

To ease the transition, the old UnsafeMutablePointer<T>.allocate(capacity:) method will be marked as deprecated. deallocate(capacity:) should be marked as unavailable since it currently encourages dangerously incorrect code.

This proposal does not change UnsafeMutableRawPointer.allocate(bytes:alignedTo:), but the bytes and alignedTo parameters should be removed from deallocate(bytes:alignedTo:) for the same reasons as deallocate(capacity:).

  • add an allocate(count:) type method to UnsafeMutableBufferPointer

To replace UnsafeMutablePointer<T>.allocate(capacity:), UnsafeMutableBufferPointer will gain an allocator type method which takes a size parameter. This makes more sense given the intended use of the two pointer types. The argument label might as well be count instead of capacity, to be consistent with its initializer init(start:count:), and its count property.

  • add a deallocate() instance method to UnsafeMutableBufferPointer

Buffer pointers will be able to deallocate their own memory. This method is equivalent to calling the new deallocate() method on baseAddress!, but is provided so that the user does not have to extract the (not-really) optional baseAddress property. This method does not take a size parameter since Swift is currently only able to free entire memory blocks. This avoids misleading users, and leaves the possibility open for us to add a deallocate(count:) method in the future, or perhaps even a reallocate(toCount:) method.

  • drop the count parameters from assign(from:count:), moveAssign(from:count:), moveInitialize(from:count:), initialize(from:count:), initialize(to:count:), deinitialize(count:), and capacity from withMemoryRebound<T, Result>(to:capacity:_:)

For similar reasons to that for dropping capacity from allocate(capacity:), the UnsafeMutablePointer memorystate functions should operate on singular instances.

For consistency, the count parameter should be dropped from UnsafeMutableRawPointer.initializeMemory<T>(as:at:count:to:), UnsafeMutableRawPointer.initializeMemory<T>(as:at:count:to:), and UnsafeMutableRawPointer.moveInitializeMemory<T>(as:from:count:), and the capacity parameter from UnsafeMutableRawPointer.bindMemory<T>(to:capacity:). The size parameters in UnsafeMutableRawPointer’s other methods should be renamed but not removed as they refer to byte quantities.

The old sized methods will be marked as deprecated.

  • move the old sized memorystate functions assign(from:count:), moveAssign(from:count:), moveInitialize(from:count:), initialize(from:count:), initialize(to:count:), deinitialize(count:), and withMemoryRebound<T, Result>(to:capacity:_:) to UnsafeMutableBufferPointer

This is analogous to moving the allocator type method to UnsafeMutableBufferPointer. The same should be done for the initializeMemory<T>(as:at:count:to:), initializeMemory<T>(as:at:count:to:), moveInitializeMemory<T>(as:from:count:), and bindMemory<T>(to:capacity:) methods of UnsafeMutableRawPointer, which should be reinstated as members of UnsafeMutableRawBufferPointer. The latter function would also benefit from a rename, as will be explained in the next point item.

It goes without saying that the source parameters should be changed to buffer pointers, where originally they were singular pointers.

  • rename copyBytes(from:count:) to copy(from:bytes:), and rename capacity in UnsafeMutableRawBufferPointer.bindMemory<T>(to:capacity:) and UnsafeMutableBufferPointer.withMemoryRebound<T, Result>(to:capacity:_:) to count

This reduces the inconsistency in our argument labeling and keeps with the convention we have adopted in UnsafeMutableBufferPointer<Element>.allocate(count:) in that count refers to strided instances, while bytes refers to, well, bytes. Since this makes the word “bytes” occur twice in copyBytes(from:bytes:), we should drop the “Bytes” suffix and further rename the method to copy(from:bytes:). Since UnsafeMutableRawPointer is inherently untyped, it is obvious that any memory transfer operation on it is a bytewise operation so the “Bytes” suffix adds only verbosity and no clarity.

  • add unsized memorystate functions assign(from:), moveAssign(from:), moveInitialize(from:), initialize(from:), initialize(to:), and deinitialize() to UnsafeMutableBufferPointer

Since buffer pointers track their own count, this value is a natural fit for the count argument in most of the memorystate functions. deinitialize() will deinitialize all of the elements in a buffer pointer. For the binary operations assign(from:), moveAssign(from:), moveInitialize(from:), and initialize(from:), it is assumed that the other buffer pointer contains at least as many elements as self does.

This proposal does not give the initializeMemory<Element>(as:at:count:to:) and bindMemory<Element>(to:count:) methods on UnsafeMutableRawPointer analogous counterparts initializeMemory<Element>(as:to:) and bindMemory<Element>(to:). It’s unclear whether such functions would be useful, since raw buffer pointers don’t inherently “know” how many instances of an arbitrary type fit in themselves and this information is likely already tracked externally by the user.

For similar reasons, UnsafeMutableBufferPointer.withMemoryRebound<T, Result>(to:count:_:) does not get an unsized variant because count may be different with a differently laid out type.

  • add an assign(to:) method to UnsafeMutablePointer and an assign(to:) and assign(to:count:) method to UnsafeMutableBufferPointer

This addresses the missing assignment analogues to initialize(to:) and initialize(to:count:) methods.

  • rename UnsafeMutableRawBufferPointer.allocate(count:) and add an alignedTo parameter to make it UnsafeMutableRawBufferPointer.allocate(bytes:alignedTo:)

This brings it in line with the UnsafeMutableRawPointer allocator, and avoids the contradictory and inconsistent use of count to represent a byte quantity. Currently UnsafeMutableRawBufferPointer.allocate(count:) aligns to the size of Int, an assumption not shared by its singular variant.

  • finally remove initialize<C>(from:) from UnsafeMutablePointer

This method was supposed to be removed per SE-147. Better late than never.

  • remove subscripts from UnsafePointer and UnsafeMutablePointer

Subscripts on UnsafePointer and UnsafeMutablePointer are inconsistent with their intended purpose. For example, ptr[0] and ptr.pointee both do the same thing. Furthermore, it is not immediately obvious from the grammar that the subscript parameter is an offset. New users may conclude that subscripting a pointer at [89] dereferences the memory at address 0x0000000000000059! It would make more sense to use pointer arithmetic and the pointee property to access memory at an offset from self than to allow subscripting.

C programmers who defend this kind of syntax are reminded that many things obvious to C programmers, are obvious to only C programmers. The trend in Swift’s design is to separate C’s array–pointer duality; pointer subscripts run counter to that goal.

There is an argument that singular pointer subscripts are useful when singular pointers of unknown length are returned by C APIs. The counter-argument is that you almost never need to do random access into a vector of unknown length, rather you would iterate one element at a time until you reach the end. This lends itself well to pointer incrementation and the pointee property rather than subscripting.

while ptr.pointee != sentinel
{
    ...
    
    ptr += 1
}

What this proposal does not do

This proposal does not solve the problem of defining a vector equivalent to UnsafeMutablePointer.move(), as it’s unclear what the return type should be. Should the community agree on what this function should look like, it can be added at a later date as a purely additive proposal. For now, move() will only exist on singular pointers.

This proposal also does not address the missing Collection/Sequence memorystate functions, although it does call for (finally) removing UnsafeMutablePointer.initialize<C>(from:). This is out of scope for this proposal, and the missing functions can always be filled in at a later date, as part of a purely additive proposal.

Detailed design

The new pointers API should look something like this:

struct UnsafePointer<T>
{
    //@available(*, deprecated, "use `(self + offset).pointee` to access memory at an offset instead")
    subscript(offset:Int) -> T
}

struct UnsafeMutablePointer<T>
{
    //@available(*, deprecated, "use `(self + offset).pointee` to access memory at an offset instead")
    subscript(offset:Int) -> T

    //@available(*, deprecated, "use the allocate() type method to allocate a single instance, or
    //    UnsafeMutableBufferPointer<Element>.allocate(count:) to allocate multiple instances instead")
    static
    func allocate<T>(capacity:Int) -> UnsafeMutablePointer<T>

    /*
    Allocates a memory block starting at `self` of size `MemoryLayout<T>.size`
    */
    static
    func allocate<T>() -> UnsafeMutablePointer<T>

    //@available(*, unavailable, "deallocate(capacity:) frees the entire memory
    //    block at `self` regardless of the `capacity` argument. use `deallocate()` instead")
    func deallocate(capacity _:Int)

    /*
    Attempts to deallocate the memory block at `self`.
    The behavior should be exactly the same as with calling `deallocate(capacity:) currently.
    */
    func deallocate()

    //@available(*, deprecated)
    func assign(from:UnsafePointer<Pointee>, count:Int)
    //@available(*, deprecated)
    func deinitialize(count:Int)
    //@available(*, deprecated)
    func initialize<C>(from:C)
    //@available(*, deprecated)
    func initialize(from:UnsafePointer<Pointee>, count:Int)
    //@available(*, deprecated)
    func initialize(to:Pointee, count:Int)
    //@available(*, deprecated)
    func moveAssign(from:UnsafeMutablePointer<Pointee>, count:Int)
    //@available(*, deprecated)
    func moveInitialize(from:UnsafeMutablePointer<Pointee>, count:Int)
    //@available(*, deprecated)
    func withMemoryRebound<T, Result>(to:T.Type, capacity:Int, _ body:(UnsafeMutablePointer<T>) -> Result)

    /*
    deinitializes `pointee`, copies the contents of the other pointer into `pointee`,
    and initializes `pointee`
    */
    func assign(from:UnsafePointer<Pointee>)
    /*
    deinitializes `pointee`, copies the contents of the first parameter into `pointee`, 
    and initializes `pointee`
    */
    func assign(to:Pointee)
    /*
    deinitializes `pointee`
    */
    func deinitialize()
    /*
    copies the contents of the other pointer into `pointee`, and initializes `pointee`
    */
    func initialize(from:UnsafePointer<Pointee>)
    /*
    copies the contents of the first parameter into `pointee`, and initializes `pointee`
    */
    func initialize(to:Pointee)
    /*
    deinitializes `pointee`, copies the contents of the other pointer into `pointee`,
    deinitializes the other pointer’s `pointee`, and initializes `pointee`
    */
    func moveAssign(from:UnsafeMutablePointer<Pointee>)
    /*
    copies the contents of the other pointer into `pointee`, deinitializes the
    other pointer’s `pointee`, and initializes `pointee`
    */
    func moveInitialize(from:UnsafeMutablePointer<Pointee>)
    /*
    rebinds `pointee` to another type
    */
    func withMemoryRebound<T, Result>(to:T.Type, _ body:(UnsafeMutablePointer<T>) -> Result)
}

struct UnsafeMutableRawPointer
{
    //@available(*, unavailable, "deallocate(bytes:alignedTo:) frees the entire memory
    //    block at `self` regardless of the arguments argument. use `deallocate()` instead")
    func deallocate(bytes _:Int, alignedTo _:Int)

    /*
    Attempts to deallocate the memory block at `self`.
    The behavior should be exactly the same as with calling
    `deallocate(bytes:alignedTo:) currently.
    */
    func deallocate()

    //@available(*, deprecated)
    func bindMemory<T>(to:T.Type, capacity:Int)
    //@available(*, deprecated, renamed: "copy(from:bytes:)")
    func copyBytes(from:UnsafeRawPointer, count:Int)
    func copy(from:UnsafeRawPointer, bytes:Int)
    //@available(*, deprecated)
    func initializeMemory<T>(as:T.Type, at:Int, count:Int, to:T)
    //@available(*, deprecated)
    func initializeMemory<C>(as:C.Element.Type, from:C)
    //@available(*, deprecated)
    func initializeMemory<T>(as:T.Type, from:UnsafePointer<T>, count:Int)
    //@available(*, deprecated)
    func moveInitializeMemory<T>(as:T.Type, from:UnsafeMutablePointer<T>, count:Int)

    func bindMemory<T>(to:T.Type)
    func initializeMemory<T>(as:T.Type, at:Int, to:T)
    func initializeMemory<T>(as:T.Type, from:UnsafePointer<T>)
    func moveInitializeMemory<T>(as:T.Type, from:UnsafeMutablePointer<T>)
}

struct UnsafeMutableBufferPointer<Element>
{
    /*
    Aligns `baseAddress` to `MemoryLayout<Element>.alignment` and
    allocates a memory block starting at `baseAddress!` of size
    `MemoryLayout<T>.stride * count`
    */
    static
    func allocate<Element>(count:Int) -> UnsafeMutableBufferPointer<Element>

    /*
    Attempts to free the memory block at `baseAddress`. Should be equivalent
    to calling self.baseAddress?.deallocate(capacity: ANYTHING) currently.
    */
    func deallocate()

    func assign(from:UnsafeBufferPointer<Pointee>, count:Int)
    func assign(to:Pointee, count:Int)
    func moveAssign(from:UnsafeMutableBufferPointer<Pointee>, count:Int)
    func moveInitialize(from:UnsafeMutableBufferPointer<Pointee>, count:Int)
    func initialize(from:UnsafeBufferPointer<Pointee>, count:Int)
    func initialize(to:Pointee, count:Int)
    func deinitialize(count:Int)
    func withMemoryRebound<Element, Result>(to:Element.Type, count:Int, _ body:(UnsafeMutableBufferPointer<Element>) -> Result)

    /*
    assigns `self.count` items from the other buffer into `self`
    */
    func assign(from:UnsafeBufferPointer<Pointee>)
    /*
    assign all elements in `self` to the parameter
    */
    func assign(to:Pointee)
    /*
    move-assigns `self.count` items from the other buffer into `self`
    */
    func moveAssign(from:UnsafeMutableBufferPointer<Pointee>)
    /*
    move-initializes `self.count` items from the other buffer into `self`
    */
    func moveInitialize(from:UnsafeMutableBufferPointer<Pointee>)
    /*
    initializes `self.count` items from the other buffer into `self`
    */
    func initialize(from:UnsafeBufferPointer<Pointee>)
    /*
    initializes all elements in `self` to the parameter
    */
    func initialize(to:Pointee)
    /*
    deinitializes all elements in `self`
    */
    func deinitialize()
}

struct UnsafeMutableRawBufferPointer
{
    //@available(*, deprecated)
    static func allocate(count:Int) -> UnsafeMutableRawBufferPointer

    static func allocate(bytes:Int, alignedTo:Int) -> UnsafeMutableRawBufferPointer
    func deallocate()
    func bindMemory<Element>(to:Element.Type, count:Int)
    func copy(from:UnsafeRawBufferPointer, bytes:Int)
    func initializeMemory<Element>(as:Element.Type, at:Int, count:Int, to:Element)
    func initializeMemory<Element>(as:Element.Type, from:UnsafeBufferPointer<Element>, count:Int)
    func moveInitializeMemory<Element>(as:Element.Type, from:UnsafeMutableBufferPointer<Element>, count:Int)
}

in summary:

struct UnsafePointer<T>
{
--- subscript(offset:Int) -> T
}

struct UnsafeMutablePointer<T>
{
--- subscript(offset:Int) -> T

--- static func allocate<T>(capacity:Int) -> UnsafeMutablePointer<T>
--- func deallocate(capacity _:Int)
+++ static func allocate<T>() -> UnsafeMutablePointer<T>
+++ func deallocate()

--- func assign(from:UnsafePointer<Pointee>, count:Int)
--- func deinitialize(count:Int)
--- func initialize<C>(from:C)
--- func initialize(from:UnsafePointer<Pointee>, count:Int)
--- func initialize(to:Pointee, count:Int)

    func move()

--- func moveAssign(from:UnsafeMutablePointer<Pointee>, count:Int)
--- func moveInitialize(from:UnsafeMutablePointer<Pointee>, count:Int)

--- func withMemoryRebound<T, Result>(to:T.Type, capacity:Int, _ body:(UnsafeMutablePointer<T>) -> Result)

+++ func assign(from:UnsafePointer<Pointee>)
+++ func assign(to:Pointee)
+++ func deinitialize()
+++ func initialize(from:UnsafePointer<Pointee>)
+++ func initialize(to:Pointee)
+++ func moveAssign(from:UnsafeMutablePointer<Pointee>)
+++ func moveInitialize(from:UnsafeMutablePointer<Pointee>)
+++ func withMemoryRebound<T, Result>(to:T.Type, _ body:(UnsafeMutablePointer<T>) -> Result)
}

struct UnsafeMutableRawPointer
{
--- func deallocate(bytes _:Int, alignedTo _:Int)
+++ func deallocate()

--- func bindMemory<T>(to:T.Type, capacity:Int)
--- func copyBytes(from:UnsafeRawPointer, count:Int)
+++ func copy(from:UnsafeRawPointer, bytes:Int)
--- func initializeMemory<T>(as:T.Type, at:Int, count:Int, to:T)
--- func initializeMemory<C>(as:C.Element.Type, from:C)
--- func initializeMemory<T>(as:T.Type, from:UnsafePointer<T>, count:Int)
--- func moveInitializeMemory<T>(as:T.Type, from:UnsafeMutablePointer<T>, count:Int)

+++ func bindMemory<T>(to:T.Type)
+++ func initializeMemory<T>(as:T.Type, at:Int, to:T)
+++ func initializeMemory<T>(as:T.Type, from:UnsafePointer<T>)
+++ func moveInitializeMemory<T>(as:T.Type, from:UnsafeMutablePointer<T>)
}

struct UnsafeMutableBufferPointer<Element>
{
+++ static func allocate<Element>(count:Int) -> UnsafeMutableBufferPointer<Element>
+++ func deallocate()

+++ func assign(from:UnsafeBufferPointer<Pointee>, count:Int)
+++ func assign(to:Pointee, count:Int)
+++ func moveAssign(from:UnsafeMutableBufferPointer<Pointee>, count:Int)
+++ func moveInitialize(from:UnsafeMutableBufferPointer<Pointee>, count:Int)
+++ func initialize(from:UnsafeBufferPointer<Pointee>, count:Int)
+++ func initialize(to:Pointee, count:Int)
+++ func deinitialize(count:Int)
+++ func withMemoryRebound<Element, Result>(to:Element.Type, count:Int, _ body:(UnsafeMutableBufferPointer<Element>) -> Result)

+++ func assign(from:UnsafeBufferPointer<Pointee>)
+++ func assign(to:Pointee)
+++ func moveAssign(from:UnsafeMutableBufferPointer<Pointee>)
+++ func moveInitialize(from:UnsafeMutableBufferPointer<Pointee>)
+++ func initialize(from:UnsafeBufferPointer<Pointee>)
+++ func initialize(to:Pointee)
+++ func deinitialize()
}

struct UnsafeMutableRawBufferPointer
{
--- static func allocate(count:Int) -> UnsafeMutableRawBufferPointer
+++ static func allocate(bytes:Int, alignedTo:Int) -> UnsafeMutableRawBufferPointer
+++ func deallocate()
+++ func bindMemory<Element>(to:Element.Type, count:Int)
+++ func copy(from:UnsafeRawBufferPointer, bytes:Int)
+++ func initializeMemory<Element>(as:Element.Type, at:Int, count:Int, to:Element)
+++ func initializeMemory<Element>(as:Element.Type, from:UnsafeBufferPointer<Element>, count:Int)
+++ func moveInitializeMemory<Element>(as:Element.Type, from:UnsafeMutableBufferPointer<Element>, count:Int)
}

Source compatibility

Some parts of this proposal are source breaking.

  • deprecating UnsafeMutablePointer<T>.allocate(capacity:) and replacing it with UnsafeMutableBufferPointer<Element>.allocate(count:)

Usage of UnsafeMutablePointer<T>.allocate(capacity:) is rare in most Swift code, and it is usually found in lower-level code such as libraries. Automigration can be provided by replacing UnsafeMutablePointer<T>.allocate(capacity:) with UnsafeMutableBufferPointer<Element>.allocate(count:).baseAddress!, however in almost all circumstances, this method is used in the context of the idiom

let buffer = UnsafeMutableBufferPointer<UInt8>(start: UnsafeMutablePointer<UInt8>.allocate(capacity: byteCount), count: byteCount)
defer
{
    buffer.baseAddress?.deallocate(capacity: buffer.count)
}

which would be automigrated to

let buffer = UnsafeMutableBufferPointer<UInt8>(start: UnsafeMutableBufferPointer<UInt8>.allocate(count: byteCount).baseAddress!, count: byteCount)
defer
{
    buffer.baseAddress?.deallocate()
}

but could be greatly simplified to

let buffer = UnsafeMutableBufferPointer<UInt8>.allocate(count: byteCount)
defer
{
    buffer.deallocate()
}
  • deprecating subscripts on singular pointers

Code using pointer subscripts can be automigrated by replacing ptr[offset] with (ptr + offset).pointee.

  • adding allocators and deallocators to buffer pointers

This change is purely additive.

  • removing sized deallocators from singular pointers

This change is source-breaking, but this is a Good Thing™. The current API encourages incorrect code to be written, and sets us up for potentially catastrophic source breakage down the road should the implementations of deallocate(capacity:) and deallocate(bytes:alignedTo:) ever be “fixed”, so users should be forced to stop using them as soon as possible.

  • moving sized sized memorystate functions to buffer pointers

This change is source-breaking, but can be migrated in a similar fashion as the allocator type methods.

  • adding unsized memorystate functions

This change is purely additive.

  • renaming raw pointer argument labels

This change is source breaking but can be trivially automigrated.

  • removing UnsafeMutablePointer.initialize<C>(from:)

This change is source-breaking only for the reason that such code should never have compiled in the first place.

Effect on ABI stability

Removing sized deallocators changes the existing ABI. Removing the deprecated UnsafeMutablePointer<T>.allocate(capacity:) and singular pointer subscripts will change the ABI.

Effect on API resilience

All proposed changes in this proposal change the public API.

Removing sized deallocaters right now will break ABI, but offers increased ABI and API stability in the future as reallocator methods can be added in the future without having to rename deallocate(capacity:) which currently occupies a “reallocator” name, but has “free()” behavior.

This proposal seeks to tackle all the breaking changes required for such an overhaul of Swift pointers, and leaves unanswered only additive changes that still need to be made in the future, reducing the need for future breakage.

Alternatives considered

  • keeping sized allocators on singular pointers

This would reduce the impact of this proposal on existing code, but has the downside of introducing two ways to do the exact same thing. Singular pointers should allocate singular instances, and vector pointers should allocate multiple instances. It is better to remove the duplicated functionality sooner rather than later.

  • keeping singular pointer subscripts

There are currently two ways to dereference a Swift pointer — through subscript and through .pointee. Dereferencing through subscript makes less sense and is potentially misleading as explained earlier. It is better to remove the duplicated functionality sooner rather than later.

  • keeping sized deallocators and fixing the stdlib implementation instead

Instead of dropping the capacity parameter from deallocate(capacity:), we could fix the underlying implementation so that the function actually deallocates capacity’s worth of memory. However this would be catastrophically, and silently, source-breaking as existing code would continue compiling, but suddenly start leaking or segfaulting at runtime. deallocate(capacity:) can always be added back at a later date without breaking ABI or API, once users have been forced to address this potential bug.

  • adding an initializer UnsafeMutableBufferPointer<Element>.init(allocatingCount:) instead of a type method to UnsafeMutableBufferPointer

The allocator could be expressed as an initializer instead of a type method. However since allocation involves acquisition of an external resource, perhaps it is better to keep with the existing convention that allocation is performed differently than regular buffer pointer construction.

  • adding a move(count:) method to UnsafeMutableBufferPointer that deinitializes multiple instances and returns a new allocated and initialized buffer pointer.

Since this solution involves allocating memory, an instance method would be inappropriate, for similar reasons as why allocators are type methods and not initializers.

  • adding a move(count:) method to UnsafeMutableBufferPointer that deinitializes multiple instances and returns a Sequence.

Although it would be a nice round trip completion complementing the initialize<S>(from:S) method, this is a bit out of scope for this proposal, since it requires deciding on an appropriate return type. This can always be added at a later date without breaking ABI or API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment