Search code examples
swiftclosuresinteropfunction-pointers

Bad Access error when calling Swift closure from C api in Cocoa


I'm trying to implement a "file copy with progress" in Swift on macOS. After lots of searching I just found rustle's implement in Objective-C. It works pretty fine. But I would like it "swifty". I tried it with some simplified code:

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    var copyfileCallback: copyfile_callback_t = {(what, stage, state, sourcePath, destPath, context) -> Int32 in
        return COPYFILE_CONTINUE
    }

    @IBOutlet weak var window: NSWindow!

    func applicationDidFinishLaunching(_ aNotification: Notification) {

        let src = NSURL(fileURLWithPath: "Source_File_Path").fileSystemRepresentation
        let dst = NSURL(fileURLWithPath: "Destination_File_Path").fileSystemRepresentation
        let flag: copyfile_flags_t = UInt32(COPYFILE_ALL)

        let state = copyfile_state_alloc()

        // If I implement this, the copyfile() method will complain "EXC_BAD_ACCESS(code=2..." error
        copyfile_state_set(state, UInt32(COPYFILE_STATE_STATUS_CB), &copyfileCallback)

        copyfile(src, dst, state, flag)
    }
}

The basic function copyfile() works fine. But if I implement the callback function by providing a pointer of copyfileCallback closure to copyfile_state_set(), then copyfile() just complains "Bad_Access...". I guess maybe the closure was released before the C api tries to access it. But I have no idea how to solve this problem... Any clue will be so appreciated.


Solution

  • The error is here:

    copyfile_state_set(state, UInt32(COPYFILE_STATE_STATUS_CB), &copyfileCallback)
    

    because is passes the address of the copyfileCallback variable to the function, not the function pointer itself. In C you can pass an arbitrary function as a void * argument. In Swift you have to cast the function to a pointer explicitly:

    let state = copyfile_state_alloc()
    copyfile_state_set(state, UInt32(COPYFILE_STATE_STATUS_CB),
                       unsafeBitCast(copyfileCallback, to: UnsafeRawPointer.self))
    

    And don't forget to release the memory eventually, after the copy operation:

    copyfile_state_free(state)
    

    Remark: In Swift it is recommended to use the (value overlay type) URL instead of NSURL:

    let srcURL = URL(fileURLWithPath: "Source_File_Path")
    let destURL = URL(fileURLWithPath: "Destination_File_Path")
    
    let result = srcURL.withUnsafeFileSystemRepresentation { srcFile in
        destURL.withUnsafeFileSystemRepresentation { destFile in
            copyfile(srcFile, destFile, state, flag)
        }
    }