I am trying to using the new sending parameter keyword together with Mutex to handle one Opaque pinter creating another but so far have not been able to get it working and am now wondering if I've misunderstood how it is supposed to work.
public final class Database {
let database: Mutex<OpaquePointer>
public init() {
var database: OpaquePointer?
try cppCreateDatabase(database) // creates and sets database
self.database = .init(database!)
}
public func createTransaction() -> Transaction {
try database.withLock {
var transaction: OpaquePointer?
cppCreateTransaction($0, &transaction) // creates and sets transaction
return try Transaction(database: self, transaction: transaction!)
}
}
}
public actor Transaction {
let database: Database
let transaction: OpaquePointer
init(database: Database, transaction: sending OpaquePointer) {
self.database = database
self.transaction = transaction
}
}
I am still getting a Sending 'transaction' risks causing data races in the createTransaction() function though.
I think the issue is cppCreateTransaction could now hold a reference to transation, which prevents it from being safely sent. I've struggled with similar issues.
You need some way to express that transaction is being initialized by cppCreateTransaction, but then not referenced again. I don't think this is possible to do with an inout, but maybe a wrapper that cheats just a little and ultimately returns a sending OpaquePointer?
Just to make sure I really am understanding, is the problem that cppCreateTransaction touches transaction? If you comment out that line from your original code, things compile?
cppCreateTransaction does touch transaction, it creates the transaction and then assigns it to the inout parameter, without it there is no transaction (not sure if that answers your question).
As an aside, this does compile, but still relies on nonisolated(unsafe):
public func startTransaction() throws -> Transaction {
var wrapper: (OpaquePointer) -> sending OpaquePointer? = { database in
nonisolated(unsafe) var transaction: OpaquePointer?
cppCreateTransaction(database, &transaction)
return transaction
}
return database.withLock {
if let transaction = wrapper($0) {
return Transaction(database: self, transaction: transaction)
} else {
throw DatabaseError.noTransaction
}
}
}
I have also found it particularly furstrating to work with non-Swift code that isn't also under your control. Clang supports many attributes for annotating C-based languages with concurrency stuff, but if you cannot modify cppCreateTransaction to instead do this sending return, then this could be about as good as you can do I think.