blob: 8dced85b41e7d0818940565af931e8c663271cdc [file] [log] [blame]
/*
* 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
#if canImport(Common)
import Common
#endif
/// `ByteBuffer` is the interface that stores the data for a `Flatbuffers` object
/// it allows users to Construct their buffers internally without much cost to performance
@usableFromInline
struct _InternalByteBuffer {
/// Storage is a container that would hold the memory pointer to solve the issue of
/// deallocating the memory that was held by (memory: UnsafeMutableRawPointer)
@usableFromInline
final class Storage {
/// pointer to the start of the buffer object in memory
private(set) var memory: UnsafeMutableRawPointer
@usableFromInline
init(count: Int, alignment: Int) {
memory = UnsafeMutableRawPointer.allocate(
byteCount: count,
alignment: alignment)
}
deinit {
memory.deallocate()
}
@usableFromInline
func initialize(for size: Int) {
memset(memory, 0, size)
}
/// Reallocates the buffer incase the object to be written doesnt fit in the current buffer
/// - Parameter size: Size of the current object
@usableFromInline
func reallocate(
capacity: Int,
writerSize: Int,
currentWritingIndex: Int,
alignment: Int)
{
let newData = UnsafeMutableRawPointer.allocate(
byteCount: capacity,
alignment: alignment)
memset(newData, 0, capacity &- writerSize)
memcpy(
newData.advanced(by: capacity &- writerSize),
memory.advanced(by: currentWritingIndex),
writerSize)
memory.deallocate()
memory = newData
}
}
@usableFromInline var _storage: Storage
private let initialSize: Int
/// The size of the elements written to the buffer + their paddings
private var _writerSize: Int = 0
/// Alignment of the current memory being written to the buffer
var alignment = 1
/// Current Index which is being used to write to the buffer, it is written from the end to the start of the buffer
var writerIndex: Int { capacity &- _writerSize }
/// Reader is the position of the current Writer Index (capacity - size)
public var reader: Int { writerIndex }
/// Current size of the buffer
public var size: UOffset { UOffset(_writerSize) }
/// Public Pointer to the buffer object in memory. This should NOT be modified for any reason
@usableFromInline
var memory: UnsafeMutableRawPointer { _storage.memory }
/// Current capacity for the buffer
public private(set) var capacity: Int
/// Constructor that creates a Flatbuffer instance with a size
/// - Parameter:
/// - size: Length of the buffer
/// - allowReadingUnalignedBuffers: allow reading from unaligned buffer
init(initialSize size: Int) {
initialSize = size.convertToPowerofTwo
capacity = initialSize
_storage = Storage(count: initialSize, alignment: alignment)
_storage.initialize(for: initialSize)
}
/// Fills the buffer with padding by adding to the writersize
/// - Parameter padding: Amount of padding between two to be serialized objects
@inline(__always)
@usableFromInline
mutating func fill(padding: Int) {
assert(padding >= 0, "Fill should be larger than or equal to zero")
ensureSpace(size: padding)
_writerSize = _writerSize &+ (MemoryLayout<UInt8>.size &* padding)
}
/// Adds an array of type Scalar to the buffer memory
/// - Parameter elements: An array of Scalars
@inline(__always)
@usableFromInline
mutating func push<T: Scalar>(elements: [T]) {
elements.withUnsafeBytes { ptr in
ensureSpace(size: ptr.count)
memcpy(
_storage.memory.advanced(by: writerIndex &- ptr.count),
ptr.baseAddress!,
ptr.count)
_writerSize = _writerSize &+ ptr.count
}
}
/// Adds an array of type Scalar to the buffer memory
/// - Parameter elements: An array of Scalars
@inline(__always)
@usableFromInline
mutating func push<T: NativeStruct>(elements: [T]) {
elements.withUnsafeBytes { ptr in
ensureSpace(size: ptr.count)
memcpy(
_storage.memory.advanced(by: writerIndex &- ptr.count),
ptr.baseAddress!,
ptr.count)
_writerSize = _writerSize &+ ptr.count
}
}
/// Adds a `ContiguousBytes` to buffer memory
/// - Parameter value: bytes to copy
#if !os(WASI)
@inline(__always)
@usableFromInline
mutating func push(bytes: ContiguousBytes) {
bytes.withUnsafeBytes { ptr in
ensureSpace(size: ptr.count)
memcpy(
_storage.memory.advanced(by: writerIndex &- ptr.count),
ptr.baseAddress!,
ptr.count)
_writerSize = _writerSize &+ ptr.count
}
}
#endif
/// Adds an object of type NativeStruct into the buffer
/// - Parameters:
/// - value: Object that will be written to the buffer
/// - size: size to subtract from the WriterIndex
@usableFromInline
@inline(__always)
mutating func push<T: NativeStruct>(struct value: T, size: Int) {
ensureSpace(size: size)
withUnsafePointer(to: value) {
memcpy(
_storage.memory.advanced(by: writerIndex &- size),
$0,
size)
_writerSize = _writerSize &+ size
}
}
/// Adds an object of type Scalar into the buffer
/// - Parameters:
/// - value: Object that will be written to the buffer
/// - len: Offset to subtract from the WriterIndex
@inline(__always)
@usableFromInline
mutating func push<T: Scalar>(value: T, len: Int) {
ensureSpace(size: len)
withUnsafePointer(to: value) {
memcpy(
_storage.memory.advanced(by: writerIndex &- len),
$0,
len)
_writerSize = _writerSize &+ len
}
}
/// Adds a RawPointer into the buffer
/// - Parameter pointer: pointer to be copied into the buffer
/// - Parameter len: length of the data
@inline(__always)
mutating func writeBytes(_ ptr: UnsafeRawPointer, len: Int) {
ensureSpace(size: len)
memcpy(
_storage.memory.advanced(by: writerIndex &- len),
ptr,
len)
_writerSize = _writerSize &+ len
}
/// Write stores an object into the buffer directly or indirectly.
///
/// Direct: ignores the capacity of buffer which would mean we are referring to the direct point in memory
/// indirect: takes into respect the current capacity of the buffer (capacity - index), writing to the buffer from the end
/// - Parameters:
/// - value: Value that needs to be written to the buffer
/// - index: index to write to
/// - direct: Should take into consideration the capacity of the buffer
@inline(__always)
func write<T>(value: T, index: Int, direct: Bool = false) {
var index = index
if !direct {
index = capacity &- index
}
assert(index < capacity, "Write index is out of writing bound")
assert(index >= 0, "Writer index should be above zero")
_ = withUnsafePointer(to: value) {
memcpy(
_storage.memory.advanced(by: index),
$0,
MemoryLayout<T>.size)
}
}
/// Makes sure that buffer has enouch space for each of the objects that will be written into it
/// - Parameter size: size of object
@discardableResult
@usableFromInline
mutating func ensureSpace(size: Int) -> Int {
let expectedWriterIndex = size &+ _writerSize
if expectedWriterIndex > capacity {
let currentWritingIndex = capacity &- _writerSize
while capacity <= expectedWriterIndex {
capacity = capacity << 1
}
/// solution take from Apple-NIO
capacity = capacity.convertToPowerofTwo
_storage.reallocate(
capacity: capacity,
writerSize: _writerSize,
currentWritingIndex: currentWritingIndex,
alignment: alignment)
}
assert(size < FlatBufferMaxSize, "Buffer can't grow beyond 2 Gigabytes")
return size
}
/// pops the written VTable if it's already written into the buffer
/// - Parameter size: size of the `VTable`
@usableFromInline
@inline(__always)
mutating func pop(_ size: Int) {
assert(
(_writerSize &- size) > 0,
"New size should NOT be a negative number")
memset(_storage.memory.advanced(by: writerIndex), 0, _writerSize &- size)
_writerSize = size
}
/// Clears the current size of the buffer
@inline(__always)
mutating public func clearSize() {
_writerSize = 0
}
/// Clears the current instance of the buffer, replacing it with new memory
@inline(__always)
mutating public func clear(keepingCapacity: Bool = false) {
_writerSize = 0
alignment = 1
if keepingCapacity {
_storage.initialize(for: capacity)
} else {
capacity = initialSize
_storage = Storage(count: initialSize, alignment: alignment)
}
}
/// Reads an object from the buffer
/// - Parameters:
/// - def: Type of the object
/// - position: the index of the object in the buffer
@inline(__always)
public func read<T>(def: T.Type, position: Int) -> T {
_storage.memory
.advanced(by: position)
.bindMemory(to: T.self, capacity: 1)
.pointee
}
@discardableResult
@inline(__always)
func withUnsafeBytes<T>(
_ body: (UnsafeRawBufferPointer) throws
-> T) rethrows -> T
{
try body(
UnsafeRawBufferPointer(
start: _storage.memory,
count: capacity))
}
@discardableResult
@inline(__always)
func withUnsafeSlicedBytes<T>(
_ body: (UnsafeRawBufferPointer) throws
-> T) rethrows -> T
{
try body(
UnsafeRawBufferPointer(
start: _storage.memory.advanced(by: writerIndex),
count: capacity &- writerIndex))
}
@discardableResult
@inline(__always)
func withUnsafeRawPointer<T>(
_ body: (UnsafeMutableRawPointer) throws
-> T) rethrows -> T
{
try body(_storage.memory)
}
@discardableResult
@inline(__always)
func readWithUnsafeRawPointer<T>(
position: Int,
_ body: (UnsafeRawPointer) throws -> T) rethrows -> T
{
try body(_storage.memory.advanced(by: position))
}
}
extension _InternalByteBuffer: CustomDebugStringConvertible {
public var debugDescription: String {
"""
buffer located at: \(_storage.memory), with capacity of \(capacity)
{ writerSize: \(_writerSize), readerSize: \(reader), writerIndex: \(
writerIndex) }
"""
}
}