Can ownership features streamline apis that provide a temporary value?

Hello,

I was discussing with a coworker of mine about withUnsafeBufferPointer,withContiguousStorageIfAvailable, and generally all withXxx methods whose closure input is only valid during the execution of the method. It is necessary to nest those functions whenever one needs to use multiple temporary values together. When the number of temporary values grows, this creates a "pyramid of doom" (a high level of nesting), and that's it. Recursion can help reducing the nesting level, but it's somewhat obfuscated.

I was wondering if the new ownership features would help streamlining those apis (i.e. remove nesting):

// Look Ma, no nesting!
let array = [1, 2, 3]
let pointer = array.unsafeBufferPointer
// print(array) // compiler error
// functionThatEscapesInput(pointer) // compiler error
2 Likes

Would love that!


Until there's a better solution I'm using this "sweep under the carpet" approach to make the pyramid of doom smaller.
extension Array {
    static func withUnsafeBufferPointer<T1, T2, T3, R>(_ a1: [T1], _ a2: [T2], _ a3: [T3], execute: (UnsafeBufferPointer<T1>, UnsafeBufferPointer<T2>, UnsafeBufferPointer<T3>) -> R) -> R {
        a1.withUnsafeBufferPointer { v1 in
            a2.withUnsafeBufferPointer { v2 in
                a3.withUnsafeBufferPointer { v3 in
                    execute(v1, v2, v3)
                }
            }
        }
    }
}
1 Like

Not exactly universal, but that works:

extension Array {
    consuming func unsafeBufferPointer() -> UnsafeBufferPointer<Element> {
        withUnsafeBufferPointer { $0 }
    }
}

func toPointer(_ array: consuming [Int]) {
    let pointer = array.unsafeBufferPointer()
    print(array) // compiler error
}

let array = [1, 2, 3]
toPointer(array)

The key here is that array itself should be passed as consuming, allowing this mechanism (if that's correct understanding) propagate behaviour downwards. The following code wouldn't prevent from accessing array:

let array = [1, 2, 3]
let pointer = array.unsafeBufferPointer()
print(array) // OK, valid

Yet how to express this in Swift (even as possible evolution) I have no idea.

If you consume a variable, it stays consumed until you reinitialize it. Your second example could not compile because array does not exist after you call unsafeBufferPointer. This also means that the buffer pointer is dangling,

What we really need is non-escapable types, then we could have a non-escapable wrapper over a buffer that could be returned from an array without needing a with function or invalidating the array.

1 Like

I think it will be more correct that it might not work in runtime, it will clearly compile, yet there is no guarantee it won't crash due to pointer manipulations.

Yep, maybe... I was trying to think how it could be expressed in Rust with lifetimes, but lifetimes still make me puzzled a lot there.

No, as far as the compiler is concerned a consumed variable is uninitialized. If you call a consuming method on an array, that array ceases to be and the compiler does check this statically.

The only way it would compile is if the compiler inserted an implicit copy of the array, which would just make the buffer extra dangling.

1 Like
Look at this beauty πŸ˜ƒ
struct WithUnsafeBufferPointer {
    static subscript<A>(_ a: [A]) -> WithUnsafeBufferPointer1<A> {
        .init(a: a)
    }
}
struct WithUnsafeBufferPointer1<A> {
    let a: [A]
    func execute<R>(callback: (UnsafeBufferPointer<A>) -> R) -> R {
        a.withUnsafeBufferPointer { va in
            callback(va)
        }
    }
    subscript<B>(_ b: [B]) -> WithUnsafeBufferPointer2<A, B> {
        .init(a: a, b: b)
    }
}
struct WithUnsafeBufferPointer2<A, B> {
    let a: [A], b: [B]
    func execute<R>(callback: (UnsafeBufferPointer<A>, UnsafeBufferPointer<B>) -> R) -> R {
        a.withUnsafeBufferPointer { va in
            b.withUnsafeBufferPointer { vb in
                callback(va, vb)
            }
        }
    }
    subscript<C>(_ c: [C]) -> WithUnsafeBufferPointer3<A, B, C> {
        .init(a: a, b: b, c: c)
    }
}
struct WithUnsafeBufferPointer3<A, B, C> {
    let a: [A], b: [B], c: [C]
    func execute<R>(callback: (UnsafeBufferPointer<A>, UnsafeBufferPointer<B>, UnsafeBufferPointer<C>) -> R) -> R {
        a.withUnsafeBufferPointer { va in
            b.withUnsafeBufferPointer { vb in
                c.withUnsafeBufferPointer { vc in
                    callback(va, vb, vc)
                }
            }
        }
    }
}

Usage:

let a = [1], b = ["2"], c = [3]

WithUnsafeBufferPointer[a].execute { pa in
    // whatever
}
WithUnsafeBufferPointer[a][b].execute { pa, pb in
    // whatever
}
WithUnsafeBufferPointer[a][b][c].execute { pa, pb, pc in
    // whatever
}

Not ideal, obviously, e.g. if you need more than three parameters, or combine withUnsafeBufferPointer with, say, withTemporaryAllocation, or withUnsafeBytes, etc – you'd need to create another ad-hoc tailored one-use workaround :frowning: