blob: 6dceb279005d6f4d25cc9b0e0dd1113d1f4cf856 [file] [log] [blame]
#include "aos/flatbuffers/static_flatbuffers.h"
#include <stddef.h>
#include <stdint.h>
#include <algorithm>
#include <map>
#include <optional>
#include <ostream>
#include <utility>
#include <vector>
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/strings/numbers.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "absl/strings/str_replace.h"
#include "absl/strings/substitute.h"
#include "flatbuffers/base.h"
#include "flatbuffers/string.h"
#include "flatbuffers/vector.h"
#include "aos/flatbuffers/base.h"
#include "aos/json_to_flatbuffer.h"
namespace aos::fbs {
namespace {
// Represents a given field within a type with all of the data that we actually
// care about.
struct FieldData {
// Field name.
std::string name;
// Whether it is an inline data type (scalar/struct vs vector/table/string).
bool is_inline = true;
// Whether the elements are inline (vector of ints vs vector of strings).
bool elements_are_inline = true;
// Whether this is a struct or not.
bool is_struct = false;
// Whether this is a repeated type (vector or string).
bool is_repeated = false;
// Full C++ type of this field.
std::string full_type = "";
// Full flatbuffer type for this field.
// Only specified for Tables.
std::optional<std::string> fbs_type = std::nullopt;
// Size of this field in the inline field data (i.e., size of the field for
// is_inline fields; 4 bytes for the offset for vectors/tables/strings).
size_t inline_size = 0u;
// Alignment of the inline data.
size_t inline_alignment = 0u;
// vtable offset of the field.
size_t vtable_offset = 0u;
// Size of the elements in the vector, if this is a vector.
size_t element_size = 0u;
};
const reflection::Object *GetObject(const reflection::Schema *schema,
const int index) {
return (index == -1) ? schema->root_table() : schema->objects()->Get(index);
}
// Returns the flatbuffer field attribute with the specified name, if available.
std::optional<std::string_view> GetAttribute(const reflection::Field *field,
std::string_view attribute) {
if (!field->has_attributes()) {
return std::nullopt;
}
const reflection::KeyValue *kv =
field->attributes()->LookupByKey(attribute.data());
if (kv == nullptr) {
return std::nullopt;
}
return kv->value()->string_view();
}
// Returns the implied value of an attribute that specifies a length (i.e., 0 if
// the attribute is not specified; the integer value otherwise).
int64_t GetLengthAttributeOrZero(const reflection::Field *field,
std::string_view attribute) {
std::optional<std::string_view> str = GetAttribute(field, attribute);
if (!str.has_value()) {
return 0;
}
int64_t value;
CHECK(absl::SimpleAtoi(str.value(), &value))
<< ": Field " << field->name()->string_view()
<< " must specify a positive integer for the " << attribute
<< " attribute. Got \"" << str.value() << "\".";
CHECK_LE(0, value) << ": Field " << field->name()->string_view()
<< " must have a non-negative " << attribute << ".";
return value;
}
const std::string ScalarCppType(const reflection::BaseType type) {
switch (type) {
case reflection::BaseType::Bool:
return "bool";
case reflection::BaseType::Byte:
return "int8_t";
case reflection::BaseType::UByte:
return "uint8_t";
case reflection::BaseType::Short:
return "int16_t";
case reflection::BaseType::UShort:
return "uint16_t";
case reflection::BaseType::Int:
return "int32_t";
case reflection::BaseType::UInt:
return "uint32_t";
case reflection::BaseType::Long:
return "int64_t";
case reflection::BaseType::ULong:
return "uint64_t";
case reflection::BaseType::Float:
return "float";
case reflection::BaseType::Double:
return "double";
case reflection::BaseType::UType:
case reflection::BaseType::String:
case reflection::BaseType::Vector:
case reflection::BaseType::Obj:
case reflection::BaseType::None:
case reflection::BaseType::Union:
case reflection::BaseType::Array:
case reflection::BaseType::MaxBaseType:
LOG(FATAL) << ": Type " << reflection::EnumNameBaseType(type)
<< " not a scalar.";
}
LOG(FATAL) << "Unreachable";
}
const std::string FlatbufferNameToCppName(const std::string_view input) {
return absl::StrReplaceAll(input, {{".", "::"}});
}
const std::string AosNameForRawFlatbuffer(const std::string_view base_name) {
return absl::StrCat(base_name, "Static");
}
const std::string IncludePathForFbs(
std::string_view fbs_file, std::string_view include_suffix = "static") {
// Special case for the reflection_generated.h, which is checked into the
// repo.
// Note that we *do* autogenerated the reflection_static.h but that because
// it uses a special import path, we end up overriding the include anyways
// (note that we could muck around with the paths on the bazel side to instead
// get a cc_library with the correct include paths specified, although it is
// not clear that that would be any simpler than the extra else-if).
if (fbs_file == "reflection/reflection.fbs") {
if (include_suffix == "generated") {
return "flatbuffers/reflection_generated.h";
} else if (include_suffix == "static") {
return "aos/flatbuffers/reflection/reflection_static.h";
} else {
LOG(FATAL) << "This should be unreachable.";
}
}
fbs_file.remove_suffix(4);
return absl::StrCat(fbs_file, "_", include_suffix, ".h");
}
std::string ScalarOrEnumType(const reflection::Schema *schema,
const reflection::BaseType type, int index) {
return (index < 0) ? ScalarCppType(type)
: FlatbufferNameToCppName(
schema->enums()->Get(index)->name()->string_view());
}
void PopulateTypeData(const reflection::Schema *schema,
const reflection::Field *field_fbs, FieldData *field) {
VLOG(1) << aos::FlatbufferToJson(field_fbs);
const reflection::Type *type = field_fbs->type();
field->inline_size = type->base_size();
field->inline_alignment = type->base_size();
field->element_size = type->element_size();
switch (type->base_type()) {
case reflection::BaseType::Bool:
case reflection::BaseType::Byte:
case reflection::BaseType::UByte:
case reflection::BaseType::Short:
case reflection::BaseType::UShort:
case reflection::BaseType::Int:
case reflection::BaseType::UInt:
case reflection::BaseType::Long:
case reflection::BaseType::ULong:
case reflection::BaseType::Float:
case reflection::BaseType::Double:
// We have a scalar field, so things are relatively
// straightforwards.
field->is_inline = true;
field->elements_are_inline = true;
field->is_struct = false;
field->is_repeated = false;
field->full_type =
ScalarOrEnumType(schema, type->base_type(), type->index());
return;
case reflection::BaseType::String: {
field->is_inline = false;
field->elements_are_inline = true;
field->is_struct = false;
field->is_repeated = true;
field->full_type =
absl::StrFormat("::aos::fbs::String<%d>",
GetLengthAttributeOrZero(field_fbs, "static_length"));
return;
}
case reflection::BaseType::Vector: {
// We need to extract the name of the elements of the vector.
std::string element_type;
bool elements_are_inline = true;
field->is_repeated = true;
if (type->base_type() == reflection::BaseType::Vector) {
switch (type->element()) {
case reflection::BaseType::Obj: {
const reflection::Object *element_object =
GetObject(schema, type->index());
element_type =
FlatbufferNameToCppName(element_object->name()->string_view());
elements_are_inline = element_object->is_struct();
if (!element_object->is_struct()) {
element_type = AosNameForRawFlatbuffer(element_type);
field->fbs_type = element_object->name()->string_view();
}
break;
}
case reflection::BaseType::String:
element_type =
absl::StrFormat("::aos::fbs::String<%d>",
GetLengthAttributeOrZero(
field_fbs, "static_vector_string_length"));
elements_are_inline = false;
break;
case reflection::BaseType::Vector:
LOG(FATAL) << "Vectors of vectors do not exist in flatbuffers.";
default:
element_type =
ScalarOrEnumType(schema, type->element(), type->index());
};
}
field->is_inline = false;
field->elements_are_inline = elements_are_inline;
field->is_struct = false;
field->full_type =
absl::StrFormat("::aos::fbs::Vector<%s, %d, %s, %s>", element_type,
GetLengthAttributeOrZero(field_fbs, "static_length"),
elements_are_inline ? "true" : "false",
GetAttribute(field_fbs, "force_align").value_or("0"));
return;
}
case reflection::BaseType::Obj: {
const reflection::Object *object = GetObject(schema, type->index());
field->is_inline = object->is_struct();
field->elements_are_inline = field->is_inline;
field->is_struct = object->is_struct();
field->is_repeated = false;
const std::string flatbuffer_name =
FlatbufferNameToCppName(object->name()->string_view());
if (field->is_inline) {
field->full_type = flatbuffer_name;
field->inline_size = object->bytesize();
field->inline_alignment = object->minalign();
} else {
field->fbs_type = object->name()->string_view();
field->full_type = AosNameForRawFlatbuffer(flatbuffer_name);
}
return;
}
case reflection::BaseType::None:
case reflection::BaseType::UType:
case reflection::BaseType::Union:
case reflection::BaseType::Array:
case reflection::BaseType::MaxBaseType:
LOG(FATAL) << ": Type " << reflection::EnumNameBaseType(type->base_type())
<< " not supported currently.";
};
}
std::string MakeMoveConstructor(std::string_view type_name) {
return absl::StrFormat(R"code(
// We need to provide a MoveConstructor to allow this table to be
// used inside of vectors, but we do not want it readily available to
// users. See TableMover for more details.
%s(%s &&) = default;
friend struct ::aos::fbs::internal::TableMover<%s>;
)code",
type_name, type_name, type_name);
}
std::string MakeConstructor(std::string_view type_name) {
const std::string constructor_body =
R"code(
CHECK_EQ(buffer.size(), kSize);
CHECK_EQ(0u, reinterpret_cast<size_t>(buffer.data() + kAlignOffset) % kAlign);
PopulateVtable();
)code";
return absl::StrFormat(R"code(
// Constructors for creating a flatbuffer object.
// Users should typically use the Builder class to create these objects,
// in order to allow it to populate the root table offset.
// The buffer provided to these constructors should be aligned to kAlign
// and kSize in length.
// The parent/allocator may not be nullptr.
%s(std::span<uint8_t> buffer, ::aos::fbs::ResizeableObject *parent) : Table(buffer, parent) {
%s
}
%s(std::span<uint8_t> buffer, ::aos::fbs::Allocator *allocator) : Table(buffer, allocator) {
%s
}
%s(std::span<uint8_t> buffer, ::std::unique_ptr<::aos::fbs::Allocator> allocator) : Table(buffer, ::std::move(allocator)) {
%s
}
)code",
type_name, constructor_body, type_name,
constructor_body, type_name, constructor_body);
}
std::string MemberName(const FieldData &data) {
return absl::StrCat(data.name, "_");
}
std::string ObjectAbsoluteOffsetName(const FieldData &data) {
return absl::StrCat("object_absolute_offset_", data.name);
}
std::string InlineAbsoluteOffsetName(const FieldData &data) {
return absl::StrCat("kInlineAbsoluteOffset_", data.name);
}
// Generate the clear_* method for the requested field.
std::string MakeClearer(const FieldData &field) {
std::string logical_clearer;
if (!field.is_inline) {
logical_clearer = MemberName(field) + ".reset();";
}
return absl::StrFormat(R"code(
// Clears the %s field. This will cause has_%s() to return false.
void clear_%s() {
%s
ClearField(%s, %d, %d);
}
)code",
field.name, field.name, field.name, logical_clearer,
InlineAbsoluteOffsetName(field), field.inline_size,
field.vtable_offset);
}
// Generate the has_* method for the requested field.
std::string MakeHaser(const FieldData &field) {
return absl::StrFormat(R"code(
// Returns true if the %s field is set and can be accessed.
bool has_%s() const {
return AsFlatbuffer().has_%s();
}
)code",
field.name, field.name, field.name);
}
// Generates the accessors for fields which are stored inline in the flatbuffer
// table (scalars, structs, and enums) .
std::string MakeInlineAccessors(const FieldData &field,
const size_t inline_absolute_offset) {
constexpr size_t kVtablePointerSize = sizeof(soffset_t);
CHECK_EQ(
(inline_absolute_offset - kVtablePointerSize) % field.inline_alignment,
0u)
<< ": Unaligned field " << field.name << " on " << field.full_type
<< " with inline offset of " << inline_absolute_offset
<< " and alignment of " << field.inline_alignment
<< " and an alignment offset of " << kVtablePointerSize;
const std::string setter =
absl::StrFormat(R"code(
// Sets the %s field, causing it to be populated if it is not already.
// This will populate the field even if the specified value is the default.
void set_%s(const %s &value) {
SetField<%s>(%s, %d, value);
}
)code",
field.name, field.name, field.full_type, field.full_type,
InlineAbsoluteOffsetName(field), field.vtable_offset);
const std::string getters = absl::StrFormat(
R"code(
// Returns the value of %s if set; nullopt otherwise.
std::optional<%s> %s() const {
return has_%s() ? std::make_optional(Get<%s>(%s)) : std::nullopt;
}
// Returns a pointer to modify the %s field.
// The pointer may be invalidated by mutations/movements of the underlying buffer.
// Returns nullptr if the field is not set.
%s* mutable_%s() {
return has_%s() ? MutableGet<%s>(%s) : nullptr;
}
)code",
field.name, field.full_type, field.name, field.name, field.full_type,
InlineAbsoluteOffsetName(field), field.name, field.full_type, field.name,
field.name, field.full_type, InlineAbsoluteOffsetName(field));
const std::string clearer = MakeClearer(field);
return setter + getters + clearer + MakeHaser(field);
}
// Generates the accessors for fields which are not inline fields and have an
// offset to the actual field content stored inline in the flatbuffer table.
std::string MakeOffsetDataAccessors(const FieldData &field) {
const std::string setter = absl::Substitute(
R"code(
// Creates an empty object for the $0 field, which you can
// then populate/modify as desired.
// The field must not be populated yet.
$1* add_$0() {
CHECK(!$2.has_value());
constexpr size_t kVtableIndex = $3;
// If this object does not normally have its initial memory statically allocated,
// allocate it now (this is used for zero-length vectors).
if constexpr ($1::kPreallocatedSize == 0) {
const size_t object_absolute_offset = $4;
std::optional<std::span<uint8_t>> inserted_bytes =
InsertBytes(buffer().data() + object_absolute_offset, $1::kSize, ::aos::fbs::SetZero::kYes);
if (!inserted_bytes.has_value()) {
return nullptr;
}
// Undo changes to the object absolute offset that will have been made by
// the InsertBytes call.
// The InsertBytes() call normally goes through and "fixes" any offsets
// that will have been affected by the memory insertion. Unfortunately,
// if this object currently takes up zero bytes then the InsertBytes()
// cannot distinguish between this offset and the (identical) offsets for any other objects
// that may have been "sharing" this location. The effect of this logic
// is that the first object that gets populated at any given location will
// bump all other objects to later. This is fine, although it does mean
// that the order in which objects appear in memory may vary depending
// on the order in which they are constructed (if they start out sharing a start pointer).
$4 = object_absolute_offset;
// Construct the *Static object that we will use for managing this subtable.
$5.emplace(BufferForObject($4, $1::$7), this);
} else {
// Construct the *Static object that we will use for managing this subtable.
$5.emplace(BufferForObject($4, $1::kSize), this);
}
// Actually set the appropriate fields in the flatbuffer memory itself.
SetField<::flatbuffers::uoffset_t>($6, kVtableIndex, $4 - $6);
return &$5.value().t;
}
)code",
field.name, // $0
field.full_type, // $1
MemberName(field), // $2
field.vtable_offset, // $3
ObjectAbsoluteOffsetName(field), // $4
MemberName(field), // $5
InlineAbsoluteOffsetName(field), // $6
(field.elements_are_inline
// When building vectors of inline elements, we want this object to
// consume as much of the memory that was allocated as possible. This
// lets the vector use the padding for storage, saving space. Round
// this down to the size of memory that the max number of elements
// fit in perfectly so the padding after that isn't owned.
? "RoundedLength(inserted_bytes.value().size())"
// For vectors of elements, we need to pad out the inline portion of
// the vector storing offsets to the alignment of the actual elements
// so we can insert elements at the end without having to allocate
// padding. This saves space in the long run, and lets us consume
// the padding for offsets if needed.
: "kSize") // $7
);
const std::string getters = absl::Substitute(
R"code(
// Returns a pointer to the $0 field, if set. nullptr otherwise.
const $1* $0() const {
return $2.has_value() ? &$2.value().t : nullptr;
}
$1* mutable_$0() {
return $2.has_value() ? &$2.value().t : nullptr;
}
)code",
field.name, // $0
field.full_type, // $1
MemberName(field) // $2
);
return setter + getters + MakeClearer(field) + MakeHaser(field);
}
std::string MakeAccessors(const FieldData &field,
size_t inline_absolute_offset) {
return field.is_inline ? MakeInlineAccessors(field, inline_absolute_offset)
: MakeOffsetDataAccessors(field);
}
std::string MakeMembers(const FieldData &field,
std::string_view offset_data_absolute_offset,
size_t inline_absolute_offset) {
if (field.is_inline) {
return absl::StrFormat(
R"code(
// Offset from the start of the buffer to the inline data for the %s field.
static constexpr size_t %s = %d;
)code",
field.name, InlineAbsoluteOffsetName(field), inline_absolute_offset);
} else {
return absl::StrFormat(
R"code(
// Members relating to the %s field.
//
// *Static object used for managing this subtable. Will be nullopt
// when the field is not populated.
// We use the TableMover to be able to make this object moveable.
std::optional<::aos::fbs::internal::TableMover<%s>> %s;
// Offset from the start of the buffer to the start of the actual
// data for this field. Will be updated even when the table is not
// populated, so that we know where to construct it when requested.
static constexpr size_t kDefaultObjectAbsoluteOffset%s = %s;
size_t %s = kDefaultObjectAbsoluteOffset%s;
// Offset from the start of the buffer to the offset in the inline data for
// this field.
static constexpr size_t %s = %d;
)code",
field.name, field.full_type, MemberName(field), field.name,
offset_data_absolute_offset, ObjectAbsoluteOffsetName(field),
field.name, InlineAbsoluteOffsetName(field), inline_absolute_offset);
}
}
std::string MakeFullClearer(const std::vector<FieldData> &fields) {
std::vector<std::string> clearers;
for (const FieldData &field : fields) {
clearers.emplace_back(absl::StrFormat("clear_%s();", field.name));
}
return absl::StrFormat(R"code(
// Clears every field of the table, removing any existing state.
void Clear() { %s })code",
absl::StrJoin(clearers, "\n"));
}
// Creates the FromFlatbuffer() method that copies from a flatbuffer object API
// object (i.e., the FlatbufferT types).
std::string MakeObjectCopier(const std::vector<FieldData> &fields) {
std::vector<std::string> copiers;
for (const FieldData &field : fields) {
if (field.is_struct) {
// Structs are stored as unique_ptr<FooStruct>
copiers.emplace_back(absl::StrFormat(R"code(
if (other.%s) {
set_%s(*other.%s);
}
)code",
field.name, field.name, field.name));
} else if (field.is_inline) {
// Inline non-struct elements are stored as FooType.
copiers.emplace_back(absl::StrFormat(R"code(
set_%s(other.%s);
)code",
field.name, field.name));
} else if (field.is_repeated) {
// strings are stored as std::string's.
// vectors are stored as std::vector's.
copiers.emplace_back(absl::StrFormat(R"code(
// Unconditionally copy strings/vectors, even if it will just end up
// being 0-length (this maintains consistency with the flatbuffer Pack()
// behavior).
{
%s* added_%s = add_%s();
CHECK(added_%s != nullptr);
if (!added_%s->FromFlatbuffer(other.%s)) {
// Fail if we were unable to copy (e.g., if we tried to copy in a long
// vector and do not have the space for it).
return false;
}
}
)code",
field.full_type, field.name,
field.name, field.name, field.name,
field.name));
} else {
// Tables are stored as unique_ptr<FooTable>
copiers.emplace_back(absl::StrFormat(R"code(
if (other.%s) {
%s* added_%s = add_%s();
CHECK(added_%s != nullptr);
if (!added_%s->FromFlatbuffer(*other.%s)) {
// Fail if we were unable to copy (e.g., if we tried to copy in a long
// vector and do not have the space for it).
return false;
}
}
)code",
field.name, field.full_type,
field.name, field.name, field.name,
field.name, field.name));
}
}
return absl::StrFormat(
R"code(
// Copies the contents of the provided flatbuffer into this flatbuffer,
// returning true on success.
// Because the Flatbuffer Object API does not provide any concept of an
// optionally populated scalar field, all scalar fields will be populated
// after a call to FromFlatbufferObject().
// This is a deep copy, and will call FromFlatbufferObject on
// any constituent objects.
[[nodiscard]] bool FromFlatbuffer([[maybe_unused]] const Flatbuffer::NativeTableType &other) {
Clear();
%s
return true;
}
[[nodiscard]] bool FromFlatbuffer(const flatbuffers::unique_ptr<Flatbuffer::NativeTableType>& other) {
return FromFlatbuffer(*other);
}
)code",
absl::StrJoin(copiers, "\n"));
}
// Creates the FromFlatbuffer() method that copies from an actual flatbuffer
// object.
std::string MakeCopier(const std::vector<FieldData> &fields) {
std::vector<std::string> copiers;
for (const FieldData &field : fields) {
if (field.is_struct) {
copiers.emplace_back(absl::StrFormat(R"code(
if (other.has_%s()) {
set_%s(*other.%s());
}
)code",
field.name, field.name, field.name));
} else if (field.is_inline) {
copiers.emplace_back(absl::StrFormat(R"code(
if (other.has_%s()) {
set_%s(other.%s());
}
)code",
field.name, field.name, field.name));
} else {
copiers.emplace_back(absl::StrFormat(R"code(
if (other.has_%s()) {
%s* added_%s = add_%s();
CHECK(added_%s != nullptr);
if (!added_%s->FromFlatbuffer(other.%s())) {
// Fail if we were unable to copy (e.g., if we tried to copy in a long
// vector and do not have the space for it).
return false;
}
}
)code",
field.name, field.full_type,
field.name, field.name, field.name,
field.name, field.name));
}
}
return absl::StrFormat(
R"code(
// Copies the contents of the provided flatbuffer into this flatbuffer,
// returning true on success.
// This is a deep copy, and will call FromFlatbuffer on any constituent
// objects.
[[nodiscard]] bool FromFlatbuffer([[maybe_unused]] const Flatbuffer &other) {
Clear();
%s
return true;
}
// Equivalent to FromFlatbuffer(const Flatbuffer&); this overload is provided
// to ease implementation of the aos::fbs::Vector internals.
[[nodiscard]] bool FromFlatbuffer(const Flatbuffer *other) {
CHECK(other != nullptr);
return FromFlatbuffer(*other);
}
)code",
absl::StrJoin(copiers, "\n"));
}
std::string MakeSubObjectList(const std::vector<FieldData> &fields) {
size_t num_object_fields = 0;
std::vector<std::string> object_offsets;
std::vector<std::string> objects;
std::vector<std::string> inline_offsets;
for (const FieldData &field : fields) {
if (!field.is_inline) {
++num_object_fields;
object_offsets.push_back(
absl::StrFormat("&%s", ObjectAbsoluteOffsetName(field)));
objects.push_back(absl::StrFormat("&%s->t", MemberName(field)));
inline_offsets.push_back(InlineAbsoluteOffsetName(field));
}
}
if (num_object_fields == 0) {
return R"code(
// This object has no non-inline subobjects, so we don't have to do anything special.
size_t NumberOfSubObjects() const final { return 0; }
using ::aos::fbs::ResizeableObject::SubObject;
SubObject GetSubObject(size_t) final { LOG(FATAL) << "No subobjects."; }
)code";
}
return absl::StrFormat(R"code(
size_t NumberOfSubObjects() const final { return %d; }
using ::aos::fbs::ResizeableObject::SubObject;
SubObject GetSubObject(size_t index) final {
SubObject object;
// Note: The below arrays are local variables rather than class members to
// avoid having to deal with what happens to them if the object is moved.
// Array of the members that we use for tracking where the buffers for
// each subobject belong.
// Pointers because these may need to be modified when memory is
// inserted into the buffer.
const std::array<size_t*, %d> subobject_object_offsets{%s};
// Actual subobjects; note that the pointers will be invalid when the
// field is not populated.
const std::array<::aos::fbs::ResizeableObject*, %d> subobject_objects{%s};
// Absolute offsets from the start of the buffer to where the inline
// entry is for each table. These offsets do not need to change at
// runtime (because memory is never inserted into the start of
// a given table), but the offsets pointed to by these offsets
// may need to be updated.
const std::array<size_t, %d> subobject_inline_offsets{%s};
object.inline_entry = MutableGet<::flatbuffers::uoffset_t>(subobject_inline_offsets[index]);
object.object = (*object.inline_entry == 0) ? nullptr : subobject_objects[index];
object.absolute_offset = subobject_object_offsets[index];
return object;
}
)code",
num_object_fields, num_object_fields,
absl::StrJoin(object_offsets, ", "), num_object_fields,
absl::StrJoin(objects, ", "), num_object_fields,
absl::StrJoin(inline_offsets, ", "));
}
std::string AlignCppString(const std::string_view expression,
const std::string_view alignment,
const std::string_view offset) {
return absl::StrCat("::aos::fbs::AlignOffset(", expression, ", ", alignment,
", ", offset, ")");
}
std::string MakeInclude(std::string_view path, bool system = false) {
return absl::StrFormat("#include %s%s%s\n", system ? "<" : "\"", path,
system ? ">" : "\"");
}
} // namespace
GeneratedObject GenerateCodeForObject(const reflection::Schema *schema,
int object_index) {
return GenerateCodeForObject(schema, GetObject(schema, object_index));
}
GeneratedObject GenerateCodeForObject(const reflection::Schema *schema,
const reflection::Object *object) {
std::vector<FieldData> fields;
for (const reflection::Field *field_fbs : *object->fields()) {
if (field_fbs->deprecated()) {
// Don't codegen anything for deprecated fields.
continue;
}
FieldData field{.name = field_fbs->name()->str(),
.vtable_offset = field_fbs->offset()};
PopulateTypeData(schema, field_fbs, &field);
fields.push_back(field);
}
std::sort(fields.begin(), fields.end(),
[](const FieldData &f1, const FieldData &f2) {
return std::make_tuple(f1.inline_alignment, f1.element_size,
f1.vtable_offset) >
std::make_tuple(f2.inline_alignment, f2.element_size,
f2.vtable_offset);
});
const size_t nominal_min_align = object->minalign();
std::string out_of_line_member_size = "";
// inline_absolute_offset tracks the current position of the inline table
// contents so that we can assign static offsets to each field.
constexpr size_t kVtablePointerSize = sizeof(soffset_t);
size_t inline_absolute_offset = kVtablePointerSize;
// offset_data_relative_offset tracks the current size of the various
// sub-tables/vectors/strings that get stored at the end of the buffer.
// For simplicity, the offset data will start at a fully aligned offset
// (which may be larger than the soffset_t at the start of the table).
// Note that this is a string because it's irritating to actually pipe the
// numbers for size/alignment up here, so we just accumulate them here and
// then write the expression directly into the C++.
const std::string offset_data_start_expression =
"(kVtableStart + kVtableSize)";
std::string offset_data_relative_offset = offset_data_start_expression;
std::vector<std::string> accessors;
std::vector<std::string> members;
std::set<std::string> includes = {
MakeInclude("optional", true),
MakeInclude("aos/flatbuffers/static_table.h"),
MakeInclude("aos/flatbuffers/static_vector.h")};
for (const reflection::SchemaFile *file : *schema->fbs_files()) {
includes.insert(
MakeInclude(IncludePathForFbs(file->filename()->string_view())));
includes.insert(MakeInclude(
IncludePathForFbs(file->filename()->string_view(), "generated")));
for (const flatbuffers::String *included : *file->included_filenames()) {
includes.insert(MakeInclude(IncludePathForFbs(included->string_view())));
}
}
std::vector<std::string> alignments;
std::set<std::string> subobject_names;
for (const FieldData &field : fields) {
inline_absolute_offset = AlignOffset(
inline_absolute_offset, field.inline_alignment, kVtablePointerSize);
if (!field.is_inline) {
alignments.push_back(field.full_type + "::kAlign");
// We care about aligning each field relative to the alignment point in
// this flatbuffer (which is kAlignOffset into the block of memory). We
// then need to report out the offset relative to the start, not the
// alignment point.
offset_data_relative_offset =
AlignCppString(offset_data_relative_offset + " - kAlignOffset",
alignments.back(),
field.full_type + "::kAlignOffset") +
" + kAlignOffset";
} else {
alignments.push_back(std::to_string(field.inline_alignment));
}
const std::string offset_data_absolute_offset = offset_data_relative_offset;
accessors.emplace_back(MakeAccessors(field, inline_absolute_offset));
members.emplace_back(MakeMembers(field, offset_data_absolute_offset,
inline_absolute_offset));
inline_absolute_offset += field.inline_size;
if (!field.is_inline) {
offset_data_relative_offset = absl::StrFormat(
"kDefaultObjectAbsoluteOffset%s + %s::kPreallocatedSize", field.name,
field.full_type);
}
if (field.fbs_type.has_value()) {
// Is this not getting populate for the root schema?
subobject_names.insert(field.fbs_type.value());
}
}
const std::string alignment = absl::StrCat(
"static constexpr size_t kAlign = std::max<size_t>({kMinAlign, ",
absl::StrJoin(alignments, ", "), "});\n");
// Same here, we want to align the end relative to the alignment point, but
// then we want to report out the size including the offset.
const std::string size = absl::StrCat(
"static constexpr size_t kSize = ",
AlignCppString(offset_data_relative_offset + " - kAlignOffset", "kAlign",
"kAlignOffset"),
" + kAlignOffset;");
const size_t inline_data_size = inline_absolute_offset;
const std::string constants = absl::StrFormat(
R"code(
// Space taken up by the inline portion of the flatbuffer table data, in
// bytes.
static constexpr size_t kInlineDataSize = %d;
// Space taken up by the vtable for this object, in bytes.
static constexpr size_t kVtableSize =
sizeof(::flatbuffers::voffset_t) * (2 + %d);
// Offset from the start of the internal memory buffer to the start of the
// vtable.
static constexpr size_t kVtableStart = ::aos::fbs::AlignOffset(
kInlineDataSize, alignof(::flatbuffers::voffset_t));
// Required alignment of this object. The buffer that this object gets
// constructed into must be aligned to this value.
%s
// Offset into this object to measure the alignment at.
static constexpr size_t kAlignOffset = sizeof(::flatbuffers::soffset_t);
static_assert(
%d <= kAlign,
"Flatbuffer schema minalign should not exceed our required alignment.");
// Offset from the start of the memory buffer to the start of any out-of-line
// data (subtables, vectors, strings).
static constexpr size_t kOffsetDataStart = %s;
// Various overrides to support the Table parent class.
size_t FixedVtableOffset() const final { return kVtableStart; }
size_t VtableSize() const final { return kVtableSize; }
size_t InlineTableSize() const final { return kInlineDataSize; }
size_t OffsetDataStart() const final { return kOffsetDataStart; }
size_t Alignment() const final { return kAlign; }
// Exposes the name of the flatbuffer type to allow interchangeable use
// of the Flatbuffer and FlatbufferStatic types in various AOS methods.
static const char *GetFullyQualifiedName() {
return Flatbuffer::GetFullyQualifiedName();
}
)code",
inline_data_size, object->fields()->size(), alignment, nominal_min_align,
offset_data_start_expression);
const std::string_view fbs_type_name = object->name()->string_view();
const std::string type_namespace = FlatbufferNameToCppName(
fbs_type_name.substr(0, fbs_type_name.find_last_of(".")));
const std::string type_name = AosNameForRawFlatbuffer(
fbs_type_name.substr(fbs_type_name.find_last_of(".") + 1));
const std::string object_code = absl::StrFormat(
R"code(
namespace %s {
class %s : public ::aos::fbs::Table {
public:
// The underlying "raw" flatbuffer type for this type.
typedef %s Flatbuffer;
typedef flatbuffers::unique_ptr<Flatbuffer::NativeTableType> FlatbufferObjectType;
// Returns this object as a flatbuffer type. This reference may not be valid
// following mutations to the underlying flatbuffer, due to how memory may get
// may get moved around.
const Flatbuffer &AsFlatbuffer() const { return *GetFlatbuffer<Flatbuffer>(); }
%s
%s
virtual ~%s() {}
%s
%s
%s
%s
private:
%s
%s
%s
public:
// Nominal size of this object, in bytes. The object may grow beyond this
// size, but will always start at this size and so the initial buffer must
// match this size.
%s
// Always statically allocate memory for tables (set for consistency with
// static_vector.h).
static constexpr size_t kPreallocatedSize = kSize;
// Size required for a buffer that includes a root table offset at the start.
static constexpr size_t kRootSize =
::aos::fbs::AlignOffset(kSize + sizeof(::flatbuffers::uoffset_t), kAlign);
};
}
)code",
type_namespace, type_name, FlatbufferNameToCppName(fbs_type_name),
constants, MakeConstructor(type_name), type_name,
absl::StrJoin(accessors, ""), MakeFullClearer(fields), MakeCopier(fields),
MakeObjectCopier(fields), MakeMoveConstructor(type_name),
absl::StrJoin(members, ""), MakeSubObjectList(fields), size);
GeneratedObject result;
result.name = fbs_type_name;
result.include_declarations = includes;
result.code = object_code;
result.subobjects = subobject_names;
return result;
}
namespace {
// Generated C++ code for an entire fbs file.
// This includes all of the actual C++ code that will be written to a file (call
// GenerateCode() to actually get the desired contents of the file).
struct GeneratedCode {
// Prefix (for include guards).
std::string contents_prefix;
// Full set of required #include declarations.
std::set<std::string> include_declarations;
// Ordered list of objects (order is necessary to ensure that any dependencies
// between objects are managed correctly).
std::vector<GeneratedObject> objects;
// Suffix (for include guards).
std::string contents_suffix;
// Combine the above things into the string that actually needs to be written
// to a file.
std::string GenerateCode() const;
// Combines the code for multiple objects into one.
static GeneratedCode MergeCode(const std::vector<GeneratedObject> &objects);
};
std::string GeneratedCode::GenerateCode() const {
std::string result =
contents_prefix + absl::StrJoin(include_declarations, "");
for (const auto &object : objects) {
result += object.code;
}
result += contents_suffix;
return result;
}
GeneratedCode GeneratedCode::MergeCode(
const std::vector<GeneratedObject> &objects) {
GeneratedCode result;
// TODO(james): Should we use #ifdef include guards instead?
result.contents_prefix =
"#pragma once\n// This is a generated file. Do not modify.\n";
// We need to get the ordering of objects correct in order to ensure that
// depended-on objects appear before their dependees.
// In order to do this, we:
// 1) Assume that any objects not in the provided vector must exist in
// #includes and so can be ignored.
// 2) Create a list of all the objects we have been provided but which we have
// not yet added to the results vector.
// 3) Until said list is empty, we iterate over it and find any object(s)
// which have no dependencies in the list itself, and add them to the
// result.
// We aren't going to worry about efficient graph traversal here or anything.
// We also don't currently attempt to support circular dependencies.
std::map<std::string_view, const GeneratedObject *> remaining_objects;
for (const auto &object : objects) {
remaining_objects[object.name] = &object;
}
while (!remaining_objects.empty()) {
std::string_view to_remove;
for (const auto &pair : remaining_objects) {
bool has_dependencies = false;
for (const std::string_view subobject : pair.second->subobjects) {
if (remaining_objects.contains(subobject)) {
has_dependencies = true;
}
}
if (has_dependencies) {
continue;
}
to_remove = pair.first;
result.objects.push_back(*pair.second);
result.include_declarations.insert(
pair.second->include_declarations.begin(),
pair.second->include_declarations.end());
break;
}
// In order to support circular dependencies, two main things have to
// change:
// 1. We have to dynamically allow depopulating table members (rather than
// just supporting dynamically lengthed vectors).
// 2. Some of the codegen needs to be tweaked so that we can have the
// generated
// C++ classes depend on one another.
CHECK(!to_remove.empty())
<< ": Circular dependencies in flatbuffers schemas are not supported.";
CHECK_EQ(1u, remaining_objects.erase(to_remove))
<< ": Failed to remove " << to_remove;
}
return result;
}
} // namespace
std::string GenerateCodeForRootTableFile(const reflection::Schema *schema,
std::string_view file_hint) {
const reflection::Object *root_object = GetObject(schema, -1);
const std::string_view root_file =
(root_object == nullptr) ? file_hint
: root_object->declaration_file()->string_view();
std::vector<GeneratedObject> objects;
if (root_object != nullptr) {
objects.push_back(GenerateCodeForObject(schema, root_object));
}
for (const reflection::Object *object : *schema->objects()) {
if (object->is_struct()) {
continue;
}
if (object->declaration_file()->string_view() == root_file) {
objects.push_back(GenerateCodeForObject(schema, object));
}
}
return GeneratedCode::MergeCode(objects).GenerateCode();
}
} // namespace aos::fbs