I’ve been busy in my increasingly rare free time adding some very useful tools to my Rubicon library for Swift. Most all of them came out of direct needs for my XML parsing library but I decided to include them in Rubicon so that they can be used in other projects.
1) RingByteBuffer
This is actually just a Swift wrapper around my C library RingBuffer which is a dynamic byte ring buffer (queue) that can dynamically grow to accept as many bytes as you have memory to hold. It mimics a queue however in that you append or prepend bytes onto it and remove bytes off of it like a FIFO buffer.
2) ManagedByteBuffer and MutableManagedByteBuffer
Dealing with Swift’s Unsafe*Pointer and UnsafeMutable*Pointer types can be a quite a pain – especially when you have to switch from raw to typed and so forth. But when you’re calling C functions you have to deal with them. So I created these two protocols (and the two concrete classes that implement them) to make that easier.
DataByteBuffer provides easier access to the contents of a Data object as a series of concurrent bytes.
EasyByteBuffer wraps an UnsafeMutablePointer<UInt8> object and will even deallocate it automatically when the EasyByteBuffer is no longer referenced.
So, what does it really get you? Well, for example, some C functions you might run across that deal with null-terminated strings take Unsafe(Mutable)Pointer<UInt8>, and some take Unsafe(Mutable)Pointer<Int8> (or <CChar>), and still others are expecting Unsafe(Mutable)RawPointer. It all depends on how they’re defined in the header files and how Swift chooses to interpret them. So if you are calling two C functions and Swift thinks one wants UnsafePointer<UInt8> and the other wants UnsafePointer<CChar> then you have to add a few extra lines to convert it from one to the other or else the compiler will throw an error.
ManagedByteBuffer and MutableManagedByteBuffer provide methods to help ease all of this madness.
@discardableResult func withBufferAs<T, V>(type: T.Type, _ body: (UnsafeBufferPointer<T>, inout Int) throws -> V) rethrows -> V @discardableResult func withBufferAs<T, V>(type: T.Type, _ body: (UnsafePointer<T>, inout Int) throws -> V) rethrows -> V
@discardableResult func withBufferAs<T, V>(type: T.Type, _ body: (UnsafeMutablePointer<T>, Int, inout Int) throws -> V) rethrows -> V @discardableResult func withBufferAs<T, V>(type: T.Type, _ body: (UnsafeMutableBufferPointer<T>, inout Int) throws -> V) rethrows -> V
These methods allow you to change the type that the underlying bytes are viewed as. So in our null-terminated C string example, you could solve it like so.
import Foundation import CoreFoundation import Rubicon let managedBuffer: MutableManagedByteBuffer = EasyByteBuffer(length: 1024) // Call a C function that is expecting the data as CChar (Int8) managedBuffer.withBufferAs(type: CChar.self) { (cStr: UnsafeMutablePointer<CChar>, maxLength: Int, val: inout Int) -> Void in // Call a C function that creates a string. strcpy(cStr, "Now is the time for all good men to come to the aid of their country.") // Have the managed buffer keep track of the string length for you. val = strlen(cStr) } // Now call a function that is expecting the data as UInt8! let string: String = managedBuffer.withBufferAs(type: UInt8.self) { (bytes: UnsafePointer<UInt8>, val: inout Int) -> String in String(cString: bytes) }
But probably the nicest thing is that the data types don’t even have to be the same size! For example, suppose you needed to work the data as bytes but then you needed to work with the data as 32-bit integers? No problem!
import Foundation import CoreFoundation import Rubicon let managedBuffer: MutableManagedByteBuffer = EasyByteBuffer(length: 1024) managedBuffer.withBufferAs(type: UInt8.self) { (bytes: UnsafeMutablePointer<T>, maxLength: Int, count: inout Int) -> Void in // "maxLength" will be equal to 1024 // "count" is measured in bytes. count = 16 for i in (0 ..< count) { bytes[i] = (i + 1) print("Byte Value \(i)> \(bytes[i])") } } print("") managedBuffer.withBufferAs(type: UInt32.self) { (words: UnsafeMutablePointer<T>, maxLength: Int, count: inout Int) -> Void in // Now, because we're working with 32-bit words (4 bytes in size): // "maxLength" is equal to 256 - (1024 / 4) // "count" is now equal to 4 - (16 / 4) for i in (0 ..< count) { print("Word Value \(i)> \(words[i])") } }
The output on a machine with little-endian byte order will be:
Byte Value 0> 1 Byte Value 1> 2 Byte Value 2> 3 Byte Value 3> 4 Byte Value 4> 5 Byte Value 5> 6 Byte Value 6> 7 Byte Value 7> 8 Byte Value 8> 9 Byte Value 9> 10 Byte Value 10> 11 Byte Value 11> 12 Byte Value 12> 13 Byte Value 13> 14 Byte Value 14> 15 Byte Value 15> 16 Word Value 0> 67305985 Word Value 1> 134678021 Word Value 2> 202050057 Word Value 3> 269422093
The output on a machine with big-endian byte order will be:
Byte Value 0> 1 Byte Value 1> 2 Byte Value 2> 3 Byte Value 3> 4 Byte Value 4> 5 Byte Value 5> 6 Byte Value 6> 7 Byte Value 7> 8 Byte Value 8> 9 Byte Value 9> 10 Byte Value 10> 11 Byte Value 11> 12 Byte Value 12> 13 Byte Value 13> 14 Byte Value 14> 15 Byte Value 15> 16 Word Value 0> 16909060 Word Value 1> 84281096 Word Value 2> 151653132 Word Value 3> 219025168
More to come…