1
votes

I'm calling a C function in a library which requires pointers to uint8_t buffers. I'm attempting to refer to UInt8 arrays in Swift as follows (to later pass to the C function):

import Foundation

struct CStruct {
    let val: UnsafeMutablePointer<UInt8>
    let count: UInt32
}

var structs = [CStruct]()
var datas = [[UInt8]]()

for _ in 0..<10 {
    // In the real application, Data contains data rather than being zero length
    let d = Data()
    var data = [UInt8](d)
    // I don't want the `data` buffers to be deallocated, so I store them in an array.
    datas.append(data)
    structs.append(CStruct(val: &data, count: UInt32(data.count)))
}

print("Structs: \(structs)")
// pass the structs to a C function which processes them

This gives the warning

Inout expression creates a temporary pointer, but argument 'val' should be a pointer that outlives the call to 'init(val:count:)'

I can't use data.withUnsafeMutableBufferPointer as the pointer it provides would be out of scope outside the closure. I can't use Unmanaged as that only operates on classes, and Array is a struct. I'm also not sure that adding the data array to an array in the parent scope will extend its lifetime (as it is a struct). What is the correct way to achieve this?


edit:

My solution, based on @bscothern's answer:

import Foundation

struct CStruct {
    let val: UnsafeMutablePointer<UInt8>
    let count: UInt32
}

var structs = [CStruct]()
var dataStorage: [UnsafeMutablePointer<UInt8>] = []

for _ in 0..<10 {
    // In the real application, Data contains data rather than being zero length
    let d = Data()
    let ptr = UnsafeMutablePointer<UInt8>.allocate(capacity: d.count)
    d.withUnsafeBytes { (buff) -> Void in
        ptr.initialize(from: buff.bindMemory(to: UInt8.self).baseAddress!, count: d.count)
    }
    dataStorage.append(ptr)
    structs.append(CStruct(val: ptr, count: UInt32(d.count)))
}
defer {
    for ptr in dataStorage {
        ptr.deallocate()
    }
    dataStorage = []
}

print("Structs: \(structs)")
// pass the structs to a C function which processes them

Perhaps there is a better way to copy from Data?

1

1 Answers

1
votes

There are lots of ways you could solve this issue but the root cause is that you need to allocate a buffer and pass that in. You will then need to have a way to know when you are cleaning that up so you don't leak memory.

A fairly simple way to address this is to use a class type to manage the lifetime of your CStruct instances. Here is an example of a way that will make most of this memory management automatic by using a class wrapper.

import Foundation

struct CStruct {
    let val: UnsafeMutablePointer<UInt8>
    let count: UInt32
}

final class CStructLifetimeManager {
    private(set) var cStruct: CStruct // This needs to be mutable so it can be deallocated later on.

    private init(count: Int) {
        var val = UnsafeMutablePointer<UInt8>.allocate(capacity: count)
        for i in 0..<10 {
            val[I] = 0
        }
        self.cStruct = CStruct(val: val, count: UInt32(count))
    }

    deinit {
        cStruct.val.deinitialize(count: 10) // Not needed if you really just have UInt8 types since they are trivial
        cStruct.val.deallocate()
    }

    static func createStruct(count: Int) -> CStructLifetimeManager {
        CStructLifetimeManager(count: count)
    }
}

var structs = [CStruct]()
var structLifetimeManagers = [CStructLifetimeManager]() // This now keeps the manager objects alive

for _ in 0..<10 {
    let cStructLifetimeManager = CStructLifetimeManager(count: 10)
    structs.append(cStructLifetimeManager.cStruct)
    structLifetimeManagers.append(cStructLifetimeManager)
}

print("Structs: \(structs)")

This issue with this approach is that structs and structLifetimeManagers mirror the same CStruct instances but they are still different. This means that it could stop existing in structLifetimeManagers an structs will then access invalid data.

So it would be better to remove the second array and just have the wrapper object managing lifetime be what you work with. And you only drop down into CStruct when needed. If this isn't possible you will just need to be very careful to make sure things don't go out of scope before they should.