How to call the C function, getline, from Swift?

This post isn't a question. Instead I am posting an example in the hope that doing so will help others. My need was to read a large text file (gigabytes in size) a line at a time. I created a bridging header to call the C getline function. Calling get line is interesting because it relies on pointers. Here is how it works.

Code Block
// Author: David Cox
// Example to illustrate how to call the C function
// getline from Swift
//
// This is the definition of getline as viewed from
// Swift:
//
// public func getline(_ linep:
// UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>!,
// _ linecapp: UnsafeMutablePointer<Int>!,
// _ __stream: UnsafeMutablePointer<FILE>!) -> Int
   
//
// readFile will call getline to read a file
// line-by-line
func readFile(fileName: String) {
// In this example, I am creating a buffer that
// getline will fill with characters.
// Technically, I don't have to do this.
// getline will accept a NULL pointer and will
// allocate memory for the incoming line.
// However, that approach requires that the
// memory be freed by the user program.
// I find that creating the buffer in advance
// is simpler to understand.
// So, here is the first step. Create a pointer
// and point it to an area of memory.
// Think of this memory as a C array.
// In this example, the area of memory will
// have a capacity of 200 Int8 elements
// and the pointer will be called buffer:
let buffer =
UnsafeMutablePointer<Int8>.allocate(capacity: 200)
// Initialize the memory. In this case I am
// initializing the memory to 200 zeros:
buffer.initialize(repeating: 0, count: 200)
// Next, create another UnsafeMutablePointer
// and point it to the buffer.
// Because buffer is also an
// UnsafeMutablePointer the type of this
// pointer will be UnsafeMutablePointer
// This pointer will point to an area of memory
// containing 1 element.
let bufferPtr =
UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>.
allocate(capacity: 1)
// Initialize this area of memory to hold buffer
// (which is an UnsafeMutablePointer)
bufferPtr.initialize(to: buffer)
// Create another pointer to an area of
// memory that will hold the size of the buffer
// The size is an stored as 1 Int. Hence, the
// capacity is 1.
let bufferSize =
UnsafeMutablePointer<Int>.allocate(capacity: 1)
bufferSize.initialize(to: 200)
// Now we are ready to open the file
let fp = fopen(fileName, "r")
// loop to read the file
while (true) {
let r = getline(bufferPtr, bufferSize, fp)
if r == -1 {
break
}
// convert the buffer contents to a string
let line = String(cString: bufferPtr.pointee!)
// do something with the line
print(line)
}
// deinitialize the pointers
bufferPtr.deinitialize(count: 1)
buffer.deinitialize(count: 200)
// deallocate the pointers
bufferPtr.deallocate()
buffer.deallocate()
}

Answered by Claude31 in 638496022
Just for information, if you ever wanted to call Swift from C, you may be interested by this discussion about Formalizing @cdecl …
https://forums.swift.org/t/formalizing-cdecl/40677
An interesting code, but there are some points you should worry about.
  • You have no need to allocate regions for bufferPtr or bufferSize, if you create some vars for them, Swift will make appropriate pointers with using &.

  • getline may realloc the buffer passed indirectly through the first argument. The realloc-ed buffer may have some different address than the original. As your buffer keeps the original address, it may not be valid after calling getline.

I would write it like this:
Code Block
func readFile(fileName: String) {
let kInitialBufferSize = 200
var buffer: UnsafeMutablePointer<Int8>? = malloc(kInitialBufferSize)
.initializeMemory(as: Int8.self, repeating: 0, count: kInitialBufferSize)
var size: Int = kInitialBufferSize
let fp = fopen(fileName, "r")
while (true) {
let r = getline(&buffer, &size, fp)
if r == -1 {
break
}
let line = String(cString: buffer!)
print(line)
}
buffer?.deinitialize(count: size) //`size` and `buffer` may be re-written at this point
free(buffer)
}


I use malloc and free because man page of getline demands malloc buffer and it may realloc the buffer. Under the current implementation of Swift runtime, it may not cause critical issues, but we should better handle pointers carefully.


