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

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.

4 Likes

I was actually headed towards something analogous to Array.withUnsafeBufferPointer(_:), maybe a synthesized Uniforms.withUnsafeBufferPointer(weights:_:) method on the struct.

1 Like

If you want a Swift struct Uniforms with the same memory layout as the C struct, and you want subscript access to its (statically allocated) weights, then you can start by writing some reusable code for managing "hacky statically allocated fixed size arrays in Swift", for example this:

protocol FixedSizeArray {
    associatedtype Element
    static var count: Int { get }
}
extension FixedSizeArray {
    var count: Int { return Self.count }
    subscript(unchecked index: Int) -> Element {
        get {
            return withUnsafeBytes(of: self) { (ptr) -> Element in
                let o = index &* MemoryLayout<Element>.stride
                return ptr.load(fromByteOffset: o, as: Element.self)
            }
        }
        set {
            withUnsafeMutableBytes(of: &self) { (ptr) -> Void in
                let o = index &* MemoryLayout<Element>.stride
                ptr.storeBytes(of: newValue, toByteOffset: o, as: Element.self)
            }
        }
    }
    subscript(index: Int) -> Element {
        get {
            typealias ML<T> = MemoryLayout<T>
            precondition(ML<Self>.alignment == ML<Element>.alignment)
            precondition(Self.count * ML<Element>.stride == ML<Self>.size)
            precondition(0 <= index && index < Self.count)
            return self[unchecked: index]
        }
        set {
            typealias ML<T> = MemoryLayout<T>
            precondition(ML<Self>.alignment == ML<Element>.alignment)
            precondition(Self.count * ML<Element>.stride == ML<Self>.size)
            precondition(0 <= index && index < Self.count)
            self[unchecked: index] = newValue
        }
    }
}

With that in place, you can now do this for your particular example:

struct Float_x51 : FixedSizeArray {
    static var count: Int { return 51 }
    typealias Element = Float
    private var _storage = (
        Float(0), Float(0), Float(0), Float(0), Float(0),
        Float(0), Float(0), Float(0), Float(0), Float(0),
        Float(0), Float(0), Float(0), Float(0), Float(0),
        Float(0), Float(0), Float(0), Float(0), Float(0),
        Float(0), Float(0), Float(0), Float(0), Float(0),
        Float(0), Float(0), Float(0), Float(0), Float(0),
        Float(0), Float(0), Float(0), Float(0), Float(0),
        Float(0), Float(0), Float(0), Float(0), Float(0),
        Float(0), Float(0), Float(0), Float(0), Float(0),
        Float(0), Float(0), Float(0), Float(0), Float(0),
        Float(0)
    )
}

struct Uniforms {
    var weights: Float_x51
    init() {
        weights = Float_x51()
    }
}


func test() {
    var uniforms = Uniforms()

    print(MemoryLayout.size(ofValue: uniforms)) // 204 == 51 * 4 bytes

    for i in 0 ..< uniforms.weights.count {
        uniforms.weights[i] = Float.random(in: -1 ... 1)
    }

    for i in 0 ..< uniforms.weights.count {
        print(uniforms.weights[i])
    }
}
test()

Note that the FixedSizeArray's regular (checked) subscript checks that any given fixed size array's Element and count matches its memory layout, so it's perfectly safe! :nerd_face: :upside_down_face:

Minor tweak to your checks:

- precondition(Self.count * ML<Element>.stride == ML<Self>.size)
+ precondition(Self.count * ML<Element>.stride == ML<Self>.stride)

or perhaps

- precondition(Self.count * ML<Element>.stride == ML<Self>.size)
+ precondition(Self.count == 0 ? (ML<Self>.size == 0) : ((Self.count - 1) * ML<Element>.stride + ML<Element>.size  == ML<Self>.size))

As demonstrated here:

  1> struct X { var a: Int64; var b: Int8 }
  2> MemoryLayout<X>.size
$R0: Int = 9
  3> MemoryLayout<X>.stride
$R1: Int = 16
  4> MemoryLayout<(X, X)>.stride 
$R2: Int = 32
  5> MemoryLayout<(X, X)>.size
$R3: Int = 25
  6> MemoryLayout<(X, X, X)>.stride
$R4: Int = 48
  7> MemoryLayout<(X, X, X)>.size
$R5: Int = 41
2 Likes

Ah, thanks! And maybe the alignment of Element should be allowed to be less than that of Self?

And all checks except the bounds check could of course be moved to the initializer instead of the subscript ...

1 Like

Depends what you're going for. If this is really only meant to be used with homogeneous tuples, it'll always be equal.

1 Like

Hello Jens,

I appreciate the suggestion, but unless I'm missing something I don't think this really solves the issue. In your example, I'd still have to type out 51 Floats at least once, which is fine, but what if I decide to change my number of weights to 5001? In the method that I suggested previously (modified to include Jordan's suggested preconditions), I wouldn't have to change anything, but in your solution, I would have to create a Float_x5001, and type out 5001 Floats!

I see what you mean. Your solution might be simpler for your specific use case.

But that Float_x51 was just meant as an example of a type that can implement the FixedSizeArray protocol. In your scenario that would have been the Uniforms struct imported from C.

That is, in your project you have this:

// my-c-types.h

typedef struct {
    float weights[51];
} Uniforms;

And you have a bridging header file with #import "my-c-types.h".

Then, you'd use the FixedSizeArray protocol as follows (no matter if the C array has 51, 42 or 123 elements):

