Convert an Array (of known fixed size) to a tuple

This is more for fun rather than something that you'd actually want to do, as I'm sure this is just exploiting some undefined behavior. The recommended way would be to manually unpack the elements manually as you've guessed.

let arr = [1, 2, 3, 4, 5, 6]

let t = arr.withUnsafeBytes {buf in
  return buf.bindMemory(to: (Int, Int, Int, Int, Int, Int).self)[0]
}

print(t)

Would still have to type in 51 Int declarations, based on the example. A more fundamental question might be how to translate between C-array and a Swift Array without using tuples.

Actually, I do think @nuclearace's suggestion is somewhat better than the manual case, only because if you pass an invalid type when creating an instance of Uniforms, you get a compiler error which tells you the proper type, which you can then copy and use as a typealias, for example:

    typealias Weights = (Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float, Float)
    let t = weights.withUnsafeBytes {
        return $0.bindMemory(to: Weights.self )[0]
    }

This is still not great, or even good, but it at least saves me from typing out the tuple manually :slight_smile:

There isn't really any Swifty solution to do this per se. In an ideal world, you would be able to make a function return a tuple with arity equal to the fixed size of the array. Unfortunately, there are a number of barriers to this actually becoming a reality and I don't expect there to be any way to do something even remotely similar to this any time in the near future. A lot of these barriers are rooted in missing features in Swift's generics system. The two main ones AFAIK are:

  1. Lack of generic value parameters
    • Because of this, you could not have the size of a length of a fixed-length array be statically known to the compiler as if it were on the same level as generic type parameter and so cannot return a tuple with the corresponding arity accordingly.
  2. Lack of variadic generics
    • As of now the arity of every function, tuple, etc. that you declare must be explicitly declared. Because of this, one function cannot return tuples of different arities and furthermore cannot change the arity of their return type based on their input.

To my knowledge, those are the two main things that are preventing you from doing that. Hopefully we will see some of these features in Swift in the future though :crossed_fingers:!

Edit:

I just realized that your question says an array of known fixed size... :roll_eyes:. Well to answer that, no, there is no way to do that right now other than the monotonous manual method and AFAIK, there are not any plans to add something like this to Swift in the near future.

Thanks for the input. If the addition of variadic generics would work in the unknown size case, then it seems that could also be a valid solution for the known fixed size case as well.

I was hoping someone would propose this as a solution, as I think the need for variadic generics is painfully obvious but it isn’t clear that any progress is being made on that front.

Seems to me we need something new here, for the C-to-Swift header import translation. There are other nasty cases as well. In Apple's audio frameworks, for example, some structs like AudioBuffer (in C) use the hokey convention of ending a variable-length C-structure by a C-array with 1 element.

Swift imports this as a 1-tuple, even though a 1-tuple doesn't actually exist, so the type ends up parenthesized, which mostly (though not completely) works as an unparenthesized type. However, it completely messes up the semantics of the structure, because an array field has been accidentally demoted to a scalar.

Perhaps there should be some annotation that could be added on the C side, that would bridge this sort of thing into Swift is a less-broken way.

1 Like

If you want to keep up to date with the state of variadic generics, even though it doesn't seem like there have been any activity on the thread in the past 3 months, @technicated was working on implementing it and it was being discussed on this pitch thread, Variadic Generics.

Here:

// 9 ints
func asCollection<T>(_ arr: (Int,Int,Int,Int,Int,Int,Int,Int,Int), _ perform: (UnsafeBufferPointer<Int>)->T) -> T {
  return withUnsafeBytes(of: arr) { ptr in
    let buffer = ptr.bindMemory(to: Int.self)
    return perform(buffer)
  }
}

func test() {
  let tup = (1,2,3,4,5,6,7,8,9)
  let _ = asCollection(tup) { coll in
    return coll.min()
  }
}

If you don't want to write out the tuple (as I imagine you don't), copy and paste the asCollection method in an extension on your enclosing type:

extension Uniforms {
  func withWeights<T>(_ perform: (UnsafeBufferPointer<Int>)->T) -> T {
    return withUnsafeBytes(of: weights) { ptr in
      let buffer = ptr.bindMemory(to: Int.self)
      return perform(buffer)
    }
  }
}

let u: Uniforms = ...
print(u.withWeights { $0.min() })

One important point is: do not return the pointer from withUnsafeBytes (or any pointer derived from that pointer). Only use it within the closure, do all your calculations and use your Collection APIs in there, then return the final result.

