import Foundation public final class FlatBufferBuilder { /// Vtables used in the buffer are stored in here, so they would be written later in EndTable private var _vtable: [UInt32] = [] /// Reference Vtables that were already written to the buffer private var _vtables: [UOffset] = [] /// Flatbuffer data will be written into private var _bb: ByteBuffer /// A check if the buffer is being written into by a different table private var isNested = false /// Dictonary that stores a map of all the strings that were written to the buffer private var stringOffsetMap: [String: Offset] = [:] /// A check to see if finish(::) was ever called to retreive data object private var finished = false /// A check to see if the buffer should serialize Default values private var serializeDefaults: Bool /// Current alignment for the buffer var _minAlignment: Int = 0 { didSet { _bb.alignment = _minAlignment } } /// Gives a read access to the buffer's size public var size: UOffset { return _bb.size } /// Data representation of the buffer public var data: Data { assert(finished, "Data shouldn't be called before finish()") return Data(bytes: _bb.memory.advanced(by: _bb.writerIndex), count: _bb.capacity - _bb.writerIndex) } /// Get's the fully sized buffer stored in memory public var fullSizedByteArray: [UInt8] { let ptr = UnsafeBufferPointer(start: _bb.memory.assumingMemoryBound(to: UInt8.self), count: _bb.capacity) return Array(ptr) } /// Returns the written size of the buffer public var sizedByteArray: [UInt8] { let cp = _bb.capacity - _bb.writerIndex let start = _bb.memory.advanced(by: _bb.writerIndex) .bindMemory(to: UInt8.self, capacity: cp) let ptr = UnsafeBufferPointer(start: start, count: cp) return Array(ptr) } /// Returns the buffer public var buffer: ByteBuffer { return _bb } /// Returns A sized Buffer from the readable bytes public var sizedBuffer: ByteBuffer { assert(finished, "Data shouldn't be called before finish()") return ByteBuffer(memory: _bb.memory.advanced(by: _bb.reader), count: _bb.reader) } // MARK: - Init /// initialize the buffer with a size /// - Parameters: /// - initialSize: Initial size for the buffer /// - force: Allows default to be serialized into the buffer public init(initialSize: Int32 = 1024, serializeDefaults force: Bool = false) { assert(initialSize > 0, "Size should be greater than zero!") guard isLitteEndian else { fatalError("Reading/Writing a buffer in big endian machine is not supported on swift") } serializeDefaults = force _bb = ByteBuffer(initialSize: Int(initialSize)) } /// Clears the buffer and the builder from it's data public func clear() { _minAlignment = 0 isNested = false _bb.clear() stringOffsetMap = [:] } /// Removes all the offsets from the VTable public func clearOffsets() { _vtable = [] } // MARK: - Create Tables /// Checks if the required fields were serialized into the buffer /// - Parameters: /// - table: offset for the table /// - fields: Array of all the important fields to be serialized public func require(table: Offset, fields: [Int32]) { for field in fields { let start = _bb.capacity - Int(table.o) let startTable = start - Int(_bb.read(def: Int32.self, position: start)) let isOkay = _bb.read(def: VOffset.self, position: startTable + Int(field)) != 0 assert(isOkay, "Flatbuffers requires the following field") } } /// Finished the buffer by adding the file id and then calling finish /// - Parameters: /// - offset: Offset of the table /// - fileId: Takes the fileId /// - prefix: if false it wont add the size of the buffer public func finish(offset: Offset, fileId: String, addPrefix prefix: Bool = false) { let size = MemoryLayout.size preAlign(len: size + (prefix ? size : 0) + FileIdLength, alignment: _minAlignment) assert(fileId.count == FileIdLength, "Flatbuffers requires file id to be 4") _bb.push(string: fileId, len: 4) finish(offset: offset, addPrefix: prefix) } /// Finished the buffer by adding the file id, offset, and prefix to it. /// - Parameters: /// - offset: Offset of the table /// - prefix: if false it wont add the size of the buffer public func finish(offset: Offset, addPrefix prefix: Bool = false) { notNested() let size = MemoryLayout.size preAlign(len: size + (prefix ? size : 0), alignment: _minAlignment) push(element: refer(to: offset.o)) if prefix { push(element: _bb.size) } clearOffsets() finished = true } /// starttable will let the builder know, that a new object is being serialized. /// /// The function will fatalerror if called while there is another object being serialized /// - Parameter numOfFields: Number of elements to be written to the buffer public func startTable(with numOfFields: Int) -> UOffset { notNested() isNested = true _vtable = [UInt32](repeating: 0, count: numOfFields) return _bb.size } /// Endtable will let the builder know that the object that's written to it is completed /// /// This would be called after all the elements are serialized, it will add the vtable into the buffer. /// it will fatalError in case the object is called without starttable, or the object has exceeded the limit of /// 2GB, /// - Parameter startOffset:Start point of the object written /// - returns: The root of the table public func endTable(at startOffset: UOffset) -> UOffset { assert(isNested, "Calling endtable without calling starttable") let sizeofVoffset = MemoryLayout.size let vTableOffset = push(element: SOffset(0)) let tableObjectSize = vTableOffset - startOffset assert(tableObjectSize < 0x10000, "Buffer can't grow beyond 2 Gigabytes") var writeIndex = 0 for (index,j) in _vtable.lazy.reversed().enumerated() { if j != 0 { writeIndex = _vtable.count - index break } } for i in stride(from: writeIndex - 1, to: -1, by: -1) { let off = _vtable[i] == 0 ? 0 : vTableOffset - _vtable[i] _bb.push(value: VOffset(off), len: sizeofVoffset) } _bb.push(value: VOffset(tableObjectSize), len: sizeofVoffset) _bb.push(value: (UInt16(writeIndex + 2) * UInt16(sizeofVoffset)), len: sizeofVoffset) clearOffsets() let vt_use = _bb.size var isAlreadyAdded: Int? let vt2 = _bb.memory.advanced(by: _bb.writerIndex) let len2 = vt2.load(fromByteOffset: 0, as: Int16.self) for table in _vtables { let position = _bb.capacity - Int(table) let vt1 = _bb.memory.advanced(by: position) let len1 = _bb.read(def: Int16.self, position: position) if (len2 != len1 || 0 != memcmp(vt1, vt2, Int(len2))) { continue } isAlreadyAdded = Int(table) break } if let offset = isAlreadyAdded { let vTableOff = Int(vTableOffset) let space = _bb.capacity - vTableOff _bb.write(value: Int32(offset - vTableOff), index: space, direct: true) _bb.resize(_bb.capacity - space) } else { _bb.write(value: Int32(vt_use) - Int32(vTableOffset), index: Int(vTableOffset)) _vtables.append(_bb.size) } isNested = false return vTableOffset } // MARK: - Builds Buffer /// asserts to see if the object is not nested fileprivate func notNested() { assert(!isNested, "Object serialization must not be nested") } /// Changes the minimuim alignment of the buffer /// - Parameter size: size of the current alignment fileprivate func minAlignment(size: Int) { if size > _minAlignment { _minAlignment = size } } /// Gets the padding for the current element /// - Parameters: /// - bufSize: Current size of the buffer + the offset of the object to be written /// - elementSize: Element size fileprivate func padding(bufSize: UInt32, elementSize: UInt32) -> UInt32 { ((~bufSize) &+ 1) & (elementSize - 1) } /// Prealigns the buffer before writting a new object into the buffer /// - Parameters: /// - len:Length of the object /// - alignment: Alignment type fileprivate func preAlign(len: Int, alignment: Int) { minAlignment(size: alignment) _bb.fill(padding: padding(bufSize: _bb.size + UOffset(len), elementSize: UOffset(alignment))) } /// Prealigns the buffer before writting a new object into the buffer /// - Parameters: /// - len: Length of the object /// - type: Type of the object to be written fileprivate func preAlign(len: Int, type: T.Type) { preAlign(len: len, alignment: MemoryLayout.size) } /// Refers to an object that's written in the buffer /// - Parameter off: the objects index value fileprivate func refer(to off: UOffset) -> UOffset { let size = MemoryLayout.size preAlign(len: size, alignment: size) return _bb.size - off + UInt32(size) } /// Tracks the elements written into the buffer /// - Parameters: /// - offset: The offset of the element witten /// - position: The position of the element fileprivate func track(offset: UOffset, at position: VOffset) { _vtable[Int(position)] = offset } // MARK: - Vectors /// Starts a vector of length and Element size public func startVector(_ len: Int, elementSize: Int) { notNested() isNested = true preAlign(len: len * elementSize, type: UOffset.self) preAlign(len: len * elementSize, alignment: elementSize) } /// Ends the vector of at length /// /// The current function will fatalError if startVector is called before serializing the vector /// - Parameter len: Length of the buffer public func endVector(len: Int) -> UOffset { assert(isNested, "Calling endVector without calling startVector") isNested = false return push(element: Int32(len)) } /// Creates a vector of type Scalar in the buffer /// - Parameter elements: elements to be written into the buffer /// - returns: Offset of the vector public func createVector(_ elements: [T]) -> Offset { return createVector(elements, size: elements.count) } /// Creates a vector of type Scalar in the buffer /// - Parameter elements: Elements to be written into the buffer /// - Parameter size: Count of elements /// - returns: Offset of the vector public func createVector(_ elements: [T], size: Int) -> Offset { let size = size startVector(size, elementSize: MemoryLayout.size) _bb.push(elements: elements) return Offset(offset: endVector(len: size)) } /// Creates a vector of type Enums in the buffer /// - Parameter elements: elements to be written into the buffer /// - returns: Offset of the vector public func createVector(_ elements: [T]) -> Offset { return createVector(elements, size: elements.count) } /// Creates a vector of type Enums in the buffer /// - Parameter elements: Elements to be written into the buffer /// - Parameter size: Count of elements /// - returns: Offset of the vector public func createVector(_ elements: [T], size: Int) -> Offset { let size = size startVector(size, elementSize: T.byteSize) for e in elements.lazy.reversed() { _bb.push(value: e.value, len: T.byteSize) } return Offset(offset: endVector(len: size)) } /// Creates a vector of type Offsets in the buffer /// - Parameter offsets:Array of offsets of type T /// - returns: Offset of the vector public func createVector(ofOffsets offsets: [Offset]) -> Offset { createVector(ofOffsets: offsets, len: offsets.count) } /// Creates a vector of type Offsets in the buffer /// - Parameter elements: Array of offsets of type T /// - Parameter size: Count of elements /// - returns: Offset of the vector public func createVector(ofOffsets offsets: [Offset], len: Int) -> Offset { startVector(len, elementSize: MemoryLayout>.size) for o in offsets.lazy.reversed() { push(element: o) } return Offset(offset: endVector(len: len)) } /// Creates a vector of Strings /// - Parameter str: a vector of strings that will be written into the buffer /// - returns: Offset of the vector public func createVector(ofStrings str: [String]) -> Offset { var offsets: [Offset] = [] for s in str { offsets.append(create(string: s)) } return createVector(ofOffsets: offsets) } /// Creates a vector of Flatbuffer structs. /// /// The function takes a Type to know what size it is, and alignment /// - Parameters: /// - structs: An array of UnsafeMutableRawPointer /// - type: Type of the struct being written /// - returns: Offset of the vector public func createVector(structs: [UnsafeMutableRawPointer], type: T.Type) -> Offset { startVector(structs.count * T.size, elementSize: T.alignment) for i in structs.lazy.reversed() { create(struct: i, type: T.self) } return Offset(offset: endVector(len: structs.count)) } // MARK: - Inserting Structs /// Writes a Flatbuffer struct into the buffer /// - Parameters: /// - s: Flatbuffer struct /// - type: Type of the element to be serialized /// - returns: Offset of the Object @discardableResult public func create(struct s: UnsafeMutableRawPointer, type: T.Type) -> Offset { let size = T.size preAlign(len: size, alignment: T.alignment) _bb.push(struct: s, size: size) return Offset(offset: _bb.size) } /// Adds the offset of a struct into the vTable /// /// The function fatalErrors if we pass an offset that is out of range /// - Parameter o: offset public func add(structOffset o: UOffset) { guard Int(o) < _vtable.count else { fatalError("Out of the table range") } _vtable[Int(o)] = _bb.size } // MARK: - Inserting Strings /// Insets a string into the buffer using UTF8 /// - Parameter str: String to be serialized /// - returns: The strings offset in the buffer public func create(string str: String) -> Offset { let len = str.count notNested() preAlign(len: len + 1, type: UOffset.self) _bb.fill(padding: 1) _bb.push(string: str, len: len) push(element: UOffset(len)) return Offset(offset: _bb.size) } /// Inserts a shared string to the buffer /// /// The function checks the stringOffsetmap if it's seen a similar string before /// - Parameter str: String to be serialized /// - returns: The strings offset in the buffer public func createShared(string str: String) -> Offset { if let offset = stringOffsetMap[str] { return offset } let offset = create(string: str) stringOffsetMap[str] = offset return offset } // MARK: - Inseting offsets /// Adds the offset of an object into the buffer /// - Parameters: /// - offset: Offset of another object to be written /// - position: The predefined position of the object public func add(offset: Offset, at position: VOffset) { if offset.isEmpty { track(offset: 0, at: position) return } add(element: refer(to: offset.o), def: 0, at: position) } /// Pushes a value of type offset into the buffer /// - Parameter o: Offset /// - returns: Position of the offset @discardableResult public func push(element o: Offset) -> UOffset { push(element: refer(to: o.o)) } // MARK: - Inserting Scalars to Buffer /// Adds a value into the buffer of type Scalar /// /// - Parameters: /// - element: Element to insert /// - def: Default value for that element /// - position: The predefined position of the element public func add(element: T, def: T, at position: VOffset) { if (element == def && !serializeDefaults) { track(offset: 0, at: position) return } let off = push(element: element) track(offset: off, at: position) } /// Adds Boolean values into the buffer /// - Parameters: /// - condition: Condition to insert /// - def: Default condition /// - position: The predefined position of the element public func add(condition: Bool, def: Bool, at position: VOffset) { if (condition == def && !serializeDefaults) { track(offset: 0, at: position) return } let off = push(element: Byte(condition ? 1 : 0)) track(offset: off, at: position) } /// Pushes the values into the buffer /// - Parameter element: Element to insert /// - returns: Postion of the Element @discardableResult public func push(element: T) -> UOffset { preAlign(len: MemoryLayout.size, alignment: MemoryLayout.size) _bb.push(value: element, len: MemoryLayout.size) return _bb.size } } extension FlatBufferBuilder: CustomDebugStringConvertible { public var debugDescription: String { """ buffer debug: \(_bb) builder debug: { finished: \(finished), serializeDefaults: \(serializeDefaults), isNested: \(isNested) } """ } }