Converting a String to a (large) Int8 tuple

The following:

char deviceName[VK_MAX_PHYSICAL_DEVICE_NAME_SIZE];

where

VK_MAX_PHYSICAL_DEVICE_NAME_SIZE = 256

gets translated as a large tuple:

(Int8, ..., Int8)

How can I transform a String into that tuple?

Tried

Array(string.utf8)

But I get type mismatch:

error: cannot convert value of type 'Array<String.UTF8View.Element>' (aka 'Array< UInt8>') to expected argument type '(Int8, ..., Int8,)

var tuple: (UInt8, UInt8, UInt8, UInt8, UInt8) = (0, 0, 0, 0, 0)

withUnsafeMutablePointer(to: &tuple) { pointer in
	let bound = pointer.withMemoryRebound(to: UInt8.self, capacity: 5) { $0 }
	"Hello".utf8.enumerated().forEach { (bound + $0.offset).pointee = $0.element }
}

print(tuple) // (72, 101, 108, 108, 111)

Obviously, you need to know the size in advance, otherwise this won't work.


var tuple: (Int8, Int8, Int8, Int8, Int8) = (0,0,0,0,0)
let str = "abcdefghijk"
let _: Void = withUnsafeMutableBytes(of: &tuple) { ptr -> Void in
    str.utf8CString.withUnsafeBytes { strPtr -> Void in 
        ptr.baseAddress!.copyMemory(from: strPtr.baseAddress!, 
                                    byteCount: min(strPtr.count, ptr.count))
    }
}

print(tuple)

I’m using the old version of Swift that is on the iPad Playground App, but I think in Swift 5 String.utf8 has a direct “withUnsafeBufferPointer” method you can use.

EDIT: Oh, I didn’t see the version above, but this way avoids the intermediate Array.

1 Like

You don't need to create an intermediate array actually, you can just do "Hello".utf8.enumerated().forEach { ... } as well.

Let me revise my answer.

It’s documented that the pointer value from withMemoryRebound is only valid within the closure... in practice however it still works, so I don’t know if you want to take that into consideration.

The biggest trouble is going to be initialising a 256-byte tuple. It’s probably best to write a little C shim for that.

You can do it from Swift, but it’s pretty ugly and doesn’t get optimised well.

typealias BigTuple = (Int8,Int8,Int8,Int8,Int8,Int8,Int8)
let BigTupleSize = 7

let p = UnsafeMutablePointer<BigTuple>.allocate(capacity: 1)
p.withMemoryRebound(to: Int8.self, capacity: BigTupleSize) { p2 in p2.assign(repeating: 0, count: BigTupleSize) }

// we’re relying on value semantics to make the compiler copy an
// independent tuple out of the memory we just allocated.
let tup = p[0]
p.deallocate()
print(tup)
1 Like

Hmm I suppose the forEach can be moved inside withMemoryRebound's closure.

withUnsafeMutablePointer(to: &tuple) { pointer in
	pointer.withMemoryRebound(to: UInt8.self, capacity: 5) { bound in
		"Hello".utf8.enumerated().forEach { (bound + $0.offset).pointee = $0.element }
	}
}

I really wouldn't recommend relying on that. Unlike C, Swift only guarantees variable lifetime up to their last usage within a scope, and not necessarily to the very end of the scope.

Smuggling out the pointer from withMemoryRebound is dangerous because there's nothing telling the compiler that the lifetime of that memory should extend beyond the call to withMemoryRebound. Luckily, in this context, the lifetime of the memory is being preserved by the enclosing withUnsafeMutablePointer(to:_:).

If you wanted to do this, I would document it with a comment, because it's the kind of thing that very easily starts off being correct, but might eventually end up being mistakenly refactored in a way that breaks it.

I wasn’t advocating for using the pointer outside the closure.

1 Like

Please don't use the approach suggested by @suyashsrijan's current comment (at time of writing). This code is trivially vulnerable to buffer overflows when used with strings whose length is not known at compile time, and doesn't generalise.

@Karl's suggestion, possibly use of .utf8 to get the collection, is better as it explicitly bounds the length of the copy. The cleanest version of this code as of Swift 5.1 is this:

var tuple: (Int8, Int8, Int8, Int8, Int8) = (0,0,0,0,0)
let str = "abcdefghijk"

withUnsafeMutableBytes(of: &tuple) { ptr in
    ptr.copyBytes(from: str.utf8.prefix(ptr.count))
}

Note that this will not null-terminate the tuple. If you need to do that, you should unconditionally write a zero byte into the tuple's last entry, or preserve the the final entry by using a slightly shorter prefix.

5 Likes