How to (kind of) emulate RAII using `defer`

Swift's defer statement has a nice capability to kind of emulate C++'s Resource Acquisition Is Initialization behavior, which we don't get due to ARC. This could be handy if you're doing something with UnsafeMutablePointer by ensuring that resources are properly cleaned up when exiting the scope:

import Foundation

class FileHandler {
    private var file: UnsafeMutablePointer<FILE>?

    init?(filePath: String) {
        file = fopen(filePath, "r")
        guard file != nil else {
            print("Error: Unable to open the file.")
            return nil
        }
    }

    deinit {
        if file != nil {
            fclose(file)
        }
    }

    func readAndProcessFile() {
        defer {
            if file != nil {
                fclose(file)
                file = nil
            }
        }

        // Read and process the file
    }
}

if let fileHandler = FileHandler(filePath: "path/to/your/file.txt") {
    fileHandler.readAndProcessFile()
}

In this example, we have a FileHandler class that manages a file. When the readAndProcessFile method is called, we use a defer block to ensure that the file is closed when the method exits, regardless of whether it exits normally or due to an error. This is similar to the RAII concept in C++, where a resource's cleanup is performed when the object goes out of scope.

Of course, this is not a 1:1 for RAII, but it shows a way defer can potentially be used to effect something similar.

I have three separate comments about this…

First, I’m not a big fan of the C++ RAII model because it ties resource management to memory management. I kinda like the way defer work without this:

let f = try open()
defer { close(f) }
… work with `f` …

Everything is nice and visible.

Second, a common idiom in Swift is to provide a withXxx(…) function that calls a closure with the value and cleans up on return. For example:

func withOpenFile<Result>(_ body: (_ f: File) throws -> Result) throws -> Result {
    let f = try open()
    defer { close(f) }
    return body(f)
}

let r = withOpenFile() { f in
    return … read f to generate r …
}

Finally, if you want to go down the RAII path, check out the recent discussions on move-only types. A good place to start is here but there’s been a lot of activity in this space recently.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

5 Likes

What would happen if you call readAndProcessFile twice in a row?

I don't quite understand this. You can open file in the initialiser, close it in deinit and not have those defer statements. I.e. the file will not be closed right after readAndProcessFile, but at some later point when "deinit" is called. Also you can have the file variable non optional and there would be no need to check for nil. That would match RAII behaviour.

I would go so far as to say the RAII pattern exists to emulate defer in languages that lack it. There is no need for RAII in Swift because Swift has defer.

2 Likes

Swift has a few current models for managing the lifetimes of objects. defer and classes with deinit implementations are two general mechanisms.

It gets trickier when you have dependent lifetimes, for example when you obtain a pointer to the internal structure of some object, you need to be certain that the pointer does not outlive the data it points to. The best model we have in Swift today is the withXxx pattern:

var x = SomeLargeDataObject()
x.withPointerToSomeInternalStructure { ptr in
   ... do something with ptr ...
}

This pattern (mostly) guarantees that the pointer ptr cannot outlive the data object x.

There's a caveat, though: Swift does not currently have any way to ensure that you don't do something silly with ptr. For example, many people make this mistake in Swift:

var x =  ... some data ...
let ptr = x.withUnsafePointer { $0 }
... do something with ptr ...

This compiles but is very dangerous: the pointer is intended to only be used within the closure. By "escaping" it, you've bypassed what little security this construct provides. (Important detail: Swift does not as a rule guarantee that any object will remain at the same location in memory. In the example just above, x won't move during the withUnsafePointer call, but as soon as that returns, the compiler is allowed to move x in memory. (To be clear, it's unlikely to deliberately "move" it; rather, if some operation would naturally leave x in a different place, the Swift compiler is not required to move the result back to where it started. This is different from C/C++ which both guarantee that under certain conditions an object will remain at the same location in memory for its entire lifetime. This sometimes requires C/C++ compilers to insert additional copies to move a result back to the guaranteed location.)

In conjunction with our work towards "noncopyable" types, we've been exploring ways to make these kinds of constructs both safer and easier to use. To make them safer, we're looking at ways to have the compiler verify that pointers used in withUnsafePointer closures do not escape. This might even allow us to drop the "Unsafe" from these names. To make them easier to use, we're looking at other ways to enforce the relative lifetimes of objects. This is all still pretty speculative, though. We have rough ideas that we'll be pitching on the forums in the coming months -- I look forward to the discussion!

2 Likes

one big thing defer cannot do: it cannot perform any async cleanup.

3 Likes

IMO RAII definitely has its merits:

  1. with blocks are nice, but:

    • You have to hand-roll each one. (There's no generalized protocol for capturing an object's with behaviour, equivalent to Python's __enter__/__exit__, C#'s IDisposable interface, Java's Closable interface, etc.)
    • Unlike with in Python, you can't pass multiple objects to open temporarily. You can build your with functions to take arrays of values, but then you still can't handle mixes of different kinds of objects (e.g. open a websocket and a file, to upload the file via the socket), unless you hand-roll all the permutations you want, or introduce your own Closable protocol.
      • You can use one withX function for each object, but then you end up with deep nesting
  2. defer is nice, clear and explicit, but you can forget to call it.

5 Likes