Application: a database written from scratch in swift
Progress: Reading/writing individual bytes works (thus far) but now I have to make sure I get/return them in the correct order.
Experience dealing with endinaness: very little.
I add features and improve correctness each time I refactor my code. This time around I want to make sure it's cross-platform so that means dealing with endianess.
At some point I have to write integers of various sizes to disk. These represents page numbers (UInt32
), page offsets (UInt16
), raw enum values, or actual user data (Int
). The code below works fine as far as I can tell. But it doesn't take into account endinaness.
The FixedWidthInteger
protocol has bigEndian
and littleEndian
properties. Should I just pick one and call ...Endian
on every value before applying these functions?
I picked ExpressibleByIntegerLiteral
because that covers floating point values as well. Floating points do not have ...Endian
, but I could always add that in an extension or handle floating points separately.
Also I only have a intel mac (and an iPhone, iPad). How do I unittest this? Endianess comes down to the hardware platform, right?
Pages are (or should become) fixed size bags of bytes. Content can be written over multiple pages.
This implies that e.g. the 8 bytes representing an Int could be written on multiple pages.
For String
s I just use their UTF8
representation. That should be fine, right.
func read<T>(type: T.Type = T.self) throws -> T
where T: ExpressibleByIntegerLiteral //because floating points are ExpressibleByIntegerLiteral
{
var value: T = 0
cursor += try Swift.withUnsafeMutableBytes(of: &value) { try self.copy(range: self.cursor ..< self.cursor + MemoryLayout<T>.size, to: $0) }
return value
}
@discardableResult
func write<T>(value: T) throws -> Int
where T: ExpressibleByIntegerLiteral
{
let endIndex = cursor + MemoryLayout<T>.size
try Swift.withUnsafeBytes(of: value) { try self.replace(range: self.cursor ..< endIndex, with: $0) }
cursor = endIndex
return MemoryLayout<T>.size
}
//because why waste disk storage if the majority of values would fit in UInt8 or UInt16
//okay, disk are big enough these days, but fitting more data onto a page should be faster
func read<T>(compressed: T.Type) throws -> (value: T, count: Int)
where T: FixedWidthInteger //compressing individual floating points makes little sense.
{
var count = 0
var value = T.zero
while cursor < endIndex
{
let byte = try self.value(at: cursor)
value |= T(byte & 127) << (7*count)
count += 1
cursor += 1
if byte < 128 { return (value, count) }
}
throw Error.outOfBounds()
}
@discardableResult
func write<T>(compressed value: T) throws -> Int
where T: FixedWidthInteger
{
let marker = cursor
var value = UInt(bitPattern: Int(value))
while value > 127 && cursor < endIndex
{
try set(value: UInt8( (value & 127) | 128 ), at: cursor)
value >>= 7
cursor += 1
}
try set(value: UInt8(value & 127), at: cursor)
cursor += 1
return cursor - marker
}
Edit: Forgot to add: generic advice, guidelines, suggestions and hints are fine and probably preferred. I am not expecting anybody to write this code for me. I just lack experience/knowledge in this particular narrow-focused topic. I rather ask now than have this blow up in my face years down the road.