Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why are Swift's UnsafePointer and UnsafeBufferPointer not interchangeable?

I've been working quite a bit with Apple's neural net tools, which means I've been working quite a bit with unsafe pointers. I grew up with C, and I've been working with Swift for quite a while, so I'm comfortable using them, but there's one thing about them that has me totally stumped.

I can't figure out why there's any effort involved in deriving one kind of unsafe pointer from another. In general, it seems like it should be trivial, but the initializers for the different types are specific about what kind of input they'll take, and I'm having a hard time figuring out the rules.

An easy and specific example, perhaps the one that stumps me the most

// The neural net components want mutable raw memory, and it's easier
// to build them up from the bottom, so: raw memory
let floats = 100
let bytes = floats * MemoryLayout<Float>.size

let raw = UnsafeMutableRawPointer.allocate(byteCount: bytes, alignment: MemoryLayout<Float>.alignment)

// Higher up in the app, I want to use memory that just looks like an array
// of Floats, to minimize the ugly unsafe stuff everywhere. So I'll use
// a buffer pointer, and that's where the first confusing thing shows up:

// This won't work
// let inputs = UnsafeMutableBufferPointer<Float>(start: raw, count: floats)

// The initializer won't take raw memory. But it will take a plain old
// UnsafePointer<Float>. Where to get that? you can get it from the raw pointer
let unsafeMutablePointer = raw.bindMemory(to: Float.self, capacity: floats)

// Buf if that's possible, then why wouldn't there be a buffer pointer initializer for it?

// Of course, with the plain old pointer, I can get my buffer pointer
let inputs = UnsafeMutableBufferPointer(start: unsafeMutablePointer, count: floats)

Although I haven't been able to find any discussion of the theory behind the different kinds, I did find a clue in this tutorial. There's a diagram comparing the different types, which says that plain old UnsafePointer is strideable but not a collection, while UnsafeBufferPointer is a collection but isn't strideable.

I understand the concept of a collection that isn't strideable, like a set. But these two types of unsafe pointers allow subscripts. They work just like regular arrays, which sounds to me like they're both strideable collections. Perhaps there's a subtle clue in there that I'm missing.

Why is it not possible to get an UnsafeBufferPointer from a type that you can get an UnsafePointer from?

like image 258
SaganRitual Avatar asked Dec 22 '22 15:12

SaganRitual


1 Answers

The distinction between these types isn't quite as large as you might imagine. Conceptually, UnsafeBufferPointer can be viewed as a tuple of (UnsafePointer, Int), i.e., a pointer to a buffer of elements in memory with a known count. UnsafePointer, in contrast, is a pointer to an element in memory with an unknown count; UnsafePointer more closely represents what you might be used to as an arbitrary pointer in C: it may point to a single element, or to the start of a contiguous grouping of several elements, but on its own, there's no way to find out.

UnsafeBufferPointer having a known count also means that it is able to conform to Collection (which requires a known start and end) as opposed to UnsafePointer, which doesn't have that information.

Swift is very much a language of semantics, and places great emphasis on expressing in the type system knowledge about the tools you have available to you. As you point out, there are operations you can perform on one type and not another — this is by design, to make some operations more difficult to perform incorrectly.

These pointers are convertible, too:

  • UnsafeBufferPointer has a baseAddress which is an UnsafePointer: given a buffer, you can always "throw away" information about the count to get the underlying uncounted pointer
  • Given an UnsafePointer and a count, you can also express the existence of a buffer in memory with UnsafeBufferPointer.init(start:count:)

The general answer is: use the most specific pointer type that you can to represent the data that you have. It is usually preferred to that you use the Buffer variants of pointers if you're pointing to more than one element, and know how many you have. Similarly, if you're pointing to arbitrary bytes in memory (which may or may not have a type), you should use Raw pointers if possible. (And, of course, if you need to write to these locations in memory, you'll need to use the Mutable variants of those too.)

For more information, I highly recommend Andrew Trick's talk from WWDC 2020 about this subject: Safely manage pointers in Swift. He goes into great detail about the conceptual state machine representing the lifetimes of pointers in Swift, and how to convert between and use the pointer types correctly. (It's as close to the horse's mouth as you can get when on the topic.)


Separately, about your example code: @Sweeper correctly points out in a comment that if you're looking to allocate a buffer of Floats, you shouldn't allocate a raw buffer and bind its memory type. In general, allocating raw buffers not only runs the risk of mistaking the size of the buffer needed, but also risks not taking into account padding (which would have to be calculated manually for some types).

Instead, you should use UnsafeMutableBufferPointer.allocate(capacity:) to allocate the buffer, which you can then write to. It correctly takes into account alignment and padding, so you can't get it wrong.

The difference between raw memory and typed memory is very subtle in Swift, and Andy describes it much better in the linked talk than I can here, but tl;dr: raw memory is a collection of untyped bytes which could represent anything, whereas typed memory represents only values of a specific type (and cannot be safely reinterpreted arbitrarly, save for a few exceptions, a major departure from C!); you should almost never have to bind memory manually, and if you bind memory to non-trivial types, you're almost certainly doing it wrong. (Not that you're doing that here, but just a heads-up)


Finally, on the subject of Strideable vs. Collection, and subscripting — the fact that you can subscript into both matches the behavior of C, but has a subtle semantic distinction in Swift.

Subscripting into an UnsafePointer means largely what it does in C: an UnsafePointer knows its base type, and referencing a single location in memory, can calculate where the next object of that type in memory would be using the type's alignment and padding (this is what its Strideable conformance implies); subscripting allows you to access one of several contiguous objects in memory relative to the one the pointer refers to. Also, just like in C: because you don't know where a group of such objects ends, you can subscript arbitrarily using an UnsafePointer with no bounds checks — there simply wouldn't be any way to know whether an access you're trying to make is valid ahead of time.

On the other hand, subscripting through UnsafeBufferPointer is like accessing an element inside of a collection of elements in memory. Because there are clear bounds on where the buffer starts and ends, you get bounds checking, and indexing out of bounds on an UnsafeBufferPointer is much more clearly an error. Along those lines, a Strideable conformance on UnsafeBufferPointer wouldn't make much sense: the "stride" of a Strideable type indicates that it knows how to get to the "next" one, but there isn't a logical "next" buffer after an entire UnsafeBufferPointer.

So both of these types end up with a subscript operator that effectively performs the same operation, but semantically has a very different meaning.

like image 101
Itai Ferber Avatar answered May 16 '23 08:05

Itai Ferber