Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

- Structs now follow FlatBuffers alignment rules, including internal and trailing padding,
nested structs, and aligned struct vectors. Buffers are verified bidirectionally against
`flatc`.

## 0.5.2 — 2026-08-28

- **Schema includes no longer require a `root_type` in every file.** Previously each included `.fbs` file had to declare its own `root_type` or the whole load failed with `:root_type_is_undefined`, which made real-world, flatc-style type-only include files unusable. The root type is now resolved once, after all includes are merged, which also means the top-level `root_type` can reference a type defined in an included file:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ Flatbuffer. CI runs them on the newest supported Elixir/OTP combination.
* shared strings (`shared` field attribute)
* shared vtables
* structs
* alignment
* unions
* enums
* defaults
Expand All @@ -181,5 +182,4 @@ Flatbuffer. CI runs them on the newest supported Elixir/OTP combination.

### features only in flatc

* alignment
* additional attributes other than `shared`
13 changes: 4 additions & 9 deletions lib/flatbuffer/reading.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,11 @@ defmodule Flatbuffer.Reading do
end

def read({:struct, %{name: struct_name}}, c, schema) do
{:struct, %{members: members}} = Map.get(schema.entities, struct_name)
{:struct, %{layout: layout}} = Map.fetch!(schema.entities, struct_name)

{struct, _offset} =
members
|> Enum.reduce({%{}, 0}, fn {name, type}, {acc, offset} ->
value = read({type, %{}}, Cursor.skip(c, offset), schema)
{Map.put(acc, name, value), offset + Utils.scalar_size(type)}
end)

struct
Enum.reduce(layout, %{}, fn {name, type, offset}, struct ->
Map.put(struct, name, read(type, Cursor.skip(c, offset), schema))
end)
end

def read({:table, %{name: table_name}}, c, schema) do
Expand Down
87 changes: 83 additions & 4 deletions lib/flatbuffer/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,14 @@ defmodule Flatbuffer.Schema do
field_ids: %{String.t() => non_neg_integer()}
}}

@type struct_def :: {:struct, %{members: [{field_name(), type_def()}]}}
@type struct_def ::
{:struct,
%{
members: [{field_name(), type_def()}],
layout: [{field_name(), type_ref(), non_neg_integer()}],
alignment: pos_integer(),
size: non_neg_integer()
}}

