Storing non-Sendable properties in Sendable classes

The following generates a warning during compilation (error with Swift 6 mode):

final class Yolo : Sendable {

   init(fileManager: FileManager) {
      self.fileManager = fileManager
   }

   let fileManager: FileManager

}
print(Yolo(fileManager: .default))

However, if I store a closure that returns the same thing, I get no such warning/error:

final class Yolo2 : Sendable {

   init(fileManagerFactory: @escaping @Sendable () -> FileManager) {
      self.fileManagerFactory = fileManagerFactory
   }

   let fileManagerFactory: @Sendable () -> FileManager
   var fileManager: FileManager {fileManagerFactory()}

}
print(Yolo2(fileManagerFactory: { .default }))

I guess my questions would be:

  • Why is the closure “{ FileManager.default }” Sendable if FileManager is not Sendable?
  • Why is Yolo2 Sendable when Yolo is not? Both can use fileManager the same way from what I can tell, so they risk the same memory corruption error, don’t they?

Sendable Conformance means that your object is safe to be passed across multiple thread and be accessed asynchronously. If you have class that conforms to Sendable all of your properties has to be Sendable and Constant.

The reason why swift is complaining here is that FileManager is a class and it is NOT Sendable which makes it impossible for swift to guaranty that it will not be changed by some one else.
Imagine this scenario :

let manager = FileManager()
let yolo1 = Yolo(fileManager: manager)
let yolo2 = Yolo(fileManager: manager)
Task{
yolo1. changeFileManager()
}
Task{
yolo2. changeFileManager()
}

because both of them share the same manager instance and FileManager is not Sendable this might cause data race at run time.

On the other hand, your second implementation :

let fileManagerFactory: @Sendable () -> FileManager
var fileManager: FileManager {fileManagerFactory()}

this will always produce a fresh instance of FileManager every single time that you call yolo.fileManager thus it doesn't make data race issue.

Task{
 yolo1. changeFileManager()
}
Task{
 yolo2. changeFileManager()
}

this code will be safe to run cause every time you access yolo1.fileManager property a new instance is created :blush:

PS: when you put @Sendable before a closure declaration it means that your closure can't CAPTURE non-Sendable types but it might still produce(return) a non-Sendable type

1 Like

Thanks for this answer.

I still don’t get how this is safer though!
The closure effectively returns the same FileManager instance every time, so in you example, the two changeFileManager calls will indeed change the same file manager, won’t they?

Your closure will return a brand-new instance of FileManager every time. so the two calls will actually change 2 separate instances which will not affect the other one.

Yeah, there’s no guarantee that FileManager.default actually gives you the same instance each time. In practice I bet it does, meaning that whatever instance it returns must be Sendable-in-practice, but that still doesn’t mean that all FileManager subclasses are Sendable.

(Yes, I realize there are no public FileManager subclasses. Maybe Apple has some for testing or something though, we don’t know.)

1 Like

AFAIK, no:

let fileManagerFactory = { FileManager.default }
print("\(Unmanaged.passUnretained(fileManagerFactory()).toOpaque())")
print("\(Unmanaged.passUnretained(fileManagerFactory()).toOpaque())")

This prints the same address twice.

Interesting.
However what prevents me from doing a “FileManager” which does not return a Sendable-in-practice instance when accessing default?

Something like this:

import Foundation

final class NotSendable {

	static let `default`: NotSendable = .init()
	var accessCount = 0
	init() {}

}

final class Yolo2 : Sendable {

   init(notSendableFactory: @escaping @Sendable () -> NotSendable) {
      self.notSendableFactory = notSendableFactory
   }

	func letsBeDangerous() {
		notSendable.accessCount += 1
	}

   let notSendableFactory: @Sendable () -> NotSendable
   var notSendable: NotSendable {notSendableFactory()}

}

let yolo1 = Yolo2(notSendableFactory: { .default })
let yolo2 = Yolo2(notSendableFactory: { .default })
let t1 = Task{ yolo1.letsBeDangerous() }
let t2 = Task{ yolo1.letsBeDangerous() }
await t1.value
await t2.value
print(NotSendable.default.accessCount)

Unless I’m mistaken this is dangerous, is it not?
I am not warned about anything at all though (Swift 6.1).

EDIT:
I actually am prevented to compile with an error when compiling with -swift-version 6. Phew!
static property 'default' is not concurrency-safe because non-'Sendable' type 'NotSendable' may have shared mutable state

1 Like

To sum up, from what I understand, it works because FileManager.default is, in fact, Sendable, and it is the Foundation framework which guarantees it.

No, that’s not it either. It works because you’re not allowed to assume you’ll get the same instance every time. It is an implementation detail that you do and that it is thread-safe nonetheless. (If probably an implementation detail that many many apps have ended up relying on before there was Sendable checking.)

1 Like

this is not safe cause NotSendable is not Sendable but if you change it to something like this:

static var `default`: NotSendable { .init() }

now swift should be fine cause you are creating a new object every single time

By just digging in Foundation source code i just came up with this :

open class FileManager : @unchecked Sendable {
    // Sendable note: _impl may only be mutated in `init`
    private var _impl: _FileManagerImpl
    private let _lock = LockedState<State>(initialState: .init(delegate: nil))
    
    private static let _default = FileManager()
    open class var `default`: FileManager {
        _default
    }
//...

this should answer your question :smiling_face: