hmlongco / Factory

A new approach to Container-Based Dependency Injection for Swift and SwiftUI.
MIT License
1.93k stars 116 forks source link

@Injected Unable to Satisfy Sendable Conformance #248

Closed KhaledChehabeddine closed 3 weeks ago

KhaledChehabeddine commented 3 weeks ago

Hello,

First, I want to thank you for releasing version 2.4.0 to allow us to more easily satisfy concurrency errors/warnings when shifting to Xcode 16 and Swift 6. However, I am currently facing an issue when injecting dependencies using the @Injected property wrapper. The setup can be configured as follows:

Package Dependencies:

Swift Version: 6.0

Issue: When injecting a dependency that conforms to Sendable using @Injected, an error is generated by Xcode with the following message:

Stored property '_sendableClass' of 'Sendable'-conforming struct 'SendableStruct' has non-sendable type 'Injected\<SendableClass>'

This is an easy example to replicate the issue:

import Factory
import Foundation

final class SendableClass: Sendable {
}

extension Container {

    var sendableClass: Factory<SendableClass> {
        self {
            .init()
        }
    }
}

struct SendableStruct: Sendable {

    @Injected(\.sendableClass) private var sendableClass: SendableClass

    private let sameSendableClass: SendableClass

    init() {
        self.sameSendableClass = Container.shared.sendableClass()
    }
}

The example showcases the two ways I inject dependencies throughout my project. If I were to set the property directly in the initializer using the container's shared property, the sendable conformance would be satisfied and no error would occur. However, if I were to use the convenient @Injected property wrapper the error arises.

Is it not possible to make Injected conform to Sendable if it's type T itself also conforms to Sendable similar to how you did with Factory, as shown below:

extension Factory: Sendable where T: Sendable {}

Let me know if any additional information is needed to replicate the issue.

Thank you,

Khaled

tareksabry1337 commented 3 weeks ago

The problem here isn't actually in Factory, This is how Swift Sendability check works, property wrappers are only applied to variables and thus will never be Sendable. Let's take a closer look at your example

import Factory
import Foundation

final class SendableClass: Sendable {
}

extension Container {

    var sendableClass: Factory<SendableClass> {
        self {
            .init()
        }
    }
}

struct SendableStruct: Sendable {

    @Injected(\.sendableClass) private var sendableClass: SendableClass

    private let sameSendableClass: SendableClass

    init() {
        self.sameSendableClass = Container.shared.sendableClass()
    }
}

On the books this looks fine, but how about if we update it to this?

import Factory
import Foundation

final class SendableClass: Sendable {
}

extension Container {

    var sendableClass: Factory<SendableClass> {
        self {
            .init()
        }
    }
}

struct SendableStruct: Sendable {

    @Injected(\.sendableClass) private var sendableClass: SendableClass

    private let sameSendableClass: SendableClass

    init() {
        self.sameSendableClass = Container.shared.sendableClass()
    }

    mutating func doSomething() {
         sendableClass = SendableClass()
    }
}

Even though you won't do that because this is DI code, Swift has no idea about that, it sees a var and it gets upset because this is no longer Sendable across different threads, as opposed to when you define your properties are let there's no way to update their values because well...they are constants. If we ever get to the point where we allow property wrappers on let declarations (https://forums.swift.org/t/pitch-allow-accessor-macros-on-let-declarations/66684) we'll be allowed to rewrite

@Injected(\.sendableClass) private var sendableClass: SendableClass

to

@Injected(\.sendableClass) private let sendableClass: SendableClass

Which allows it to be really Sendable, until then there's no way to fix this error unless you mark your class @MainActor or actor or something.

KhaledChehabeddine commented 3 weeks ago

Thank you for the helpful response, it seems I misinterpreted the error from Xcode. Unfortunate it was a Swift language limitation, I really enjoyed using @Injected to help reduce the size of my initializers.