blob: 0c65c6f3a520ba328f16307eaa1d25bf81132bc4 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
const fileIdentifierLength = 4
"""
Scalar
A Union of the Julia types `T <: Number` that are allowed in FlatBuffers schema
"""
const Scalar = Union{Bool,
Int8, Int16, Int32, Int64,
UInt8, UInt16, UInt32, UInt64,
Float32, Float64, Enum}
"""
Builder is a state machine for creating FlatBuffer objects.
Use a Builder to construct object(s) starting from leaf nodes.
A Builder constructs byte buffers in a last-first manner for simplicity and
performance.
"""
mutable struct Builder
bytes::Vector{UInt8}
minalign::Int
vtable::Vector{UOffsetT}
objectend::UOffsetT
vtables::Vector{UOffsetT}
head::UOffsetT
nested::Bool
finished::Bool
sharedstrings::Dict{String, UOffsetT}
end
bytes(b::Builder) = getfield(b, :bytes)
Builder(size=0) = Builder(zeros(UInt8, size), 1, UOffsetT[], UOffsetT(0), UOffsetT[], UOffsetT(size), false, false, Dict{String, UOffsetT}())
function reset!(b::Builder)
empty!(b.bytes)
empty!(b.vtable)
emtpy!(b.vtables)
empty!(b.sharedstrings)
b.minalign = 1
b.nested = false
b.finished = false
b.head = 0
return
end
Base.write(sink::Builder, o, x::Union{Bool,UInt8}) = sink.bytes[o+1] = UInt8(x)
function Base.write(sink::Builder, off, x::T) where {T}
off += 1
for (i, ind) = enumerate(off:(off + sizeof(T) - 1))
sink.bytes[ind] = (x >> ((i-1) * 8)) % UInt8
end
end
Base.write(b::Builder, o, x::Float32) = write(b, o, reinterpret(UInt32, x))
Base.write(b::Builder, o, x::Float64) = write(b, o, reinterpret(UInt64, x))
Base.write(b::Builder, o, x::Enum) = write(b, o, basetype(x)(x))
"""
`finishedbytes` returns a pointer to the written data in the byte buffer.
Panics if the builder is not in a finished state (which is caused by calling
`finish!()`).
"""
function finishedbytes(b::Builder)
assertfinished(b)
return view(b.bytes, (b.head + 1):length(b.bytes))
end
function startobject!(b::Builder, numfields)
assertnotnested(b)
b.nested = true
resize!(b.vtable, numfields)
fill!(b.vtable, 0)
b.objectend = offset(b)
return
end
"""
WriteVtable serializes the vtable for the current object, if applicable.
Before writing out the vtable, this checks pre-existing vtables for equality
to this one. If an equal vtable is found, point the object to the existing
vtable and return.
Because vtable values are sensitive to alignment of object data, not all
logically-equal vtables will be deduplicated.
A vtable has the following format:
<VOffsetT: size of the vtable in bytes, including this value>
<VOffsetT: size of the object in bytes, including the vtable offset>
<VOffsetT: offset for a field> * N, where N is the number of fields in
the schema for this type. Includes deprecated fields.
Thus, a vtable is made of 2 + N elements, each SizeVOffsetT bytes wide.
An object has the following format:
<SOffsetT: offset to this object's vtable (may be negative)>
<byte: data>+
"""
function writevtable!(b::Builder)
# Prepend a zero scalar to the object. Later in this function we'll
# write an offset here that points to the object's vtable:
prepend!(b, SOffsetT(0))
objectOffset = offset(b)
existingVtable = UOffsetT(0)
# Trim vtable of trailing zeroes.
i = findlast(!iszero, b.vtable)
if i !== nothing
resize!(b.vtable, i)
end
# Search backwards through existing vtables, because similar vtables
# are likely to have been recently appended. See
# BenchmarkVtableDeduplication for a case in which this heuristic
# saves about 30% of the time used in writing objects with duplicate
# tables.
for i = length(b.vtables):-1:1
# Find the other vtable, which is associated with `i`:
vt2Offset = b.vtables[i]
vt2Start = length(b.bytes) - vt2Offset
vt2Len = readbuffer(b.bytes, vt2Start, VOffsetT)
metadata = VtableMetadataFields * sizeof(VOffsetT)
vt2End = vt2Start + vt2Len
vt2 = view(b.bytes, (vt2Start + metadata + 1):vt2End) #TODO: might need a +1 on the start of range here
# Compare the other vtable to the one under consideration.
# If they are equal, store the offset and break:
if vtableEqual(b.vtable, objectOffset, vt2)
existingVtable = vt2Offset
break
end
end
if existingVtable == 0
# Did not find a vtable, so write this one to the buffer.
# Write out the current vtable in reverse , because
# serialization occurs in last-first order:
for i = length(b.vtable):-1:1
off::UOffsetT = 0
if b.vtable[i] != 0
# Forward reference to field;
# use 32bit number to assert no overflow:
off = objectOffset - b.vtable[i]
end
prepend!(b, VOffsetT(off))
end
# The two metadata fields are written last.
# First, store the object bytesize:
objectSize = objectOffset - b.objectend
prepend!(b, VOffsetT(objectSize))
# Second, store the vtable bytesize:
vbytes = (length(b.vtable) + VtableMetadataFields) * sizeof(VOffsetT)
prepend!(b, VOffsetT(vbytes))
# Next, write the offset to the new vtable in the
# already-allocated SOffsetT at the beginning of this object:
objectStart = SOffsetT(length(b.bytes) - objectOffset)
write(b, objectStart, SOffsetT(offset(b) - objectOffset))
# Finally, store this vtable in memory for future
# deduplication:
push!(b.vtables, offset(b))
else
# Found a duplicate vtable.
objectStart = SOffsetT(length(b.bytes) - objectOffset)
b.head = objectStart
# Write the offset to the found vtable in the
# already-allocated SOffsetT at the beginning of this object:
write(b, b.head, SOffsetT(existingVtable) - SOffsetT(objectOffset))
end
empty!(b.vtable)
return objectOffset
end
"""
`endobject` writes data necessary to finish object construction.
"""
function endobject!(b::Builder)
assertnested(b)
n = writevtable!(b)
b.nested = false
return n
end
offset(b::Builder) = UOffsetT(length(b.bytes) - b.head)
pad!(b::Builder, n) = foreach(x->place!(b, 0x00), 1:n)
"""
`prep!` prepares to write an element of `size` after `additionalbytes`
have been written, e.g. if you write a string, you need to align such
the int length field is aligned to sizeof(Int32), and the string data follows it
directly.
If all you need to do is align, `additionalbytes` will be 0.
"""
function prep!(b::Builder, size, additionalbytes)
# Track the biggest thing we've ever aligned to.
if size > b.minalign
b.minalign = size
end
# Find the amount of alignment needed such that `size` is properly
# aligned after `additionalBytes`:
alignsize = xor(Int(-1), (length(b.bytes) - b.head) + additionalbytes) + 1
alignsize &= (size - 1)
# Reallocate the buffer if needed:
totalsize = alignsize + size + additionalbytes
if b.head <= totalsize
len = length(b.bytes)
prepend!(b.bytes, zeros(UInt8, totalsize))
b.head += length(b.bytes) - len
end
pad!(b, alignsize)
return
end
function Base.prepend!(b::Builder, x::T) where {T}
prep!(b, sizeof(T), 0)
place!(b, x)
return
end
function prependoffset!(b::Builder, off)
prep!(b, sizeof(Int32), 0) # Ensure alignment is already done.
if !(off <= offset(b))
throw(ArgumentError("unreachable: $off <= $(offset(b))"))
end
place!(b, SOffsetT(offset(b) - off + sizeof(SOffsetT)))
return
end
function prependoffsetslot!(b::Builder, o::Int, x::T, d) where {T}
if x != T(d)
prependoffset!(b, x)
slot!(b, o)
end
return
end
"""
`startvector` initializes bookkeeping for writing a new vector.
A vector has the following format:
<UOffsetT: number of elements in this vector>
<T: data>+, where T is the type of elements of this vector.
"""
function startvector!(b::Builder, elemSize, numElems, alignment)
assertnotnested(b)
b.nested = true
prep!(b, sizeof(UInt32), elemSize * numElems)
prep!(b, alignment, elemSize * numElems)
return offset(b)
end
"""
`endvector` writes data necessary to finish vector construction.
"""
function endvector!(b::Builder, vectorNumElems)
assertnested(b)
place!(b, UOffsetT(vectorNumElems))
b.nested = false
return offset(b)
end
function createsharedstring!(b::Builder, s::AbstractString)
get!(b.sharedstrings, s) do
createstring!(b, s)
end
end
"""
`createstring!` writes a null-terminated string as a vector.
"""
function createstring!(b::Builder, s::Union{AbstractString, AbstractVector{UInt8}})
assertnotnested(b)
b.nested = true
s = codeunits(s)
prep!(b, sizeof(UInt32), sizeof(s) + 1)
place!(b, UInt8(0))
l = sizeof(s)
b.head -= l
copyto!(b.bytes, b.head+1, s, 1, l)
return endvector!(b, sizeof(s))
end
createbytevector(b::Builder, v) = createstring!(b, v)
function assertnested(b::Builder)
# If you get this assert, you're in an object while trying to write
# data that belongs outside of an object.
# To fix this, write non-inline data (like vectors) before creating
# objects.
if !b.nested
throw(ArgumentError("Incorrect creation order: must be inside object."))
end
return
end
function assertnotnested(b::Builder)
# If you hit this, you're trying to construct a Table/Vector/String
# during the construction of its parent table (between the MyTableBuilder
# and builder.Finish()).
# Move the creation of these view-objects to above the MyTableBuilder to
# not get this assert.
# Ignoring this assert may appear to work in simple cases, but the reason
# it is here is that storing objects in-line may cause vtable offsets
# to not fit anymore. It also leads to vtable duplication.
if b.nested
throw(ArgumentError("Incorrect creation order: object must not be nested."))
end
return
end
function assertfinished(b::Builder)
# If you get this assert, you're attempting to get access a buffer
# which hasn't been finished yet. Be sure to call builder.Finish()
# with your root table.
# If you really need to access an unfinished buffer, use the bytes
# buffer directly.
if !b.finished
throw(ArgumentError("Incorrect use of FinishedBytes(): must call 'Finish' first."))
end
end
"""
`prependslot!` prepends a `T` onto the object at vtable slot `o`.
If value `x` equals default `d`, then the slot will be set to zero and no
other data will be written.
"""
function prependslot!(b::Builder, o::Int, x::T, d, sh=false) where {T <: Scalar}
if x != T(d)
prepend!(b, x)
slot!(b, o)
end
return
end
"""
`prependstructslot!` prepends a struct onto the object at vtable slot `o`.
Structs are stored inline, so nothing additional is being added.
In generated code, `d` is always 0.
"""
function prependstructslot!(b::Builder, voffset, x, d)
if x != d
assertnested(b)
if x != offset(b)
throw(ArgumentError("inline data write outside of object"))
end
slot!(b, voffset)
end
return
end
"""
`slot!` sets the vtable key `voffset` to the current location in the buffer.
"""
function slot!(b::Builder, slotnum)
b.vtable[slotnum + 1] = offset(b)
end
# FinishWithFileIdentifier finalizes a buffer, pointing to the given `rootTable`.
# as well as applys a file identifier
function finishwithfileidentifier(b::Builder, rootTable, fid)
if length(fid) != fileIdentifierLength
error("incorrect file identifier length")
end
# In order to add a file identifier to the flatbuffer message, we need
# to prepare an alignment and file identifier length
prep!(b, b.minalign, sizeof(Int32) + fileIdentifierLength)
for i = fileIdentifierLength:-1:1
# place the file identifier
place!(b, fid[i])
end
# finish
finish!(b, rootTable)
end
"""
`finish!` finalizes a buffer, pointing to the given `rootTable`.
"""
function finish!(b::Builder, rootTable)
assertnotnested(b)
prep!(b, b.minalign, sizeof(UOffsetT))
prependoffset!(b, UOffsetT(rootTable))
b.finished = true
return
end
"vtableEqual compares an unwritten vtable to a written vtable."
function vtableEqual(a::Vector{UOffsetT}, objectStart, b::AbstractVector{UInt8})
if length(a) * sizeof(VOffsetT) != length(b)
return false
end
for i = 0:(length(a)-1)
x = read(IOBuffer(view(b, (i * sizeof(VOffsetT) + 1):length(b))), VOffsetT)
# Skip vtable entries that indicate a default value.
x == 0 && a[i+1] == 0 && continue
y = objectStart - a[i+1]
x != y && return false
end
return true
end
"""
`place!` prepends a `T` to the Builder, without checking for space.
"""
function place!(b::Builder, x::T) where {T}
b.head -= sizeof(T)
write(b, b.head, x)
return
end