Combing sending and Mutex in Swift 6

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.

1 Like

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?

1 Like

I've been messing around with a nested function/closure but still getting errors. The only option I am seeing is using nonisolated(unsafe) for

            var transaction: OpaquePointer?       

It feels really frustrating to put so much work into respecting isolation requirements to ultimately have to escape hatch the whole thing.

--
edit: scratch that, it is still unhappy even with nonisolated(unsafe)

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
            }
        }
    }
1 Like

Ok glad you found a workaround!

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.

1 Like