1

I would like to use some C code that uses a file descriptor. Background is that I would like to read some data from cgraph library.

public extension UnsafeMutablePointer where Pointee == Agraph_t {
   func saveTo(fileName: String)  {
      let f = fopen(cString(fileName), cString("w"))
      agwrite(self,f)
      fsync(fileno(f))
      fclose(f)
   }
}

I would like to have the file output, but without writing to a temp file. Hence, I would like to do something like this:

public extension UnsafeMutablePointer where Pointee == Agraph_t {
    var asString: String  {
        let pipe = Pipe()
        let fileDescriptor = UnsafeMutablePointer<Int32>.allocate(capacity: 1)
        fileDescriptor.pointee = pipe.fileHandleForWriting.fileDescriptor
        agwrite(self, fileDescriptor)
        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        if let output = String(data: data, encoding: .utf8) {
            return output  
        }  
        return ""
    }
}

But it doesn't work, resulting in a EXC_BAD_ACCESS within agwrite(,). What do I need to do instead? Many thanks in advance!

Wizard of Kneup
  • 1,863
  • 1
  • 18
  • 35

1 Answers1

3

File descriptors and file pointers are not the same thing. It's confusing, and made even more frustrating by the fact that FILE * is really hard to Google because of the symbol.

You need to fdopen the file descriptor (pipe.fileHandleForWriting.fileDescriptor), to receive a FILE * (UnsafeMutablePointer<FILE> in Swift). This is what you then pass to agwrite.

It's important to fclose the file pointer when you're done writing to it, otherwise .readDataToEndOfFile() will never terminate. I made a helper function to ensure the fclose can't be forgetten. It's possible that agwrite closes the file pointer itself, internally. If that's the case, you should delete this code and just give it the result of fdopen, plain and simple.

import Foundation

public typealias Agraph_t = Int // Dummy value

public struct AGWriteWrongEncoding: Error { }

func agwrite(_: UnsafeMutablePointer<Agraph_t>, _ filePointer: UnsafeMutablePointer<FILE>) {
    let message = "This is a stub."

    _ = message.withCString { cString in
        fputs(cString, stderr)
    }
}

@discardableResult
func use<R>(
    fileDescriptor: Int32,
    mode: UnsafePointer<Int8>!,
    closure: (UnsafeMutablePointer<FILE>) throws -> R
) rethrows -> R {
    // Should prob remove this `!`, but IDK what a sensible recovery mechanism would be.
    let filePointer = fdopen(fileDescriptor, mode)!
    defer { fclose(filePointer) }
    return try closure(filePointer)

}

public extension UnsafeMutablePointer where Pointee == Agraph_t {
    func asString() throws -> String {
        let pipe = Pipe()

        use(fileDescriptor: pipe.fileHandleForWriting.fileDescriptor, mode: "w") { filePointer in
            agwrite(self, filePointer)
        }

        let data = pipe.fileHandleForReading.readDataToEndOfFile()

        guard let output = String(data: data, encoding: .utf8) else {
            throw AGWriteWrongEncoding()
        }  
        return output  
    }
}

let ptr = UnsafeMutablePointer<Agraph_t>.allocate(capacity: 1) // Dummy value
print(try ptr.asString())

Several other things:

  1. Throwing an error is probably a better choice than returning "". Empty strings aren't a good error handling mechanism. Returning an optional would also work, but it's likely to always be force unwrapped, anyway.
  2. readDataToEndOfFile is a blocking call, which can lead to a bad use experience. It's probably best that this code be run on a background thread, or use a FileHandle.readabilityHandler to asynchronously consume the data as it comes in.
Alexander
  • 59,041
  • 12
  • 98
  • 151