Files
flatbuffers-bigfoot/swift/Sources/FlatBuffers/FlatBufferBuilder.swift
mustiikhalil bd1b2d0baf [Swift] Adds new API to reduce memory copying within swift (#8484)
* Adds new API to reduce memory copying within swift

Adds new storage container _InternalByteBuffer which
will be holding the data that will be created within the swift
lib, however reading data will be redirected to ByteBuffer, which
should be able to handle all types of data that swift provide without
the need to copy the data itself. This is due to holding a reference to
the data.

Replaces assumingMemoryBinding with bindMemory which is safer

Adds function that provides access to a UnsafeBufferPointer for
scalars and NativeStructs within swift

Updates docs

Suppress compilation warnings by replacing var with let

Using overflow operators within swift to improve performance

Adds tests for GRPC message creation from a retained _InternalByteBuffer
2025-03-18 07:48:39 +01:00

949 lines
30 KiB
Swift

/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// ``FlatBufferBuilder`` builds a `FlatBuffer` through manipulating its internal state.
///
/// This is done by creating a ``ByteBuffer`` that hosts the incoming data and
/// has a hardcoded growth limit of `2GiB` which is set by the Flatbuffers standards.
///
/// ```swift
/// var builder = FlatBufferBuilder()
/// ```
/// The builder should be always created as a variable, since it would be passed into the writers
///
@frozen
public struct FlatBufferBuilder {
/// Storage for the Vtables used in the buffer are stored in here, so they would be written later in EndTable
@usableFromInline internal var _vtableStorage = VTableStorage()
/// Flatbuffer data will be written into
@usableFromInline internal var _bb: _InternalByteBuffer
/// Reference Vtables that were already written to the buffer
private var _vtables: [UOffset] = []
/// 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 { _bb.size }
/// Current allocated capacity within the storage
@inline(__always)
public var capacity: Int { _bb.capacity }
#if !os(WASI)
/// Data representation of the buffer
///
/// Should only be used after ``finish(offset:addPrefix:)`` is called
public var data: Data {
assert(finished, "Data shouldn't be called before finish()")
return _bb.withUnsafeSlicedBytes { ptr in
var data = Data()
data.append(
ptr.baseAddress!.bindMemory(
to: UInt8.self,
capacity: _bb.capacity),
count: _bb.capacity)
return data
}
}
#endif
/// Returns the underlying bytes in the ``ByteBuffer``
///
/// Note: This should be used with caution.
public var fullSizedByteArray: [UInt8] {
_bb.withUnsafeBytes { ptr in
Array(ptr)
}
}
/// Returns the written bytes into the ``ByteBuffer``
///
/// Should only be used after ``finish(offset:addPrefix:)`` is called
public var sizedByteArray: [UInt8] {
assert(finished, "Data shouldn't be called before finish()")
return _bb.withUnsafeSlicedBytes { ptr in
Array(ptr)
}
}
/// Returns the original ``ByteBuffer``
///
/// Returns the current buffer that was just created
/// with the offsets, and data written to it.
public var buffer: ByteBuffer {
ByteBuffer(byteBuffer: _bb)
}
/// Returns a newly created sized ``ByteBuffer``
///
/// returns a new buffer that is sized to the data written
/// to the main buffer
public var sizedBuffer: ByteBuffer {
assert(finished, "Data shouldn't be called before finish()")
return _bb.withUnsafeSlicedBytes { ptr in
ByteBuffer(
copyingMemoryBound: ptr.baseAddress!,
capacity: ptr.count)
}
}
// MARK: - Init
/// Initialize the buffer with a size
/// - Parameters:
/// - initialSize: Initial size for the buffer
/// - force: Allows default to be serialized into the buffer
///
/// This initializes a new builder with an initialSize that would initialize
/// a new ``ByteBuffer``. ``FlatBufferBuilder`` by default doesnt serialize defaults
/// however the builder can be force by passing true for `serializeDefaults`
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 = _InternalByteBuffer(initialSize: Int(initialSize))
}
/// Clears the builder and the buffer from the written data.
mutating public func clear() {
_minAlignment = 0
isNested = false
stringOffsetMap.removeAll(keepingCapacity: true)
_vtables.removeAll(keepingCapacity: true)
_vtableStorage.clear()
_bb.clear()
}
// 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
///
/// *NOTE: Never call this function, this is only supposed to be called
/// by the generated code*
@inline(__always)
mutating public func require(table: Offset, fields: [Int32]) {
for index in stride(from: 0, to: fields.count, by: 1) {
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(fields[index])) != 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
///
/// ``finish(offset:fileId:addPrefix:)`` should be called at the end of creating
/// a table
/// ```swift
/// var root = SomeObject
/// .createObject(&builder,
/// name: nameOffset)
/// builder.finish(
/// offset: root,
/// fileId: "ax1a",
/// addPrefix: true)
/// ```
/// File id would append a file id name at the end of the written bytes before,
/// finishing the buffer.
///
/// Whereas, if `addPrefix` is true, the written bytes would
/// include the size of the current buffer.
mutating public func finish(
offset: Offset,
fileId: String,
addPrefix prefix: Bool = false)
{
let size = MemoryLayout<UOffset>.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
///
/// ``finish(offset:addPrefix:)`` should be called at the end of creating
/// a table
/// ```swift
/// var root = SomeObject
/// .createObject(&builder,
/// name: nameOffset)
/// builder.finish(
/// offset: root,
/// addPrefix: true)
/// ```
/// If `addPrefix` is true, the written bytes would
/// include the size of the current buffer.
mutating public func finish(
offset: Offset,
addPrefix prefix: Bool = false)
{
notNested()
let size = MemoryLayout<UOffset>.size
preAlign(len: size &+ (prefix ? size : 0), alignment: _minAlignment)
push(element: refer(to: offset.o))
if prefix { push(element: _bb.size) }
_vtableStorage.clear()
finished = true
}
/// ``startTable(with:)`` 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.
/// ```swift
/// let start = Monster
/// .startMonster(&fbb)
/// ```
/// - Parameter numOfFields: Number of elements to be written to the buffer
/// - Returns: Offset of the newly started table
@inline(__always)
mutating public func startTable(with numOfFields: Int) -> UOffset {
notNested()
isNested = true
_vtableStorage.start(count: numOfFields)
return _bb.size
}
/// ``endTable(at:)`` will let the ``FlatBufferBuilder`` 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 current vtable into the ``ByteBuffer``.
/// The functions will `fatalError` in case the object is called
/// without ``startTable(with:)``, or the object has exceeded the limit of 2GB.
///
/// - Parameter startOffset:Start point of the object written
/// - returns: The root of the table
@inline(__always)
mutating public func endTable(at startOffset: UOffset) -> UOffset {
assert(isNested, "Calling endtable without calling starttable")
let sizeofVoffset = MemoryLayout<VOffset>.size
let vTableOffset = push(element: SOffset(0))
let tableObjectSize = vTableOffset &- startOffset
assert(tableObjectSize < 0x10000, "Buffer can't grow beyond 2 Gigabytes")
let _max = Int(_vtableStorage.maxOffset) &+ sizeofVoffset
_bb.fill(padding: _max)
_bb.write(
value: VOffset(tableObjectSize),
index: _bb.writerIndex &+ sizeofVoffset,
direct: true)
_bb.write(value: VOffset(_max), index: _bb.writerIndex, direct: true)
var itr = 0
while itr < _vtableStorage.writtenIndex {
let loaded = _vtableStorage.load(at: itr)
itr = itr &+ _vtableStorage.size
guard loaded.offset != 0 else { continue }
let _index = (_bb.writerIndex &+ Int(loaded.position))
_bb.write(
value: VOffset(vTableOffset &- loaded.offset),
index: _index,
direct: true)
}
_vtableStorage.clear()
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 index in stride(from: 0, to: _vtables.count, by: 1) {
let position = _bb.capacity &- Int(_vtables[index])
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(_vtables[index])
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.pop(_bb.capacity &- space)
} else {
_bb.write(value: Int32(vt_use &- vTableOffset), index: Int(vTableOffset))
_vtables.append(_bb.size)
}
isNested = false
return vTableOffset
}
// MARK: - Builds Buffer
/// Asserts to see if the object is not nested
@inline(__always)
@usableFromInline
mutating internal func notNested() {
assert(!isNested, "Object serialization must not be nested")
}
/// Changes the minimuim alignment of the buffer
/// - Parameter size: size of the current alignment
@inline(__always)
@usableFromInline
mutating internal 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
@inline(__always)
@usableFromInline
mutating internal 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
@inline(__always)
@usableFromInline
mutating internal func preAlign(len: Int, alignment: Int) {
minAlignment(size: alignment)
_bb.fill(
padding: Int(
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
@inline(__always)
@usableFromInline
mutating internal func preAlign<T: Scalar>(len: Int, type: T.Type) {
preAlign(len: len, alignment: MemoryLayout<T>.size)
}
/// Refers to an object that's written in the buffer
/// - Parameter off: the objects index value
@inline(__always)
@usableFromInline
mutating internal func refer(to off: UOffset) -> UOffset {
let size = MemoryLayout<UOffset>.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
@inline(__always)
@usableFromInline
mutating internal func track(offset: UOffset, at position: VOffset) {
_vtableStorage.add(loc: (offset: offset, position: position))
}
// MARK: - Inserting Vectors
/// ``startVector(_:elementSize:)`` creates a new vector within buffer
///
/// The function checks if there is a current object being written, if
/// the check passes it creates a buffer alignment of `length * elementSize`
/// ```swift
/// builder.startVector(
/// int32Values.count, elementSize: 4)
/// ```
///
/// - Parameters:
/// - len: Length of vector to be created
/// - elementSize: Size of object type to be written
@inline(__always)
mutating public func startVector(_ len: Int, elementSize: Int) {
notNested()
isNested = true
preAlign(len: len &* elementSize, type: UOffset.self)
preAlign(len: len &* elementSize, alignment: elementSize)
}
/// ``endVector(len:)`` ends the currently created vector
///
/// Calling ``endVector(len:)`` requires the length, of the current
/// vector. The length would be pushed to indicate the count of numbers
/// within the vector. If ``endVector(len:)`` is called without
/// ``startVector(_:elementSize:)`` it asserts.
///
/// ```swift
/// let vectorOffset = builder.
/// endVector(len: int32Values.count)
/// ```
///
/// - Parameter len: Length of the buffer
/// - Returns: Returns the current ``Offset`` in the ``ByteBuffer``
@inline(__always)
mutating public func endVector(len: Int) -> Offset {
assert(isNested, "Calling endVector without calling startVector")
isNested = false
return Offset(offset: push(element: Int32(len)))
}
/// Creates a vector of type ``Scalar`` into the ``ByteBuffer``
///
/// ``createVector(_:)-4swl0`` writes a vector of type Scalars into
/// ``ByteBuffer``. This is a convenient method instead of calling,
/// ``startVector(_:elementSize:)`` and then ``endVector(len:)``
/// ```swift
/// let vectorOffset = builder.
/// createVector([1, 2, 3, 4])
/// ```
///
/// The underlying implementation simply calls ``createVector(_:size:)-4lhrv``
///
/// - Parameter elements: elements to be written into the buffer
/// - returns: ``Offset`` of the vector
@inline(__always)
mutating public func createVector<T: Scalar>(_ elements: [T]) -> Offset {
createVector(elements, size: elements.count)
}
/// Creates a vector of type Scalar in the buffer
///
/// ``createVector(_:)-4swl0`` writes a vector of type Scalars into
/// ``ByteBuffer``. This is a convenient method instead of calling,
/// ``startVector(_:elementSize:)`` and then ``endVector(len:)``
/// ```swift
/// let vectorOffset = builder.
/// createVector([1, 2, 3, 4], size: 4)
/// ```
///
/// - Parameter elements: Elements to be written into the buffer
/// - Parameter size: Count of elements
/// - returns: ``Offset`` of the vector
@inline(__always)
mutating public func createVector<T: Scalar>(
_ elements: [T],
size: Int) -> Offset
{
let size = size
startVector(size, elementSize: MemoryLayout<T>.size)
_bb.push(elements: elements)
return endVector(len: size)
}
#if swift(>=5.0) && !os(WASI)
@inline(__always)
/// Creates a vector of bytes in the buffer.
///
/// Allows creating a vector from `Data` without copying to a `[UInt8]`
///
/// - Parameter bytes: bytes to be written into the buffer
/// - Returns: ``Offset`` of the vector
mutating public func createVector(bytes: ContiguousBytes) -> Offset {
bytes.withUnsafeBytes {
startVector($0.count, elementSize: MemoryLayout<UInt8>.size)
_bb.push(bytes: $0)
return endVector(len: $0.count)
}
}
#endif
/// Creates a vector of type ``Enum`` into the ``ByteBuffer``
///
/// ``createVector(_:)-9h189`` writes a vector of type ``Enum`` into
/// ``ByteBuffer``. This is a convenient method instead of calling,
/// ``startVector(_:elementSize:)`` and then ``endVector(len:)``
/// ```swift
/// let vectorOffset = builder.
/// createVector([.swift, .cpp])
/// ```
///
/// The underlying implementation simply calls ``createVector(_:size:)-7cx6z``
///
/// - Parameter elements: elements to be written into the buffer
/// - returns: ``Offset`` of the vector
@inline(__always)
mutating public func createVector<T: Enum>(_ elements: [T]) -> Offset {
createVector(elements, size: elements.count)
}
/// Creates a vector of type ``Enum`` into the ``ByteBuffer``
///
/// ``createVector(_:)-9h189`` writes a vector of type ``Enum`` into
/// ``ByteBuffer``. This is a convenient method instead of calling,
/// ``startVector(_:elementSize:)`` and then ``endVector(len:)``
/// ```swift
/// let vectorOffset = builder.
/// createVector([.swift, .cpp])
/// ```
///
/// - Parameter elements: Elements to be written into the buffer
/// - Parameter size: Count of elements
/// - returns: ``Offset`` of the vector
@inline(__always)
mutating public func createVector<T: Enum>(
_ elements: [T],
size: Int) -> Offset
{
let size = size
startVector(size, elementSize: T.byteSize)
for index in stride(from: elements.count, to: 0, by: -1) {
_bb.push(value: elements[index &- 1].value, len: T.byteSize)
}
return endVector(len: size)
}
/// Creates a vector of already written offsets
///
/// ``createVector(ofOffsets:)`` creates a vector of ``Offset`` into
/// ``ByteBuffer``. This is a convenient method instead of calling,
/// ``startVector(_:elementSize:)`` and then ``endVector(len:)``.
///
/// The underlying implementation simply calls ``createVector(ofOffsets:len:)``
///
/// ```swift
/// let namesOffsets = builder.
/// createVector(ofOffsets: [name1, name2])
/// ```
/// - Parameter offsets: Array of offsets of type ``Offset``
/// - returns: ``Offset`` of the vector
@inline(__always)
mutating public func createVector(ofOffsets offsets: [Offset]) -> Offset {
createVector(ofOffsets: offsets, len: offsets.count)
}
/// Creates a vector of already written offsets
///
/// ``createVector(ofOffsets:)`` creates a vector of ``Offset`` into
/// ``ByteBuffer``. This is a convenient method instead of calling,
/// ``startVector(_:elementSize:)`` and then ``endVector(len:)``
///
/// ```swift
/// let namesOffsets = builder.
/// createVector(ofOffsets: [name1, name2])
/// ```
///
/// - Parameter offsets: Array of offsets of type ``Offset``
/// - Parameter size: Count of elements
/// - returns: ``Offset`` of the vector
@inline(__always)
mutating public func createVector(
ofOffsets offsets: [Offset],
len: Int) -> Offset
{
startVector(len, elementSize: MemoryLayout<Offset>.size)
for index in stride(from: offsets.count, to: 0, by: -1) {
push(element: offsets[index &- 1])
}
return endVector(len: len)
}
/// Creates a vector of strings
///
/// ``createVector(ofStrings:)`` creates a vector of `String` into
/// ``ByteBuffer``. This is a convenient method instead of manually
/// creating the string offsets, you simply pass it to this function
/// and it would write the strings into the ``ByteBuffer``.
/// After that it calls ``createVector(ofOffsets:)``
///
/// ```swift
/// let namesOffsets = builder.
/// createVector(ofStrings: ["Name", "surname"])
/// ```
///
/// - Parameter str: Array of string
/// - returns: ``Offset`` of the vector
@inline(__always)
mutating public func createVector(ofStrings str: [String]) -> Offset {
var offsets: [Offset] = []
for index in stride(from: 0, to: str.count, by: 1) {
offsets.append(create(string: str[index]))
}
return createVector(ofOffsets: offsets)
}
/// Creates a vector of type ``NativeStruct``.
///
/// Any swift struct in the generated code, should confirm to
/// ``NativeStruct``. Since the generated swift structs are padded
/// to the `FlatBuffers` standards.
///
/// ```swift
/// let offsets = builder.
/// createVector(ofStructs: [NativeStr(num: 1), NativeStr(num: 2)])
/// ```
///
/// - Parameter structs: A vector of ``NativeStruct``
/// - Returns: ``Offset`` of the vector
@inline(__always)
mutating public func createVector<T: NativeStruct>(ofStructs structs: [T])
-> Offset
{
startVector(
structs.count * MemoryLayout<T>.size,
elementSize: MemoryLayout<T>.alignment)
_bb.push(elements: structs)
return endVector(len: structs.count)
}
// MARK: - Inserting Structs
/// Writes a ``NativeStruct`` into the ``ByteBuffer``
///
/// Adds a native struct that's build and padded according
/// to `FlatBuffers` standards. with a predefined position.
///
/// ```swift
/// let offset = builder.create(
/// struct: NativeStr(num: 1),
/// position: 10)
/// ```
///
/// - Parameters:
/// - s: ``NativeStruct`` to be inserted into the ``ByteBuffer``
/// - position: The predefined position of the object
/// - Returns: ``Offset`` of written struct
@inline(__always)
@discardableResult
mutating public func create<T: NativeStruct>(
struct s: T, position: VOffset) -> Offset
{
let offset = create(struct: s)
_vtableStorage.add(
loc: (offset: _bb.size, position: VOffset(position)))
return offset
}
/// Writes a ``NativeStruct`` into the ``ByteBuffer``
///
/// Adds a native struct that's build and padded according
/// to `FlatBuffers` standards, directly into the buffer without
/// a predefined position.
///
/// ```swift
/// let offset = builder.create(
/// struct: NativeStr(num: 1))
/// ```
///
/// - Parameters:
/// - s: ``NativeStruct`` to be inserted into the ``ByteBuffer``
/// - Returns: ``Offset`` of written struct
@inline(__always)
@discardableResult
mutating public func create<T: NativeStruct>(
struct s: T) -> Offset
{
let size = MemoryLayout<T>.size
preAlign(len: size, alignment: MemoryLayout<T>.alignment)
_bb.push(struct: s, size: size)
return Offset(offset: _bb.size)
}
// MARK: - Inserting Strings
/// Insets a string into the buffer of type `UTF8`
///
/// Adds a swift string into ``ByteBuffer`` by encoding it
/// using `UTF8`
///
/// ```swift
/// let nameOffset = builder
/// .create(string: "welcome")
/// ```
///
/// - Parameter str: String to be serialized
/// - returns: ``Offset`` of inserted string
@inline(__always)
mutating public func create(string str: String?) -> Offset {
guard let str = str else { return Offset() }
let len = str.utf8.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)
}
/// Insets a shared string into the buffer of type `UTF8`
///
/// Adds a swift string into ``ByteBuffer`` by encoding it
/// using `UTF8`. The function will check if the string,
/// is already written to the ``ByteBuffer``
///
/// ```swift
/// let nameOffset = builder
/// .createShared(string: "welcome")
///
///
/// let secondOffset = builder
/// .createShared(string: "welcome")
///
/// assert(nameOffset.o == secondOffset.o)
/// ```
///
/// - Parameter str: String to be serialized
/// - returns: ``Offset`` of inserted string
@inline(__always)
mutating public func createShared(string str: String?) -> Offset {
guard let str = str else { return Offset() }
if let offset = stringOffsetMap[str] {
return offset
}
let offset = create(string: str)
stringOffsetMap[str] = offset
return offset
}
// MARK: - Inseting offsets
/// Writes the ``Offset`` of an already written table
///
/// Writes the ``Offset`` of a table if not empty into the
/// ``ByteBuffer``
///
/// - Parameters:
/// - offset: ``Offset`` of another object to be written
/// - position: The predefined position of the object
@inline(__always)
mutating public func add(offset: Offset, at position: VOffset) {
if offset.isEmpty { return }
add(element: refer(to: offset.o), def: 0, at: position)
}
/// Pushes a value of type ``Offset`` into the ``ByteBuffer``
/// - Parameter o: ``Offset``
/// - returns: Current position of the ``Offset``
@inline(__always)
@discardableResult
mutating public func push(element o: Offset) -> UOffset {
push(element: refer(to: o.o))
}
// MARK: - Inserting Scalars to Buffer
/// Writes a ``Scalar`` value into ``ByteBuffer``
///
/// ``add(element:def:at:)`` takes in a default value, and current value
/// and the position within the `VTable`. The default value would not
/// be serialized if the value is the same as the current value or
/// `serializeDefaults` is equal to false.
///
/// If serializing defaults is important ``init(initialSize:serializeDefaults:)``,
/// passing true for `serializeDefaults` would do the job.
///
/// ```swift
/// // Adds 10 to the buffer
/// builder.add(element: Int(10), def: 1, position 12)
/// ```
///
/// *NOTE: Never call this manually*
///
/// - Parameters:
/// - element: Element to insert
/// - def: Default value for that element
/// - position: The predefined position of the element
@inline(__always)
mutating public func add<T: Scalar>(
element: T,
def: T,
at position: VOffset)
{
if element == def && !serializeDefaults { return }
track(offset: push(element: element), at: position)
}
/// Writes a optional ``Scalar`` value into ``ByteBuffer``
///
/// Takes an optional value to be written into the ``ByteBuffer``
///
/// *NOTE: Never call this manually*
///
/// - Parameters:
/// - element: Optional element of type scalar
/// - position: The predefined position of the element
@inline(__always)
mutating public func add<T: Scalar>(element: T?, at position: VOffset) {
guard let element = element else { return }
track(offset: push(element: element), at: position)
}
/// Pushes a values of type ``Scalar`` into the ``ByteBuffer``
///
/// *NOTE: Never call this manually*
///
/// - Parameter element: Element to insert
/// - returns: position of the Element
@inline(__always)
@discardableResult
mutating public func push<T: Scalar>(element: T) -> UOffset {
let size = MemoryLayout<T>.size
preAlign(
len: size,
alignment: size)
_bb.push(value: element, len: size)
return _bb.size
}
@inline(__always)
public func read<T>(def: T.Type, position: Int) -> T {
_bb.read(def: def, position: position)
}
}
extension FlatBufferBuilder: CustomDebugStringConvertible {
public var debugDescription: String {
"""
buffer debug:
\(_bb)
builder debug:
{ finished: \(finished), serializeDefaults: \(
serializeDefaults), isNested: \(isNested) }
"""
}
typealias FieldLoc = (offset: UOffset, position: VOffset)
/// VTableStorage is a class to contain the VTable buffer that would be serialized into buffer
@usableFromInline
internal class VTableStorage {
/// Memory check since deallocating each time we want to clear would be expensive
/// and memory leaks would happen if we dont deallocate the first allocated memory.
/// memory is promised to be available before adding `FieldLoc`
private var memoryInUse = false
/// Size of FieldLoc in memory
let size = MemoryLayout<FieldLoc>.stride
/// Memeory buffer
var memory: UnsafeMutableRawBufferPointer!
/// Capacity of the current buffer
var capacity: Int = 0
/// Maximuim offset written to the class
var maxOffset: VOffset = 0
/// number of fields written into the buffer
var numOfFields: Int = 0
/// Last written Index
var writtenIndex: Int = 0
/// Creates the memory to store the buffer in
@usableFromInline
@inline(__always)
init() {
memory = UnsafeMutableRawBufferPointer.allocate(
byteCount: 0,
alignment: 0)
}
@inline(__always)
deinit {
memory.deallocate()
}
/// Builds a buffer with byte count of fieldloc.size * count of field numbers
/// - Parameter count: number of fields to be written
@inline(__always)
func start(count: Int) {
assert(count >= 0, "number of fields should NOT be negative")
let capacity = count &* size
ensure(space: capacity)
}
/// Adds a FieldLoc into the buffer, which would track how many have been written,
/// and max offset
/// - Parameter loc: Location of encoded element
@inline(__always)
func add(loc: FieldLoc) {
memory.baseAddress?.advanced(by: writtenIndex).storeBytes(
of: loc,
as: FieldLoc.self)
writtenIndex = writtenIndex &+ size
numOfFields = numOfFields &+ 1
maxOffset = max(loc.position, maxOffset)
}
/// Clears the data stored related to the encoded buffer
@inline(__always)
func clear() {
maxOffset = 0
numOfFields = 0
writtenIndex = 0
}
/// Ensure that the buffer has enough space instead of recreating the buffer each time.
/// - Parameter space: space required for the new vtable
@inline(__always)
func ensure(space: Int) {
guard space &+ writtenIndex > capacity else { return }
memory.deallocate()
memory = UnsafeMutableRawBufferPointer.allocate(
byteCount: space,
alignment: size)
capacity = space
}
/// Loads an object of type `FieldLoc` from buffer memory
/// - Parameter index: index of element
/// - Returns: a FieldLoc at index
@inline(__always)
func load(at index: Int) -> FieldLoc {
memory.load(fromByteOffset: index, as: FieldLoc.self)
}
}
}