1 Like

Thanks for the suggestion, but it is not a solution to the stated issue. I do not want to convert a tuple to an array. I want to convert an array to a tuple.

For those following along, it turns out that this actually is doable:

func bindArrayToTuple<T, U>(array: Array<T>, tuple: inout U) {
    withUnsafeMutablePointer(to: &tuple) {
        $0.withMemoryRebound(to: T.self, capacity: array.count) {
            let ptr = UnsafeMutableBufferPointer<T>(start: $0, count: array.count)
            for (index, value) in array.enumerated() {
                ptr[index] = value
            }
        }
    }
}

I've tried your bindArrayToTuple. It's worked so far in my testing.
Thanks!

1 Like

You can do it more compactly thanks to array-pointer magic:

func bindArrayToTuple<T, U>(array: Array<T>, tuple: inout U) {
  withUnsafeMutablePointer(to: &tuple) {
    $0.withMemoryRebound(to: T.self, capacity: array.count) {
      $0.assign(from: array, count: array.count)
    }
  }
}
2 Likes

Awesome I will try this out!

Hey there,

Slight variation on your suggestion:

func bindArrayToTuple<T, U>(array: Array<T>, tuple: UnsafeMutablePointer<U>) {
    tuple.withMemoryRebound(to: T.self, capacity: array.count) {
        $0.assign(from: array, count: array.count)
    }
}

It's a little cleaner and the call site doesn't change.

1 Like

If you are going to use this sort of code, I suggest making sure that you're not overflowing the bounds of the tuple.

precondition(
  MemoryLayout<U>.stride >= array.count * MemoryLayout<T>.stride,
  "too many elements in array")
precondition(
  MemoryLayout<U>.alignment == MemoryLayout<T>.alignment,
  "elements are not the same type (this is an overly conservative test and it still failed)")
3 Likes

Good idea I'll add the preconditions.

If you are going to use this sort of code

Does this mean there is a better alternative?

Technically you're not allowed to use withMemoryRebound(to:capacity:) for a type of a different size; the correct use of pointers would be to go through UnsafeMutableRawPointer and use assumingMemoryBound(to:), since the tuple's elements are a "related type".

Besides that, though, there probably isn't a better solution if you care about performance. I sketched out something similar recently and wasn't really happy with it either. You could make it slightly safer by using reflection to check that the tuple really is homogeneous (all children have the same type and it's the same as the array's element type), and then check that the tuple's stride is the same as the element stride times the number of children in the tuple, to make sure it really is a tuple and not some monstrosity masquerading as a tuple.

(Even that isn't 100% safe, but it's closer. Mirror should probably have an enum asking what kind of value something is, like displayStyle but without being fakeable. But the language / standard library should also have built-in safe answers for homogeneous tuples / fixed-sized arrays, and the team knows that.)

3 Likes

Thanks, very informative :)

Going back the larger issue, "this sort of code" wouldn't be necessary if there was a better way of handling the translation for imported C structs.

While we're waiting for fixed-size Swift arrays, it occurs to me that C's own horrible solution to the equivalent problem might actually be a better solution for importing into Swift. For example, start from a slightly expanded original:

typedef struct {
    int count;
    float weights[51];
    int hash;
} Uniforms;

Rather than translating this into:

struct Uniforms {
    var count: Int
    var weights: (Float, Float, …, Float)
    var hash: Int
}

it might work better to translate into this:

struct Uniforms {
    var count: Int
    var _: (Float, Float, …, Float) // not an actual member, just a space placeholder
    var hash: Int
    var weights: UnsafeMutableBufferPointer<Float> {
        return … // a buffer pointer to the start of the placeholder in the struct
    }
}

Note that this might not be the best choice if the array is the last field in the struct, because the following kind of thing:

typedef struct {
    int count;
    float weights[0];
} Uniforms;

or

typedef struct {
    int count;
    float weights[1];
} Uniforms;

tends to indicate a variable length struct in C. In that case, the computed property would be better as an UnsafeMutablePointer, instead of an UnsafeMutableBufferPointer.

Also, I'm aware this is source-breaking, but in this case the current behavior is so horrible, it'd be a positive good to break source compatibility.

That idea's been floated before, but it wouldn't be correct since a value doesn't inherently have an address. The UnsafeMutableBufferPointer would be invalid as soon as the getter returned.

2 Likes
Terms of Service

Privacy Policy

Cookie Policy