How to constrain generic to value type

I opened an old project and got these warnings that weren’t there before

Forming ‘UnsafeRawPointer’ to a variable of type ‘[T]’; this is likely incorrect because ‘T’ may contain an object reference

extension MTLDevice {
    func makeEZBuffer<T>(array: [T]) -> MTLBuffer? {
        self.makeBuffer(
            bytes: array,    //<<<< warning
            length: MemoryLayout<T>.stride * array.count, 
            options: [.storageModeShared])
    }
    func makeEZBuffer<T>(single: T) -> MTLBuffer? {
        var v = single
        return self.makeBuffer(
            bytes: &v,      //<<<< warning
            length: MemoryLayout<T>.stride, 
            options: [.storageModeShared])
    }
}
//makeBuffer signature
func makeBuffer(
    bytes pointer: UnsafeRawPointer,
    length: Int,
    options: MTLResourceOptions = []
) -> MTLBuffer?

I get the warning and it seems I could fix it by constraining the generic T to value types, but how? I could constrain to classes with <T: AnyClass> but there’s no <T: AnyValue> as far as I know. What should I do?

I could make a marker protocol that’s added to all my structs used with those functions, is that the solution or is there a more flexible way?

To my knowledge there is no way to express AnyValue, alas.

Could you work around it by using Array.withUnsafeBytes(_:) (or similar)? I'm assuming you don't need to escape the pointer to the raw bytes.

2 Likes

On the terminology front, AnyValue (if existed) is probably not what you want... Many things with references inside would be "Value type" (take String for example). "AnyValueWithNoObjectReferences" or some "AnySimpleValue" would be more appropriate. Not that we have it though.


Try playing with this signature:

func foo<T>(_ v: UnsafePointer<T>) {}

It doesn't seem to give a warning.


On a different note: you didn't show us your "makeBuffer" so we could only assume you are not using the pointer passed to "makeBuffer" in the resulting MTLBuffer directly, you are allocating the new "length" bytes and copy the "length" bytes referenced by the "pointer", all within "makeBuffer" call and not using the "pointer: afterwards. Hope that's the case.

2 Likes

I’m not sure if I know how to use that. I tried like this…

func makeEZBuffer<T>(array: [T]) -> MTLBuffer? {
    array.withUnsafeBytes { ptr in
        return self.makeBuffer(
            bytes: ptr, 
            length: MemoryLayout<T>.stride * array.count, 
            options: [.storageModeShared])
    }
}

Which gives the error “cannot convert UnsafeRawBufferPointer to UnsafeRawPointer”. And for the other function with a single element I don’t get withUnsafeBytes autocompleting from it. Pointer stuff I barely understand, these functions are convenience functions so I don’t have to deal with them.

I hadn’t considered there might be a pointer solution to the warnings but such a solution might be over my head.

Yes! I see that. However then the parameter isn’t an array so there’s no count property to calculate the length.

The makeBuffer function, MTLDevice, MTLBuffer are all from Apples MetalKit and I’ve made several ‘EZ’ extensions to make them easier to use. …I see you’re replying, you’re too fast!

let urbp: UnsafeRawBufferPointer = ...
let urp: UnsafeRawPointer = urbp.baseAddress!

Bear in mind that "Unsafe" stuff in Swift is quite dangerous, you have to understand it to use it right. Show us the selection of the call sites to your makeEZBuffer(array:) and makeEZBuffer(single:) (e.g. why it needs to be generic in the first place) and the makeBuffer implementation.

2 Likes

Ah, I see, makeBuffer is Metal's API. This should do the trick:

extension MTLDevice {
    func makeEZBuffer<T>(array: [T]) -> MTLBuffer? {
        array.withUnsafeBytes { p in
            makeBuffer(
                bytes: p.baseAddress!,
                length: MemoryLayout<T>.stride * array.count,
                options: .storageModeShared
            )
        }
    }
    func makeEZBuffer<T>(single: UnsafePointer<T>) -> MTLBuffer? {
        makeBuffer(
            bytes: single,
            length: MemoryLayout.size(ofValue: single),
            options: .storageModeShared
        )
    }
}

and this if you want to keep your current use-sites intact (passing single values without &):

    func makeEZBuffer<T>(single: T) -> MTLBuffer? {
        var single = single
        return makeEZBuffer_internal(single: &single)
    }
    private func makeEZBuffer_internal<T>(single: UnsafePointer<T>) -> MTLBuffer? {
        makeBuffer(
            bytes: single,
            length: MemoryLayout.size(ofValue: single),
            options: .storageModeShared
        )
    }
3 Likes

YES YES that did! Thank you. I see now how to use withUnsafeBytes and handling the single one. Will have test it tomorrow but the warning went away.

Thanks again, I have to go to bed now but will study this more in-depth then!

According to the proposal for this change, we should get a BitwiseCopyable layout constraint soon that can be used to constrain generic parameters to trivial value types (like the AnyValue type you were proposing). For now, though, you'll need to use more explicit workarounds like Array.withUnsafeBytes and withUnsafePointer.

4 Likes

If you're using Array.withUnsafeBytes, then you don't need to calculate MemoryLayout<T>.stride * array.count — you can just use the count property of UnsafeRawBufferPointer.