extension Uniforms : FixedSizeArray { // This is all you have to do to
    static var count: Int { 51 }      // add subscript access to a
    typealias Element = Float         // C imported type like Uniforms.
}

func test() {
    var u = Uniforms()
    for i in 0 ..< u.count {
        u[i] = Float(i) // Or whatever you'd like to set the values to.
    }
}
test()

And, as long as you use the regular subscript (rather than the unchecked one) it will trap if you accidentally specified a count or Element that doesn't match the memory layout of the C struct.

Since the protocol has subscripts and count, it can easily be extended to implement RandomAccessCollection.

Like this.
protocol FixedSizeArray : RandomAccessCollection {
    associatedtype Element
    static var count: Int { get }
}
extension FixedSizeArray {
    public var count: Int { return Self.count }
    public var startIndex: Int { 0 }
    public var endIndex: Int { Self.count }

    public subscript(unchecked index: Int) -> Element {
        get {
            return withUnsafeBytes(of: self) { (ptr) -> Element in
                let o = index &* MemoryLayout<Element>.stride
                return ptr.load(fromByteOffset: o, as: Element.self)
            }
        }
        set {
            withUnsafeMutableBytes(of: &self) { (ptr) -> Void in
                let o = index &* MemoryLayout<Element>.stride
                ptr.storeBytes(of: newValue, toByteOffset: o, as: Element.self)
            }
        }
    }
    public subscript(index: Int) -> Element {
        get {
            typealias ML<T> = MemoryLayout<T>
            precondition(ML<Self>.alignment == ML<Element>.alignment)
            precondition(Self.count * ML<Element>.stride == ML<Self>.stride)
            precondition(0 <= index && index < Self.count)
            return self[unchecked: index]
        }
        set {
            typealias ML<T> = MemoryLayout<T>
            precondition(ML<Self>.alignment == ML<Element>.alignment)
            precondition(Self.count * ML<Element>.stride == ML<Self>.stride)
            precondition(0 <= index && index < Self.count)
            self[unchecked: index] = newValue
        }
    }
}

If the Uniforms C struct had contained not only the array, you'd have to modify the protocol so that you conform by also specifying a byteOffset to where the array starts (as well as count and Element). But in such cases you'd probably want to use an all together different approach.

2 Likes

Ah I see now, thank you for clarifying!

To round this out, I wrote this as a global function, so that structs with tuple pseudo-arrays can be accessed without having to extend each one explicitly. My version:

func withUnsafeMutableBufferPointer<Struct,Element,Tuple,R>(
    to value: inout Struct,
    at property: WritableKeyPath<Struct,Tuple>,
    using type: Element.Type,
    _ body: (UnsafeMutableBufferPointer<Element>) throws -> R) rethrows -> R {
    return try withUnsafeMutablePointer(to: &value [keyPath: property]) {
        let count = MemoryLayout<Tuple>.stride / MemoryLayout<Element>.stride
        return try $0.withMemoryRebound(to: Element.self, capacity: count) {
            return try body(UnsafeMutableBufferPointer(start: $0, count: count))
        }
    }
}

used like this:

    struct Uniforms { // assume this came from an imported header
        var weights: (Float, Float, Float, Float)
    }

    var uniforms = Uniforms(weights: (1,2,3,4))
    print("Uniforms: \(uniforms)") // 'Uniforms: Uniforms(weights: (1.0, 2.0, 3.0, 4.0))'
    withUnsafeMutableBufferPointer(to: &uniforms, at: \.weights, using: Float.self) {
        for index in 0 ..< $0.count {
            $0 [index] *= 2
        }
    }
    print("Uniforms: \(uniforms)") // 'Uniforms: Uniforms(weights: (2.0, 4.0, 6.0, 8.0))'

Ideally, though, I'd like to see the C header importer conform such structs to a protocol that provides this functionality as an instance method. I'd also like to see a distinction between accessing a fixed size C array (Unsafe[Mutable]BufferPointer) and a variable size C array (Unsafe[Mutable]Pointer).

2 Likes

I have tested it on my C structure

struct FixedCell {
    simd_int2 adress;
    int particleCount;
    struct Particle particles[100];
};

And it works perfect.

k: Particle(velocity: SIMD2<Float>(1.0, 1.0), position: SIMD2<Float>(2.0, 2.0))
k: Particle(velocity: SIMD2<Float>(3.0, 3.0), position: SIMD2<Float>(4.0, 4.0))
k: Particle(velocity: SIMD2<Float>(4.0, 4.0), position: SIMD2<Float>(5.0, 5.0))
k: Particle(velocity: SIMD2<Float>(0.0, 0.0), position: SIMD2<Float>(0.0, 0.0))
k: Particle(velocity: SIMD2<Float>(0.0, 0.0), position: SIMD2<Float>(0.0, 0.0))
k: Particle(velocity: SIMD2<Float>(0.0, 0.0), position: SIMD2<Float>(0.0, 0.0))
...

Nonetheless, the memory allocation for such structures I forced to do in C module.

When typing by hand becomes too painful, I would just create a little code-generator widget for this kind of thing. Then I would run it, copy and paste its output. :slight_smile:

weightsTuple = tuple51FromArray (array)
// Tuple51FromArray.swift
//
// *** Widget generated code - Do not edit ***
//
func tuple51FromArray (array:[Float]) -> (Float, Float, Float, ...) {
     return (array[0], array[1], array[2], ...)
}