How to Deal with Buffered Strings from C in Swift

Create a String buffer in Swift for C to consume and free later

Two possible options:

let swiftString = "Hello world"
swiftString.withCString { cStringPtr in
// use `cStringPtr` ...

}

calls the closure with a pointer a C string representation of the Swift string (UTF-8 encoded). That pointer is only valid during the execution of the closure.

For a longer lifetime, you can do

let swiftString = "Hello world"
let cStringPtr = strdup(swiftString)
// use `cStringPtr` ...

which allocates a new C String from the given Swift string, and release that memory later with

free(cStringPtr)

Swift String from C String has correct length but incorrect contents

One issue you should observe is this within the C++ function:

Given that this is your scene definition:

typedef struct {
const char* name;
arrayID roots;
} scene;

Inside the getScene C++ function, you're doing this:

auto ss = globalObjects.scenes.at(s);

Then you do this later:

  scene sr;
sr.name = ss.name.c_str();
//...
return sr;

The issue is that since ss is a local variable, the ss.name.c_str() pointer assigned to sr.name will not be valid when the function returns. Accessing this pointer leads to undefined behavior.

One fix for the problem is to copy the contents of the string to a buffer that is sent by the client program, or copy the contents to a buffer you know is available by the client and won't be invalidated. Since I do not know Swift, you will have to negotiate how to accomplish this.


Another solution would be to make sure you are accessing the same std::string, not a copy of the std::string:

auto& ss = globalObjects.scenes.at(s);

Now a reference to the scene is returned. But then you have the issue of making sure that the string within the scene is not mutated at all when you actually refer to the returned c_str() value later on in your application. A C++ programmer would not hold onto the return value of c_str() for an extended time, since this can lead to hard-to-diagnose bugs.


The bottom line is that always be suspicious of returning a character pointer as a means of returning string data. The usual way that an API returns string data is to have the client supply a buffer and the API fills the buffer with the string data, or less likely, the API allocates memory and returns a pointer to the allocated memory (which leads to complications as the API has to free the memory by some means to avoid memory leaks).

Swift - C++ string via C API

No, there isn't another way.

A std::string, by design, owns its buffer. If you don't want the data to die when the string does, you'll have to copy it into a new buffer.

In this contrived example there's really no need for the std::string in the first place, but I'm sure you know that and have a more solid use case.

Swift String to bytes in a buffer

This is how you can set up the buffers and pass them to the C function:

let queryString = "Query ..."

let queryBuffer = Array(queryString.utf8)
var replyBuffer = Array(repeating: UInt8(0), count: 180)

let result = query(queryBuffer, numericCast(queryBuffer.count), &replyBuffer, numericCast(replyBuffer.count))

queryBuffer is an array with the UTF-8 representation of the string,
and replyBuffer an array containing a specified amount of zero bytes.

The arrays are passed to the function with queryBuffer and &replyBuffer, respectively, which passes pointers to the element storage
to the C function.

If the result buffer is filled with a null-terminated C string on return
then you can create a Swift string with

let resultString = String(cString: replyBuffer)

More Buffer for FileHandler Output?

There are two problems:

  • The process may terminate before all data has been read from the pipe.
  • Your function blocks the main thread, so that the UI eventually freezes.

Similarly as in How can I tell when a FileHandle has nothing left to be read? you should wait asynchronously for the process to terminate, and also wait for “end-of-file” on the pipe:

func asyncShellExec(path: String, args: [String] = []) {
let process = Process()
process.launchPath = "/bin/bash"
process.arguments = [path] + args
let outputPipe = Pipe()
let filelHandler = outputPipe.fileHandleForReading
process.standardOutput = outputPipe
process.launch()

let group = DispatchGroup()
group.enter()
filelHandler.readabilityHandler = { pipe in
let data = pipe.availableData
if data.isEmpty { // EOF
filelHandler.readabilityHandler = nil
group.leave()
return
}

if let line = String(data: data, encoding: String.Encoding.utf8) {
DispatchQueue.main.sync {
self.output_window.string += line
self.output_window.scrollToEndOfDocument(nil)
}
} else {
print("Error decoding data: \(data.base64EncodedString())")
}
}

process.terminationHandler = { process in
group.wait()
DispatchQueue.main.sync {
// Update UI that process has finished.
}
}
}


Related Topics



Leave a reply



Submit