Additionally, your code for makeEZBuffer(single:) has a bug: MemoryLayout.size(ofValue: single) doesn't return the size of T, it returns the size of UnsafePointer<T>. Bugs like this are why it's generally a bad idea to use the same variable name for two conceptually different variables.

Also, since the Metal Shading Language is a C++-based language, it's probably a good idea to use the stride of the type for the length of the buffer, not the size. You also don't need to write two functions for makeEZBuffer(single:); you can use withUnsafePointer instead.

Here's my code:

extension MTLDevice {
    func makeEZBuffer(array: [some Any]) -> MTLBuffer? {
        return array.withUnsafeBytes {
            return makeBuffer(
                bytes: $0.baseAddress!,
                length: $0.count,
                options: .storageModeShared)
        }
    }
    func makeEZBuffer<T>(single: T) -> MTLBuffer? {
        return withUnsafePointer(to: single) {
            return makeBuffer(
                bytes: $0,
                length: MemoryLayout<T>.stride,
                options: .storageModeShared)
        }
    }
}
4 Likes

Good catches! Yet another example "unsafe" stuff is dangerous in Swift.
I'd remove the explicit "returns" as these are single statement expressions.
Why did you change makeEZBuffer signature to take [some Any] from the generic form, is that preferable?

This can't be a good idea... You've allocated only the "size" bytes of data but telling Metal that there are "stride" bytes available (but they are not!)

struct S {
     var x: Int = 0
     var y: Int8 = 0
}

print(MemoryLayout<S>.size)     // 9
print(MemoryLayout<S>.stride)   // 16

var a = S()
var b = S()
// an equivalent of what Metal API will do inside
memmove(&a, &b, MemoryLayout.stride(ofValue: a)) // oops
3 Likes

Hint

func foo <T> (u:T) {
}

:=

func foo <T:Any> (u:T) {
}

:=

func foo  (u:some Any) {
}
2 Likes

Thanks everyone, I got it working with Teras code and now mixed in 1-877s code. There was actually 6 functions with this warning, 2 each of makeEZBuffer, setEZVertexBytes, and copyFrom. setEZVertexBytes is the only pair being used in this project and it’s still running properly!

About size vs stride, that’s something I’ve been long undecided on. I have a vague recollection reading that Metal wants its memory in ‘aligned’ units, which to me means a struct of 13 bytes should be padded up to 16 or whatever the alignment is. That’s why I was using stride. However I share Teras concern that this is then copying memory from beyond the end of the struct. The thing is I did some experiments before with structs of varied sizes and both size and stride worked. Neither crashed or drew wrong stuff in Metal, so that’s why I’m undecided which is correct.

aside: I’m also doing something else i believe is a no-no ;) These structs I’m sending to Metal are simply Swift structs and I heard that the order of properties in memory isn’t guaranteed, so when accessing it on the Metal shader C++ side it could possibly be mangled. The solution is to somehow link with C++ defined structs which guarantee order but I’m using iPad Playgrounds atm and can’t do that. Yet thankfully so far these Swift structs have always yielded memory with properties in the order defined, never had a problem with this possible issue🤞. (Today though I open an Xcode device and can put this concern to rest).

One last small question I ran into while rectifying my functions. In the copyFrom function pair it introduced the warning “Result of call to ‘withUnsafeBytes/Pointer’ is unused”. These functions copy the memory of structs to the contents() pointer of an existing MTLBuffer. memcpy returns an UnsafeMutableRawPointer? but I wasn’t being warned about this previously.

extension MTLBuffer {
    func copyFrom(array: [some Any]) {
        array.withUnsafeBytes { //<< warning
            memcpy(self.contents(), $0.baseAddress!, $0.count)
        }
    }
    func copyFrom<T>(single: T) {
        withUnsafePointer(to: single) { //<< warning
            memcpy(self.contents(), $0, MemoryLayout<T>.stride)
        }
    }
}

My fix is to add “let _ =“

let _ = array.withUnsafeBytes {

But it also works to add it to the memcpy line

let _ = memcpy(self.contents(), $0.baseAddress!, $0.count)

Is this the way you’d fix it and is there any meaningful preference to which line to add it to?

1 Like

Swift aligns quantities on their natural boundaries, e.g. in the struct { Int8, Int32, Int8 } the Int32 field will be at offset 4 instead of offset 1. Yes, C or C++ provided struct would be best to have the guaranteed layout, though personally I'd just put the relevant preconditions:

struct S {
    var byte: Int8
    var field: Int64
}
precondition(MemoryLayout<S>.offset(of: \.field) == 8)

and only start worrying about it when it fails (which may well be years from now or never). YAGNI principle :-)

memcpy returns the first argument, and withUnsafe returns whatever the closure returns, so the cleanest workaround is to assign memcpy to "_ =" (let is optional in this case):

_ = memcpy(...)

make sure contents() has enough bytes, if it doesn't it won't end well. Maybe you can doublecheck yourself here: make contents() taking size parameter and check inside that it doesn't exceed the allocated space.

1 Like

This I can't explain:

func foo() {
    malloc(0) // 🔶 Result of call to 'malloc' is unused
    // public func malloc(....) -> UnsafeMutableRawPointer!
}

func bar() {
    memcpy(nil, nil, 0) // 🤔 no warning here!
    // public func memcpy(....) -> UnsafeMutableRawPointer!
}

The autogenerated swift interface doesn't have @discardableResult for "memcpy", yet somehow I am not getting an unused result warning.