Darwin.write, Array & UnsafeRawPointer

Dear community --

As personal side project and to understand how it works, I'm currently working on a serial library written in Swift.

I've thoroughly studied the code written in both yeokm1/SwiftSerial and armadsen/ORSSerialPort (in Obj-C as well as the uncompleted Swift rewrite in branch 3.0)

After spending several days neck deep in documentation trying to understand the ins & outs of termios & co., things are getting clearer and it's actually pretty simple to understand and use once you get your head around it (even though Swift is not really helping with control characters, C-functions and tuples...).

Now that I can setup and open a port, my next goal (obviously) is to write to it.

Foundation provides us with a nice wrapper around the C-function write:

// C function
ssize_t write(int fd, const void *buf, size_t count);
// Swift
func write(_ __fd: Int32, _ __buf: UnsafeRawPointer!, _ __nbyte: Int) -> Int

Now I would like to be able to write only one byte with the write(byte:UInt8) function. This was my first attempt:

func write(byte: UInt8)  Int {
	let bytesWritten = Darwin.write(fd, byte, 1)
	return bytesWritten
}

Obviously this does not work as I'm not passing an UnsafeRawPointer to write.

The other libraries will usually take the Data/NSData path to achieve the result. For example in SwiftSerial:

func writeData(_ data: Data) throws -> Int {
    let size = data.count
    let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: size)
    defer {
        buffer.deallocate(capacity: size)
    }

    data.copyBytes(to: buffer, count: size)

    let bytesWritten = Darwin.write(fd, buffer, size)
    return bytesWritten
}

But I find this allocate/deallocate thing cumbersome and overly complex... After some experimentation, I managed to fix my write function by simply doing this:

func write(byte: UInt8)  Int {
	let buffer: [UInt8] = [byte]
	let bytesWritten = Darwin.write(fd, buffer, 1)
	return bytesWritten
}

And if I want to write an array of bytes, I can just do this:

func write(array: [UInt8])  Int {
	let size = array.count
	let bytesWritten = Darwin.write(fd, array, size)
	return bytesWritten
}

Now I have 4 questions:

  • Am I doing something wrong? Or is using [UInt8] perfectly valid?
  • Am I losing some optimization/speed compared to the whole UnsafeMutablePointer.allocate/deallocate thing?
  • Is Array just a clever way of hiding the complexity of UnsafeMutablePointer?
  • Would Data/NSData bring useful features I might need in the future?

Thanks a lot! :slight_smile:
-- Ladislas

-- Edit --

I changed my write(byte:) function to the following, which seems to be better.

func write(byte: UInt8)  Int {
	var buffer = byte
	let bytesWritten = Darwin.write(fd, &buffer, 1)
	return bytesWritten
}
1 Like

In order to smooth out interactions with C APIs, Swift lets you implicitly convert some kinds of parameters into pointers. When you need to pass an UnsafeRawPointer, array and string parameters are automatically converted to pointers to the beginning of their buffer, and other values are converted when you use inout syntax. So your write(array:) function is great as is, but you could skip the array allocation for write(byte:) by changing it to:

func write(byte: UInt8)  Int {
	var byte = byte
	let bytesWritten = Darwin.write(fd, &byte, 1)
	return bytesWritten
}

You can read more about pointer conversions in this article.

3 Likes

Thanks @nnnnnnnn! That's exactly what I ended up figuring as well :)

1 Like

Just reading that article, I believe there is an error in that documentation (emphasis mine):

The pointer you pass to the function is guaranteed to be valid only for the duration of the function call. Do not persist the pointer and access it after the function has returned.

I do not believe Swift provides that guarantee. I think what was intended was:

The pointer you pass to the function is only guaranteed to be valid for the duration of the function call. Do not persist the pointer and access it after the function has returned.

Great point!

??? Closure pointers have that guarantee inside the closure and nowhere else. the documentation is correct.

It's a difference in how that sentence is interpreted — depending on how you read it, that sentence can say:

  1. The pointer is guaranteed to be valid for the duration of the call, and makes no guarantees about anything else, or
  2. The pointer is guaranteed to be valid for the duration of the call, and guaranteed to not be valid afterwards.

The second one is a stronger guarantee than Swift makes, and moving the word "only" lessens the chance of confusion.

how can a pointer be guaranteed “not valid”? besides unmapping its page from process memory?

Data is the common-currency type for any untyped binary data. Using it was a good idea. You manually allocated, initialized, used and deallocated a UnsafeMutablePointer to use with Darwin.write, but this was unecessary. Instead, you can just directly use Data.withUnsafeBytes to obtain a pointer to the Data instances already existing buffer.

Try something like this:

import Foundation

extension Data {
    @discardableResult
    func write(to fileHandle: FileHandle) -> Int {
        let bytesWritten = self.withUnsafeBytes { bytePointer in
            return Darwin.write(fileHandle.fileDescriptor, bytePointer, self.count)
        }
        
        return bytesWritten
    }
}

var data = "Your Text Here".data(using: .utf8)!
let fileHandle = FileHandle(forWritingAtPath: "/Your/Path/Here.txt")!
data.write(to: fileHandle)

Well, if you’re in the Foundation world. Technically, UnsafeRawBufferPointer is the canonical structure for untyped binary data.