Calling overridden extension methods

I'm working on a Path library based around the C APIs as opposed to just being a wrapper around Foundation's FileManager.

I've got the following protocols/classes that are failing some of my unit tests:

public protocol Openable {
    func open() throws
    func close() throws
}
public class Open<PathType: Path>: Openable {
    let path: PathType
    var fileDescriptor: Int32 = -1

    public init(_ path: PathType) throws {
        self.path = path
    }

    public func open() throws {
        fatalError("open() has not been implemented for the \(PathType.self) type")
    }

    public func close() throws {
        fatalError("close() has not been implemented for the \(PathType.self) type")
    }

    deinit {
        try? close()
    }
}
#if os(Linux)
import Glibc
private let cOpen = Glibc.open
private let cClose = Glibc.close
#else
import Darwin
private let cOpen = Darwin.open
private let cClose = Darwin.close
#endif

public extension Open where PathType == FilePath {
    public func open() throws {
        self.fileDescriptor = cOpen(self.path)
        guard self.fileDescriptor != -1 else {
            throw OpenFileError.getError() // A static func that generates an error based on the errno
        }
    }

    public func close() throws {
        guard cClose(self.fileDescriptor) != -1 else {
            throw CloseFileError.getError() // A static func that generates an error based on the errno
        }
    }
}

These are simplifications of the actual classes which only include the relevant parts.

The code compiles successfully, but this will run and then fail during the deinit closure:

let file = FilePath("/path/to/some/file")
let open = Open(file)
try open.open() // no errors here
// crashes at the program end with the message 'close() has not been implemented for the FilePath type'

I can't put a deinit in the extension. So how do I get my Open class to always run the overridden code from my extensions?

I sort-of understand why it's running the code directly in the class during the deinit, but I don't fully get why calling open directly works or how I can get it working the way I'm intending.

When the compiler is generating the code in deinit it can't assume that the instance that is being deinitialised has the extension methods. Thus it only has the choice of using the implementation in the main class definition.

By contrast, in the code that opens the file, it has already inferred the type as being Open<FilePath> and can thus use the extension methods.

how I can get it working the way I'm intending

The cleanest way I can think of is to have your base close() call a closure to actually close the file and have the open() method set the closure. e.g.

public class Open<PathType: Path>: Openable {
    let path: PathType
    var fileDescriptor: Int32 = -1
    var closeMethod: (() throws -> ())?

    public init(_ path: PathType) throws {
        self.path = path
    }

    public func open() throws {
        fatalError("open() has not been implemented for the \(PathType.self) type")
    }

    public func close() throws {
        guard let closeMethod = closeMethod else { throw CloseFileError.fileWasNotOpen }
        try closeMethod()
    }

    deinit {
        try? close() 
    }
}

public extension Open where PathType == FilePath {
    public func open() throws {
        self.fileDescriptor = cOpen(self.path)
        guard self.fileDescriptor != -1 else {
            throw OpenFileError.getError() // A static func that generates an error based on the errno
        }
        closeMethod = { 
           [unowned self] in 
           guard cClose(self.fileDescriptor) != -1 else {
                throw CloseFileError.getError() // A static func that generates an error based on the errno
        }           
    }
}

Of course, given that all instances of Open use a file descriptor, it is unlikely that your close() method is going to do anything other that use the Posix close() so you can simply implement close() in the generic base class.

Another point: I would avoid using deinit to close the file since you cannot guarantee when or if it will get called. Probably better to explicitly close the file when you are done with it. defer is useful for such things/

1 Like

Thank you for your helpful answer! You've definitely helped me figure out how to get my code functioning.

I decided on using the close method because I wanted it to be dynamically useable for different types of paths. ie: opening a directory with opendir and then closing it with closedir.

I did not realize that deinit is not guaranteed to be called. While I do agree that it is better to explicitly close file descriptors, I assumed all objects are deinitialized at some point and so just to ensure they are closed that I would use deinit to do this.

Would you be able to provide any links to documentation or discussions about deinit not being reliably/regularly called?

Yeah, I think "cannot guarantee when or if it will get called" is an overstatement. It's certainly possible that somewhere else in the program there might be an outstanding reference to the object, in which case it won't be deallocated. And it's also possible that at some point you hand it to Objective-C and it gets "autoreleased" (put into a stack-based pool for later removal of a strong reference). But Swift will always deallocate your objects deterministically when the last strong reference goes away, by the end of the scope (the close brace) or before the next function call.*

* The exception is if the compiler can prove the deinit does not have side effects. In that case it may move that last "release" around within the function, though never in a way that would destroy the object while it's still being used on this thread.

2 Likes