@type union_def ::
{:union,
Expand All @@ -98,6 +105,8 @@ defmodule Flatbuffer.Schema do
| {:error, {:root_type_not_found, type_name :: String.t()}}
| {:error, {:root_type_is_not_a_table, type_name :: String.t()}}
| {:error, {:type_not_found, type_name :: String.t()}}
| {:error, {:recursive_struct, [type_name()]}}
| {:error, {:invalid_struct_member, type_name(), type_ref()}}

@doc """
Reads and parses a FlatBuffer schema from a file.
Expand Down Expand Up @@ -321,15 +330,85 @@ defmodule Flatbuffer.Schema do
Map.put(acc, key, {:union, %{members: members}})

{key, {:struct, fields}}, acc ->
members = Enum.map(fields, fn {name, type} -> {output_name(name, safe), type} end)
Map.put(acc, key, {:struct, %{members: members}})
Map.put(acc, key, {:struct, struct_options(key, fields, entities, safe)})
end
)}
catch
{:error, {:type_not_found, _type_name}} = error ->
{:error, _reason} = error ->
error
end

defp struct_options(name, fields, entities, safe) do
{members, layout, offset, alignment} =
Enum.reduce(fields, {[], [], 0, 1}, fn {field_name, field_type},
{members, layout, offset, struct_alignment} ->
resolved_type = resolve_field_type(field_type, entities)
{size, alignment} = inline_type_measure(resolved_type, entities, [name])
offset = Flatbuffer.Utils.align(offset, alignment)
output_name = output_name(field_name, safe)

{
[{output_name, field_type} | members],
[{output_name, resolved_type, offset} | layout],
offset + size,
max(struct_alignment, alignment)
}
end)

%{
members: Enum.reverse(members),
layout: Enum.reverse(layout),
alignment: alignment,
size: Flatbuffer.Utils.align(offset, alignment)
}
end

defp inline_type_measure({:struct, %{name: name}}, entities, parents) do
if name in parents do
throw({:error, {:recursive_struct, Enum.reverse([name | parents])}})
end

case Map.fetch!(entities, name) do
{:struct, fields} ->
{offset, alignment} =
Enum.reduce(fields, {0, 1}, fn {_field_name, field_type}, {offset, struct_alignment} ->
resolved_type = resolve_field_type(field_type, entities)
{size, alignment} = inline_type_measure(resolved_type, entities, [name | parents])
offset = Flatbuffer.Utils.align(offset, alignment)
{offset + size, max(struct_alignment, alignment)}
end)

{Flatbuffer.Utils.align(offset, alignment), alignment}
end
end

defp inline_type_measure({:enum, %{name: name}}, entities, _parents) do
{{:enum, type}, _members} = Map.fetch!(entities, name)
size = Flatbuffer.Utils.scalar_size(type)
{size, size}
end

defp inline_type_measure({type, _options}, _entities, _parents)
when type in [
:bool,
:byte,
:ubyte,
:short,
:ushort,
:int,
:uint,
:float,
:long,
:ulong,
:double
] do
size = Flatbuffer.Utils.scalar_size(type)
{size, size}
end

defp inline_type_measure(type, _entities, parents),
do: throw({:error, {:invalid_struct_member, List.first(parents), type}})

defp enumerate_members(members, safe) do
{members, _next_value} =
Enum.reduce(members, {%{}, 0}, fn member, {acc, next_value} ->
Expand Down
20 changes: 18 additions & 2 deletions lib/flatbuffer/utils.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule Flatbuffer.Utils do
@moduledoc false

def align(offset, alignment), do: offset + Integer.mod(-offset, alignment)

def scalar_size({type, _options}), do: scalar_size(type)
def scalar_size(:byte), do: 1
def scalar_size(:ubyte), do: 1
Expand All @@ -21,8 +23,8 @@ defmodule Flatbuffer.Utils do
end

def sizeof({:struct, %{name: struct_name}}, schema) do
{:struct, %{members: members}} = Map.get(schema.entities, struct_name)
Enum.reduce(members, 0, fn {_, type}, acc -> acc + sizeof(type, schema) end)
{:struct, %{size: size}} = Map.fetch!(schema.entities, struct_name)
size
end

def sizeof({:table, _}, _), do: 4
Expand All @@ -31,4 +33,18 @@ defmodule Flatbuffer.Utils do
def sizeof({:string, _}, _), do: 4

def sizeof(type, _), do: scalar_size(type)

def alignment({:enum, %{name: enum_name}}, schema) do
{:enum, %{type: type}} = Map.fetch!(schema.entities, enum_name)
alignment(type, schema)
end

def alignment({:struct, %{name: struct_name}}, schema) do
{:struct, %{alignment: alignment}} = Map.fetch!(schema.entities, struct_name)
alignment
end

def alignment({type, _}, _schema) when type in [:string, :vector, :table], do: 4
def alignment({type, _}, _schema), do: scalar_size(type)
def alignment(type, _schema), do: scalar_size(type)
end
38 changes: 26 additions & 12 deletions lib/flatbuffer/writer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -269,11 +269,30 @@ defmodule Flatbuffer.Writer do
end

defp inline({:struct, %{name: struct_name}}, map, path, schema) when is_map(map) do
{:struct, %{members: members}} = Map.fetch!(schema.entities, struct_name)
{:struct, %{layout: layout, size: struct_size}} =
Map.fetch!(schema.entities, struct_name)

{chunks, end_offset} =
Enum.reduce(layout, {[], 0}, fn {name, type, offset}, {chunks, end_offset} ->
padding = zero_padding(offset - end_offset)
data = inline(type, get_field(map, name), [name | path], schema)
size = Flatbuffer.Utils.sizeof(type, schema)

chunks =
case padding do
<<>> -> [data | chunks]
_ -> [data, padding | chunks]
end

{chunks, offset + size}
end)

Enum.map(members, fn {name, type} ->
inline({type, %{}}, get_field(map, name), [name | path], schema)
end)
tail_padding = zero_padding(struct_size - end_offset)

case tail_padding do
<<>> -> Enum.reverse(chunks)
_ -> Enum.reverse([tail_padding | chunks])
end
end

defp inline({:bool, _}, true, _path, _schema), do: <<1>>
Expand Down Expand Up @@ -327,15 +346,10 @@ defmodule Flatbuffer.Writer do

defp without_default(type), do: type

defp alignment({:enum, %{name: enum_name}}, schema) do
{:enum, %{type: type}} = Map.fetch!(schema.entities, enum_name)
alignment(type, schema)
end
defp alignment(type, schema), do: Flatbuffer.Utils.alignment(type, schema)

defp alignment({:struct, _}, _schema), do: 1
defp alignment({type, _}, _schema) when type in [:string, :vector, :table], do: 4
defp alignment({type, _}, _schema), do: Flatbuffer.Utils.scalar_size(type)
defp alignment(type, _schema), do: Flatbuffer.Utils.scalar_size(type)
defp zero_padding(0), do: <<>>
defp zero_padding(size), do: :binary.copy(<<0>>, size)

defp push_raw(%State{} = state, data) when is_binary(data),
do: %{state | chunks: [data | state.chunks], size: state.size + byte_size(data)}
Expand Down
15 changes: 15 additions & 0 deletions test/fixtures/flatc/interop.fbs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ struct Position {
z: float;
}

struct ScalarMix {
prefix: ubyte;
wide: ulong;
suffix: ushort;
}

struct NestedMix {
flag: ubyte;
value: ScalarMix;
count: uint;
}

table Child {
id: int;
label: string;
Expand All @@ -26,6 +38,9 @@ table Root {
name: string;
values: [int];
child: Child;
scalar_mix: ScalarMix;
scalar_mixes: [ScalarMix];
nested_mix: NestedMix;
}

root_type Root;
14 changes: 14 additions & 0 deletions test/fixtures/flatc/interop.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,19 @@
child: {
id: 7,
label: "nested"
},
scalar_mix: {
prefix: 1,
wide: 72623859790382856,
suffix: 515
},
scalar_mixes: [
{ prefix: 2, wide: 17, suffix: 18 },
{ prefix: 3, wide: 19, suffix: 20 }
],
nested_mix: {
flag: 4,
value: { prefix: 5, wide: 21, suffix: 22 },
count: 4000000001
}
}
18 changes: 18 additions & 0 deletions test/fixtures/flatc/read_interop.cc
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,23 @@ int main(int argc, char **argv) {
if (child == nullptr || child->id() != 7) return 16;
if (child->label() == nullptr || child->label()->str() != "nested") return 17;

const ScalarMix *scalar_mix = root->scalar_mix();
if (scalar_mix == nullptr || scalar_mix->prefix() != 1) return 18;
if (scalar_mix->wide() != UINT64_C(72623859790382856)) return 19;
if (scalar_mix->suffix() != 515) return 20;

const auto *scalar_mixes = root->scalar_mixes();
if (scalar_mixes == nullptr || scalar_mixes->size() != 2) return 21;
if (scalar_mixes->Get(0)->prefix() != 2 || scalar_mixes->Get(0)->wide() != 17 ||
scalar_mixes->Get(0)->suffix() != 18) return 22;
if (scalar_mixes->Get(1)->prefix() != 3 || scalar_mixes->Get(1)->wide() != 19 ||
scalar_mixes->Get(1)->suffix() != 20) return 23;

const NestedMix *nested_mix = root->nested_mix();
if (nested_mix == nullptr || nested_mix->flag() != 4) return 24;
if (nested_mix->value().prefix() != 5 || nested_mix->value().wide() != 21 ||
nested_mix->value().suffix() != 22) return 25;
if (nested_mix->count() != UINT32_C(4000000001)) return 26;

return 0;
}
12 changes: 11 additions & 1 deletion test/flatbuffer/schema/table_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,17 @@ defmodule Flatbuffer.Schema.TableTest do
"vector_of_struct" => 27
}
}},
"Struct" => {:struct, %{members: [field1: :byte, field2: :short]}},
"Struct" =>
{:struct,
%{
members: [field1: :byte, field2: :short],
layout: [
{:field1, {:byte, %{default: 0}}, 0},
{:field2, {:short, %{default: 0}}, 2}
],
alignment: 2,
size: 4
}},
"ByteEnum" =>
{:enum,
%{
Expand Down
6 changes: 5 additions & 1 deletion test/flatbuffer/schema/union_types_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ defmodule FlatBuffer.Schema.UnionTypesTest do
field_ids: %{"data_type" => 0, "data" => 1, "additions_value" => 2}
}},
"hello" =>
{:table, %{fields: {{:salute, {:string, %{}}}}, field_ids: %{"salute" => 0}}}
{:table,
%{
fields: {{:salute, {:string, %{}}}},
field_ids: %{"salute" => 0}
}}
},
root_type: {:table, %{name: "command_root"}},
id: "cmnd"
Expand Down
Loading