2

I would like to know how to read a binary file into memory (writing it to memory like an "Array Buffer" from JavaScript), and write to different parts of memory 8-bit, 16-bit, 32-bit etc. values, even 5 bit or 10 bit values.

extension Binary {
  static func readFileToMemory(_ file) -> ArrayBuffer {        
    let data = NSData(contentsOfFile: "/path/to/file/7CHands.dat")!
    var dataRange = NSRange(location: 0, length: ?)
    var ? = [Int32](count: ?, repeatedValue: ?)
    data.getBytes(&?, range: dataRange)
  }

  static func writeToMemory(_ buffer, location, value) {
    buffer[location] = value
  }

  static func readFromMemory(_ buffer, location) {
    return buffer[location]
  }
}

I have looked at a bunch of places but haven't found a standard reference.

I would like for this to be as low-level as possible. So perhaps using UnsafeMutablePointer, UnsafePointer, or UnsafeMutableRawPointer.

Saw this as well:

let data = NSMutableData()
var goesIn: Int32 = 42
data.appendBytes(&goesIn, length: sizeof(Int32))
println(data) // <2a000000]

var comesOut: Int32 = 0
data.getBytes(&comesOut, range: NSMakeRange(0, sizeof(Int32)))
println(comesOut) // 42

I would basically like to allocate a chunk of memory and be able to read and write from it. Not sure how to do that. Perhaps using C is the best way, not sure.

Just saw this too:

let rawData = UnsafeMutablePointer<UInt8>.allocate(capacity: width * height * 4)
Lance
  • 75,200
  • 93
  • 289
  • 503

1 Answers1

5

If you're looking for low level code you'll need to use UnsafeMutableRawPointer. This is a pointer to a untyped data. Memory is accessed in bytes, so 8 chunks of at least 8 bits. I'll cover multiples of 8 bits first.

Reading a File

To read a file this way, you need to manage file handles and pointers yourself. Try the the following code:

// Open the file in read mode
let file = fopen("/Users/joannisorlandos/Desktop/ownership", "r")

// Files need to be closed manually
defer { fclose(file) }

// Find the end
fseek(file, 0, SEEK_END)
// Count the bytes from the start to the end
let fileByteSize = ftell(file)
// Return to the start
fseek(file, 0, SEEK_SET)

// Buffer of 1 byte entities
let pointer = UnsafeMutableRawPointer.allocate(byteCount: fileByteSize, alignment: 1)

// Buffer needs to be cleaned up manually
defer { pointer.deallocate() }

// Size is 1 byte
let readBytes = fread(pointer, 1, fileByteSize, file)
let errorOccurred = readBytes != fileByteSize

First you need to open the file. This can be done using Swift strings since the compiler makes them into a CString itself.

Because cleanup is all for us on this low level, a defer is put in place to close the file at the end.

Next, the file is set to seek the end of the file. Then the distance between the start of the file and the end is calculated. This is used later, so the value is kept.

Then the program is set to return to the start of the file, so the application starts reading from the start.

To store the file, a pointer is allocated with the amount of bytes that the file has in the file system. Note: This can change inbetween the steps if you're extremely unlucky or the file is accessed quite often. But I think for you, this is unlikely.

The amount of bytes is set, and aligned to one byte. (You can learn more about memory alignment on Wikipedia.

Then another defer is added to make sure no memory leaks at the end of this code. The pointer needs to be deallocated manually.

The file's bytes are read and stored in the pointer. Do note that this entire process reads the file in a blocking manner. It can be more preferred to read files asynchronously, if you plan on doing that I'll recommend looking into a library like SwiftNIO instead.

errorOccurred can be used to throw an error or handle issues in another manner.

From here, your buffer is ready for manipulation. You can print the file if it's text using the following code:

print(String(cString: pointer.bindMemory(to: Int8.self, capacity: fileByteSize)))

From here, it's time to learn how to read manipulate the memory.

Manipulating Memory

The below demonstrates reading byte 20..<24 as an Int32.

let int32 = pointer.load(fromByteOffset: 20, as: Int32.self)

I'll leave the other integers up to you. Next, you can alos put data at a position in memory.

pointer.storeBytes(of: 40, toByteOffset: 30, as: Int64.self)

This will replace byte 30..<38 with the number 40. Note that big endian systems, although uncommon, will store information in a different order from normal little endian systems. More about that here.

Modifying Bits

As you notes, you're also interested in modifying five or ten bits at a time. To do so, you'll need to mix the previous information with the new information.

var data32bits = pointer.load(fromByteOffset: 20, as: Int32.self)
var newData = 0b11111000

In this case, you'll be interested in the first 5 bits and want to write them over bit 2 through 7. To do so, first you'll need to shift the bits to a position that matches the new position.

newData = newData >> 2

This shifts the bits 2 places to the right. The two left bits that are now empty are therefore 0. The 2 bits on the right that got shoved off are not existing anymore. Next, you'll want to get the old data from the buffer and overwrite the new bits. To do so, first move the new byte into a 32-bits buffer.

var newBits = numericCast(newData) as Int32

The 32 bits will be aligned all the way to the right. If you want to replace the second of the four bytes, run the following:

newBits = newBits << 16

This moves the fourth pair 16 bit places left, or 2 bytes. So it's now on position 1 starting from 0.

Then, the two bytes need to be added on top of each other. One common method is the following:

let oldBits = data32bits & 0b11111111_11000001_11111111_11111111
let result = oldBits | newBits

What happens here is that we remove the 5 bits with new data from the old dataset. We do so by doing a bitwise and on the old 32 bits and a bitmap.

The bitmap has all 1's except for the new locations which are being replaced. Because those are empty in the bitmap, the and operator will exclude those bits since one of the two (old data vs. bitmap) is empty.

AND operators will only be 1 if both sides of the operator are 1.

Finally, the oldBits and the newBits are merged with an OR operator. This will take each bit on both sides and set the result to 1 if the bits at both positions are 1. This will merge successfully since both buffers contain 1 bits that the other number doesn't set.

JoannisO
  • 875
  • 7
  • 13