By the way, have you already checked Swift System?
It is an Open Source project which tries to provide Swift-friendly APIs for C-based system calls.

I do not know if getline-like functions are (or will be) included in the project,
but I think you can contribute to it.


One more.

I created a bridging header to call the C getline function.

You can use import Darwin (on Apple's platforms) to use getline. Many Apple's frameworks (for example, Foundation) indirectly imports Darwin.
Accepted Answer
Just for information, if you ever wanted to call Swift from C, you may be interested by this discussion about Formalizing @cdecl …
https://forums.swift.org/t/formalizing-cdecl/40677
Thanks for this additional information. You raised points that I wasn’t aware of. I’ll incorporate them into another example.
I was able to simplify the call to getline a bit with the following code. What makes the call a bit tricky is the first argument which is a pointer to a pointer. I find that I can use an ampersand on standard types such as Int. For example, if I declare linecapp to be an integer, then passing &linecapp causing linecapp to be encapsulated within a UnsafeMutablePoint. Similarly, if I declare line to be an array, then passing &line encapsulates the array in an UnsafeMutablePointer. Notice how I use &line when initializing linep, the pointer to line. And notice how I pass &linecapp as the second parameter to linecapp. At the moment I don't see any other simplifications that I can perform on the code.

// declare an array to hold an incoming line
var line = Array<Int8>(repeating: 0, count: 200)

// declare a pointer to the line
let linep = UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>.allocate(capacity: 1)
linep.initialize(to: &line)

// specify the size of the line
var linecapp = 200

// open the file
let fp = fopen(fileName, "r")

// loop to read the file
while (true) {
let r = getline(linep, &linecapp, fp)
if r == -1 {
break
}

// do something with the line
// convert the line to a string
let str = String(cString: line)

// print the string
print(str)
}

linep.deinitialize(count: 1)
linep.deallocate


I was able to simplify the call to getline a bit with the following code. What makes the call a bit tricky is the first argument which is a pointer to a pointer. I find that I can use an ampersand on standard types such as Int. For example, if I declare linecapp to be an integer, then passing &linecapp causing linecapp to be encapsulated within a UnsafeMutablePoint. Similarly, if I declare line to be an array, then passing &line encapsulates the array in an UnsafeMutablePointer. Notice how I use &line when initializing linep, the pointer to line. And notice how I pass &linecapp as the second parameter to linecapp. At the moment I don't see any other simplifications that I can perform on the code.

// declare an array to hold an incoming line
var line = Array<Int8>(repeating: 0, count: 200)

// declare a pointer to the line
let linep = UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>.allocate(capacity: 1)
linep.initialize(to: &line)

// specify the size of the line
var linecapp = 200

// open the file
let fp = fopen(fileName, "r")

// loop to read the file
while (true) {
let r = getline(linep, &linecapp, fp)
if r == -1 {
break
}

// do something with the line
// convert the line to a string
let str = String(cString: line)

// print the string
print(str)
}

linep.deinitialize(count: 1)
linep.deallocate

 if I declare line to be an array, then passing &line encapsulates the array in an UnsafeMutablePointer.

Unfortunately, your usage of converting an Array to an UnsafeMutablePointer is wrong.

When Swift generates a pointer by passing &-prefixed Array, a temporal region may be allocated, and the content of the Array is copied to the region and then the address of the region is passed to the function call. After the function call, the region may be release and the pointer will not be valid any more.

In your case, the address passed to linep.initialize(to: &line) may not be valid after the line. Which means getline might access to the already released address.

What is bad is that this sort of misusage does seemingly work. Swift may optimize the copying Array content to a temporary region in some context, or the region may not be reused when accessed later. Shortly, just by luck.

Many developers are suffering with this sort of misusage, saying once working code stopped working abruptly.

Avoid such misusage even if it seemingly works in a limited context.
Thanks for these additional pointers! Greatly appreciated.
How to call the C function, getline, from Swift?
 
 
Q