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.
I was actually headed towards something analogous to Array.withUnsafeBufferPointer(_:)
, maybe a synthesized Uniforms.withUnsafeBufferPointer(weights:_:)
method on the struct.
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!
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
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 ...
Depends what you're going for. If this is really only meant to be used with homogeneous tuples, it'll always be equal.
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.
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
).
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.
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], ...)
}