blob: 368be2f30bd7baa401d4b423da134d0cac82dfa6 [file] [log] [blame]
/*
* Copyright 2014 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.
*/
package com.google.flatbuffers;
import static com.google.flatbuffers.Constants.*;
import java.util.Arrays;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
/**
* Class that helps you build a FlatBuffer. See the section
* <a href="http://google.github.io/flatbuffers/md__java_usage.html">"Use in Java"</a> in the
* main FlatBuffers documentation.
*/
public class FlatBufferBuilder {
ByteBuffer bb; // Where we construct the FlatBuffer.
int space; // Remaining space in the ByteBuffer.
static final Charset utf8charset = Charset.forName("UTF-8");
int minalign = 1; // Minimum alignment encountered so far.
int[] vtable = null; // The vtable for the current table.
int vtable_in_use = 0; // The amount of fields we're actually using.
boolean nested = false; // Whether we are currently serializing a table.
int object_start; // Starting offset of the current struct/table.
int[] vtables = new int[16]; // List of offsets of all vtables.
int num_vtables = 0; // Number of entries in `vtables` in use.
int vector_num_elems = 0; // For the current vector being built.
boolean force_defaults = false; // False omits default values from the serialized data
/**
* Start with a buffer of size {@code initial_size}, then grow as required.
*
* @param initial_size The initial size of the internal buffer to use
*/
public FlatBufferBuilder(int initial_size) {
if (initial_size <= 0) initial_size = 1;
space = initial_size;
bb = newByteBuffer(initial_size);
}
/**
* Start with a buffer of 1KiB, then grow as required.
*/
public FlatBufferBuilder() {
this(1024);
}
/**
* Alternative constructor allowing reuse of {@link ByteBuffer}s. The builder
* can still grow the buffer as necessary. User classes should make sure
* to call {@link #dataBuffer()} to obtain the resulting encoded message
*
* @param existing_bb The byte buffer to reuse
*/
public FlatBufferBuilder(ByteBuffer existing_bb) {
init(existing_bb);
}
/**
* Alternative initializer that allows reusing this object on an existing
* ByteBuffer. This method resets the builder's internal state, but keeps
* objects that have been allocated for temporary storage.
*
* @param existing_bb The byte buffer to reuse
* @return this
*/
public FlatBufferBuilder init(ByteBuffer existing_bb){
bb = existing_bb;
bb.clear();
bb.order(ByteOrder.LITTLE_ENDIAN);
minalign = 1;
space = bb.capacity();
vtable_in_use = 0;
nested = false;
object_start = 0;
num_vtables = 0;
vector_num_elems = 0;
return this;
}
static ByteBuffer newByteBuffer(int capacity) {
ByteBuffer newbb = ByteBuffer.allocate(capacity);
newbb.order(ByteOrder.LITTLE_ENDIAN);
return newbb;
}
/**
* Doubles the size of the backing {link ByteBuffer} and copies the old data towards the
* end of the new buffer (since we build the buffer backwards).
*
* @param bb The current buffer with the existing data
* @return A new byte buffer with the old data copied copied to it. The data is
* located at the end of the buffer.
*/
static ByteBuffer growByteBuffer(ByteBuffer bb) {
int old_buf_size = bb.capacity();
if ((old_buf_size & 0xC0000000) != 0) // Ensure we don't grow beyond what fits in an int.
throw new AssertionError("FlatBuffers: cannot grow buffer beyond 2 gigabytes.");
int new_buf_size = old_buf_size << 1;
bb.position(0);
ByteBuffer nbb = newByteBuffer(new_buf_size);
nbb.position(new_buf_size - old_buf_size);
nbb.put(bb);
return nbb;
}
/**
* Offset relative to the end of the buffer.
*
* @return Offset relative to the end of the buffer.
*/
public int offset() {
return bb.capacity() - space;
}
/**
* Add zero valued bytes to prepare a new entry to be added
*
* @param byte_size Number of bytes to add.
*/
public void pad(int byte_size) {
for (int i = 0; i < byte_size; i++) bb.put(--space, (byte)0);
}
/**
* Prepare to write an element of {@code size} after {@code additional_bytes}
* have been written, e.g. if you write a string, you need to align such
* the int length field is aligned to {@link com.google.flatbuffers.Constants#SIZEOF_INT}, and
* the string data follows it directly. If all you need to do is alignment, {@code additional_bytes}
* will be 0.
*
* @param size This is the of the new element to write
* @param additional_bytes The padding size
*/
public void prep(int size, int additional_bytes) {
// Track the biggest thing we've ever aligned to.
if (size > minalign) minalign = size;
// Find the amount of alignment needed such that `size` is properly
// aligned after `additional_bytes`
int align_size = ((~(bb.capacity() - space + additional_bytes)) + 1) & (size - 1);
// Reallocate the buffer if needed.
while (space < align_size + size + additional_bytes) {
int old_buf_size = bb.capacity();
bb = growByteBuffer(bb);
space += bb.capacity() - old_buf_size;
}
pad(align_size);
}
// Add a scalar to the buffer, backwards from the current location.
// Doesn't align nor check for space.
public void putBoolean(boolean x) { bb.put (space -= 1, (byte)(x ? 1 : 0)); }
public void putByte (byte x) { bb.put (space -= 1, x); }
public void putShort (short x) { bb.putShort (space -= 2, x); }
public void putInt (int x) { bb.putInt (space -= 4, x); }
public void putLong (long x) { bb.putLong (space -= 8, x); }
public void putFloat (float x) { bb.putFloat (space -= 4, x); }
public void putDouble (double x) { bb.putDouble(space -= 8, x); }
// Adds a scalar to the buffer, properly aligned, and the buffer grown
// if needed.
public void addBoolean(boolean x) { prep(1, 0); putBoolean(x); }
public void addByte (byte x) { prep(1, 0); putByte (x); }
public void addShort (short x) { prep(2, 0); putShort (x); }
public void addInt (int x) { prep(4, 0); putInt (x); }
public void addLong (long x) { prep(8, 0); putLong (x); }
public void addFloat (float x) { prep(4, 0); putFloat (x); }
public void addDouble (double x) { prep(8, 0); putDouble (x); }
/**
* Adds on offset, relative to where it will be written.
*
* @param off The offset to add
*/
public void addOffset(int off) {
prep(SIZEOF_INT, 0); // Ensure alignment is already done.
assert off <= offset();
off = offset() - off + SIZEOF_INT;
putInt(off);
}
/**
* Start a new array/vector of objects. Users usually will not call
* this directly. The {@code FlatBuffers} compiler will create a start/end
* method for vector types in generated code.
* <p>
* The expected sequence of calls is:
* <ol>
* <li>Start the array using this method.</li>
* <li>Call {@link #addOffset(int)} {@code num_elems} number of times to set
* the offset of each element in the array.</li>
* <li>Call {@link #endVector()} to retrieve the offset of the array.</li>
* </ol>
* <p>
* For example, to create an array of strings, do:
* <pre>{@code
* // Need 10 strings
* FlatBufferBuilder builder = new FlatBufferBuilder(existingBuffer);
* int[] offsets = new int[10];
*
* for (int i = 0; i < 10; i++) {
* offsets[i] = fbb.createString(" " + i);
* }
*
* // Have the strings in the buffer, but don't have a vector.
* // Add a vector that references the newly created strings:
* builder.startVector(4, offsets.length, 4);
*
* // Add each string to the newly created vector
* // The strings are added in reverse order since the buffer
* // is filled in back to front
* for (int i = offsets.length - 1; i >= 0; i--) {
* builder.addOffset(offsets[i]);
* }
*
* // Finish off the vector
* int offsetOfTheVector = fbb.endVector();
* }</pre>
*
* @param elem_size The size of each element in the array
* @param num_elems The number of elements in the array
* @param alignment The alignment of the array
*/
public void startVector(int elem_size, int num_elems, int alignment) {
notNested();
vector_num_elems = num_elems;
prep(SIZEOF_INT, elem_size * num_elems);
prep(alignment, elem_size * num_elems); // Just in case alignment > int.
}
/**
* Finish off the creation of an array and all its elements. The array
* must be created with {@link #startVector(int, int, int)}.
*
* @return The offset at which the newly created array starts.
* @see #startVector(int, int, int)
*/
public int endVector() {
putInt(vector_num_elems);
return offset();
}
/**
* Encode the string {@code s} in the buffer using UTF-8.
*
* @param s The string to encode
* @return The offset in the buffer where the encoded string starts
*/
public int createString(String s) {
byte[] utf8 = s.getBytes(utf8charset);
addByte((byte)0);
startVector(1, utf8.length, 1);
bb.position(space -= utf8.length);
bb.put(utf8, 0, utf8.length);
return endVector();
}
/**
* Encode the string {@code s} in the buffer using UTF-8.
*
* @param s An already encoded UTF-8 string
* @return The offset in the buffer where the encoded string starts
*/
public int createString(ByteBuffer s) {
int length = s.remaining();
addByte((byte)0);
startVector(1, length, 1);
bb.position(space -= length);
bb.put(s);
return endVector();
}
/**
* Should not be creating any other object, string or vector
* while an object is being constructed
*/
public void notNested() {
if (nested)
throw new AssertionError("FlatBuffers: object serialization must not be nested.");
}
/**
* Structures are always stored inline, they need to be created right
* where they're used. You'll get this assertion failure if you
* created it elsewhere.
*
* @param obj The offset of the created object
*/
public void Nested(int obj) {
if (obj != offset())
throw new AssertionError("FlatBuffers: struct must be serialized inline.");
}
/**
* Start encoding a new object in the buffer. Users will not usually need to
* call this directly. The {@code FlatBuffers} compiler will generate helper methods
* that call this method internally.
* <p>
* For example, using the "Monster" code found on the
* <a href="http://google.github.io/flatbuffers/md__java_usage.html">landing page</a>. An
* object of type {@code Monster} can be created using the following code:
*
* <pre>{@code
* int testArrayOfString = Monster.createTestarrayofstringVector(fbb, new int[] {
* fbb.createString("test1"),
* fbb.createString("test2")
* });
*
* Monster.startMonster(fbb);
* Monster.addPos(fbb, Vec3.createVec3(fbb, 1.0f, 2.0f, 3.0f, 3.0,
* Color.Green, (short)5, (byte)6));
* Monster.addHp(fbb, (short)80);
* Monster.addName(fbb, str);
* Monster.addInventory(fbb, inv);
* Monster.addTestType(fbb, (byte)Any.Monster);
* Monster.addTest(fbb, mon2);
* Monster.addTest4(fbb, test4);
* Monster.addTestarrayofstring(fbb, testArrayOfString);
* int mon = Monster.endMonster(fbb);
* }</pre>
* <p>
* Here:
* <ul>
* <li>The call to {@code Monster#startMonster(FlatBufferBuilder)} will call this
* method with the right number of fields set.</li>
* <li>{@code Monster#endMonster(FlatBufferBuilder)} will ensure {@link #endObject()} is called.</li>
* </ul>
* <p>
* It's not recommended to call this method directly. If it's called manually, you must ensure
* to audit all calls to it whenever fields are added or removed from your schema. This is
* automatically done by the code generated by the {@code FlatBuffers} compiler.
*
* @param numfields The number of fields found in this object.
*/
public void startObject(int numfields) {
notNested();
if (vtable == null || vtable.length < numfields) vtable = new int[numfields];
vtable_in_use = numfields;
Arrays.fill(vtable, 0, vtable_in_use, 0);
nested = true;
object_start = offset();
}
// Add a scalar to a table at `o` into its vtable, with value `x` and default `d`
public void addBoolean(int o, boolean x, boolean d) { if(force_defaults || x != d) { addBoolean(x); slot(o); } }
public void addByte (int o, byte x, int d) { if(force_defaults || x != d) { addByte (x); slot(o); } }
public void addShort (int o, short x, int d) { if(force_defaults || x != d) { addShort (x); slot(o); } }
public void addInt (int o, int x, int d) { if(force_defaults || x != d) { addInt (x); slot(o); } }
public void addLong (int o, long x, long d) { if(force_defaults || x != d) { addLong (x); slot(o); } }
public void addFloat (int o, float x, double d) { if(force_defaults || x != d) { addFloat (x); slot(o); } }
public void addDouble (int o, double x, double d) { if(force_defaults || x != d) { addDouble (x); slot(o); } }
public void addOffset (int o, int x, int d) { if(force_defaults || x != d) { addOffset (x); slot(o); } }
// Structs are stored inline, so nothing additional is being added. `d` is always 0.
public void addStruct(int voffset, int x, int d) {
if(x != d) {
Nested(x);
slot(voffset);
}
}
// Set the current vtable at `voffset` to the current location in the buffer.
public void slot(int voffset) {
vtable[voffset] = offset();
}
/**
* Finish off writing the object that is under construction.
*
* @return The offset to the object inside {@link #dataBuffer()}
* @see #startObject(int)
*/
public int endObject() {
if (vtable == null || !nested)
throw new AssertionError("FlatBuffers: endObject called without startObject");
addInt(0);
int vtableloc = offset();
// Write out the current vtable.
for (int i = vtable_in_use - 1; i >= 0 ; i--) {
// Offset relative to the start of the table.
short off = (short)(vtable[i] != 0 ? vtableloc - vtable[i] : 0);
addShort(off);
}
final int standard_fields = 2; // The fields below:
addShort((short)(vtableloc - object_start));
addShort((short)((vtable_in_use + standard_fields) * SIZEOF_SHORT));
// Search for an existing vtable that matches the current one.
int existing_vtable = 0;
outer_loop:
for (int i = 0; i < num_vtables; i++) {
int vt1 = bb.capacity() - vtables[i];
int vt2 = space;
short len = bb.getShort(vt1);
if (len == bb.getShort(vt2)) {
for (int j = SIZEOF_SHORT; j < len; j += SIZEOF_SHORT) {
if (bb.getShort(vt1 + j) != bb.getShort(vt2 + j)) {
continue outer_loop;
}
}
existing_vtable = vtables[i];
break outer_loop;
}
}
if (existing_vtable != 0) {
// Found a match:
// Remove the current vtable.
space = bb.capacity() - vtableloc;
// Point table to existing vtable.
bb.putInt(space, existing_vtable - vtableloc);
} else {
// No match:
// Add the location of the current vtable to the list of vtables.
if (num_vtables == vtables.length) vtables = Arrays.copyOf(vtables, num_vtables * 2);
vtables[num_vtables++] = offset();
// Point table to current vtable.
bb.putInt(bb.capacity() - vtableloc, offset() - vtableloc);
}
nested = false;
return vtableloc;
}
// This checks a required field has been set in a given table that has
// just been constructed.
public void required(int table, int field) {
int table_start = bb.capacity() - table;
int vtable_start = table_start - bb.getInt(table_start);
boolean ok = bb.getShort(vtable_start + field) != 0;
// If this fails, the caller will show what field needs to be set.
if (!ok)
throw new AssertionError("FlatBuffers: field " + field + " must be set");
}
public void finish(int root_table) {
prep(minalign, SIZEOF_INT);
addOffset(root_table);
bb.position(space);
}
public void finish(int root_table, String file_identifier) {
prep(minalign, SIZEOF_INT + FILE_IDENTIFIER_LENGTH);
if (file_identifier.length() != FILE_IDENTIFIER_LENGTH)
throw new AssertionError("FlatBuffers: file identifier must be length " +
FILE_IDENTIFIER_LENGTH);
for (int i = FILE_IDENTIFIER_LENGTH - 1; i >= 0; i--) {
addByte((byte)file_identifier.charAt(i));
}
finish(root_table);
}
/**
* In order to save space, fields that are set to their default value
* don't get serialized into the buffer. Forcing defaults provides a
* way to manually disable this optimization.
*
* @param forceDefaults true always serializes default values
* @return this
*/
public FlatBufferBuilder forceDefaults(boolean forceDefaults){
this.force_defaults = forceDefaults;
return this;
}
// Get the ByteBuffer representing the FlatBuffer. Only call this after you've
// called finish(). The actual data starts at the ByteBuffer's current position,
// not necessarily at 0.
public ByteBuffer dataBuffer() { return bb; }
/**
* The FlatBuffer data doesn't start at offset 0 in the {@link ByteBuffer}, but
* now the {@code ByteBuffer}'s position is set to that location upon {@link #finish(int)}.
*
* @return The {@link ByteBuffer#position() position} the data starts in {@link #dataBuffer()}
* @deprecated This method should not be needed anymore, but is left
* here for the moment to document this API change. It will be removed in the future.
*/
@Deprecated
private int dataStart() {
return space;
}
/**
* Utility function for copying a byte array from {@code start} to
* {@code start} + {@code length}
*
* @param start Start copying at this offset
* @param length How many bytes to copy
* @return A range copy of the {@link #dataBuffer() data buffer}
* @throws IndexOutOfBoundsException If the range of bytes is ouf of bound
*/
public byte[] sizedByteArray(int start, int length){
byte[] array = new byte[length];
bb.position(start);
bb.get(array);
return array;
}
/**
* Utility function for copying a byte array that starts at 0.
*
* @return A full copy of the {@link #dataBuffer() data buffer}
*/
public byte[] sizedByteArray() {
return sizedByteArray(space, bb.capacity() - space);
}
}