Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Flatbuffer Schema Support #70

Merged
merged 16 commits into from
Nov 22, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
129 changes: 129 additions & 0 deletions internal/generateFlatbuffer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { generateFlatbuffer } from "./generateFlatbuffer";
import { exampleEnum, exampleMessage } from "./testFixtures";

describe("generateFlatBuffer", () => {
it("generates Message .fbs files", () => {
expect(generateFlatbuffer(exampleMessage)).toMatchInlineSnapshot(`
"// Generated by https://github.com/foxglove/schemas

include "ByteVectorForNesting.fbs";
include "Duration.fbs";
include "ExampleEnum.fbs";
include "NestedMessage.fbs";
include "Time.fbs";

namespace foxglove;

/// An example type
table ExampleMessage {
/// duration field
field_duration:Duration;

/// time field
field_time:Time;

/// boolean field
field_boolean:bool;

/// bytes field
field_bytes:[byte];

/// float64 field
field_float64:double;

/// uint32 field
field_uint32:uint;

/// string field
field_string:string;

/// duration array field
field_duration_array:[Duration];

/// time array field
field_time_array:[Time];

/// boolean array field
field_boolean_array:[bool];

/// bytes array field
field_bytes_array:[ByteVectorForNesting];

/// float64 array field
field_float64_array:[double];

/// uint32 array field
field_uint32_array:[uint];

/// string array field
field_string_array:[string];

/// duration fixed-length array field
/// length 3
field_duration_fixed_array:[Duration];

/// time fixed-length array field
/// length 3
field_time_fixed_array:[Time];

/// boolean fixed-length array field
/// length 3
field_boolean_fixed_array:[bool];

/// bytes fixed-length array field
/// length 3
field_bytes_fixed_array:[ByteVectorForNesting];

/// float64 fixed-length array field
/// length 3
field_float64_fixed_array:[double];

/// uint32 fixed-length array field
/// length 3
field_uint32_fixed_array:[uint];

/// string fixed-length array field
/// length 3
field_string_fixed_array:[string];

/// An enum field
field_enum:ExampleEnum;

/// An enum array field
field_enum_array:[ExampleEnum];

/// A nested field
field_nested:foxglove.NestedMessage;

/// A nested array field
/// With
/// a
/// very
/// long
/// description
field_nested_array:[foxglove.NestedMessage];
}

root_type ExampleMessage;
"
`);
});
it("generates Enum .fbs files", () => {
expect(generateFlatbuffer(exampleEnum)).toMatchInlineSnapshot(`
"// Generated by https://github.com/foxglove/schemas

namespace foxglove;

/// An example enum
enum ExampleEnum : byte {
/// Value A
A = 1,

/// Value B
B = 2,
}

"
`);
});
});
137 changes: 137 additions & 0 deletions internal/generateFlatbuffer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { FoxgloveMessageField, FoxglovePrimitive, FoxgloveSchema } from "./types";

// Flatbuffer only supports nested vectors via table
export const BYTE_VECTOR_FB = `table ByteVectorForNesting {
snosenzo marked this conversation as resolved.
Show resolved Hide resolved
data:[byte];
}

root_type ByteVectorForNesting;
`;

// Same as protobuf wellknown types
export const TIME_FB = `struct Time {
sec:long;
nsec:int;
}
`;

export const DURATION_FB = `struct Duration {
sec:long;
nsec:int;
}
`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use the int64 and int32 aliases for clarity?

Maybe worth copying/adapting this from protobuf docs to give more explanation on how negative durations work?
image

On the other hand, an option would be to simply use a single uint64 (time) or int64 (duration). That would give a wider range of representable values too, although it probably doesn't matter because the range is so large already.

Note that ROS does it differently:

ROS 2:
int32 sec, uint32 nsec: https://github.com/ros2/rcl_interfaces/blob/master/builtin_interfaces/msg/Time.msg
int32 sec, uint32 nsec: https://github.com/ros2/rcl_interfaces/blob/master/builtin_interfaces/msg/Duration.msg
C++ API: Time (int32_t seconds, uint32_t nanoseconds https://docs.ros2.org/beta3/api/rclcpp/classrclcpp_1_1Time.html
C++ API: Duration (int32_t seconds, uint32_t nanoseconds) https://docs.ros2.org/dashing/api/rclcpp/classrclcpp_1_1Duration.html
C API: uint64 time, int64 duration, both storing nanoseconds: https://docs.ros2.org/beta1/api/rcl/time_8h.html#a5957493a6bde7c3878f947398f048a24

ROS 1:
uint32 sec, uint32 nsec http://docs.ros.org/en/latest/api/rostime/html/classros_1_1TimeBase.html
int32 sec, int32 nsec http://docs.ros.org/en/latest/api/rostime/html/classros_1_1DurationBase.html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah good call, added the comment in line with the protobuf.
What kind of consideration should be made to the ROS times schemas in this context? Is this difference going to cause issues at read time?


// fields that would benefit from having a default of 1
const defaultOneNumberFields = new Set(["x", "y", "z", "r", "g", "b", "a", "w"]);
function isDefaultOneField(field: FoxgloveMessageField): boolean {
return (
field.type.type === "primitive" &&
field.type.name === "float64" &&
defaultOneNumberFields.has(field.name)
);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't belong here because it's not really specific to flatbuffers. If we want to add defaults to some fields, we need to add the default values to schemas.ts and just read them here.

Copy link
Contributor Author

@snosenzo snosenzo Nov 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to the schema under defaultValue?: string under the MessageSchemaField type if that seems appropriate


function primitiveToFlatbuffer(type: Exclude<FoxglovePrimitive, "time" | "duration">) {
switch (type) {
case "uint32":
return "uint";
snosenzo marked this conversation as resolved.
Show resolved Hide resolved
case "bytes":
return "[byte]";
snosenzo marked this conversation as resolved.
Show resolved Hide resolved
case "string":
return "string";
case "boolean":
return "bool";
case "float64":
return "double";
}
}

export function generateFlatbuffer(schema: FoxgloveSchema): string {
let definition;
const imports = new Set<string>();
switch (schema.type) {
case "enum": {
const fields = schema.values.map(({ name, value, description }) => {
if (description != undefined) {
return `/// ${description}\n ${name} = ${value},`;
} else {
return `${name} = ${value},`;
}
});

// `///` comments required to show up in compiled flatbuffer schemas
definition = `/// ${schema.description}\nenum ${schema.name} : byte {\n ${fields.join(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use ubyte for enums? Might want to add validation that the values all fit within the range of the type we choose. I assume the compiler would validate this, so if we can get the compiler to be tested in CI, then that would be sufficient.

"\n\n ",
)}\n}\n`;
break;
}
case "message": {
const fields = schema.fields.map((field) => {
const isArray = field.array != undefined;

let type;
switch (field.type.type) {
case "enum":
type = field.type.enum.name;
imports.add(field.type.enum.name);
break;
case "nested":
type = `foxglove.${field.type.schema.name}`;
imports.add(field.type.schema.name);
break;
case "primitive":
if (field.type.name === "time") {
type = "Time";
imports.add(`Time`);
} else if (field.type.name === "duration") {
type = "Duration";
imports.add(`Duration`);
} else if (field.type.name === "bytes" && isArray) {
type = "ByteVectorForNesting";
imports.add("ByteVectorForNesting");
} else {
type = primitiveToFlatbuffer(field.type.name);
}
break;
}
let lengthComment;

if (typeof field.array === "number") {
// can't specify length of vector outside of struct, all of these are tables
lengthComment = ` /// length ${field.array}\n`;
}
return `${field.description
.trim()
.split("\n")
.map((line) => ` /// ${line}\n`)
.join("")}${
// can't have inline comments, so the lengthComment needs to be above
lengthComment ?? ""
// convert field.name to lowercase for flatbuffer compilation compliance
} ${field.name.toLowerCase()}:${isArray ? `[${type}]` : type}${
isDefaultOneField(field) ? ` = 1.0` : ""
};`;
});

definition = `/// ${schema.description}\ntable ${schema.name} {\n${fields.join(
"\n\n",
)}\n}\n\nroot_type ${schema.name};`;
break;
}
}

const outputSections = [
`// Generated by https://github.com/foxglove/schemas`,

Array.from(imports)
.sort()
.map((name) => `include "${name}.fbs";`)
.join("\n"),

`namespace foxglove;`,

definition,
].filter(Boolean);

return outputSections.join("\n\n") + "\n";
}
29 changes: 29 additions & 0 deletions schemas/flatbuffer/foxglove/ArrowPrimitive.fbs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions schemas/flatbuffer/foxglove/ByteVectorForNesting.fbs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

76 changes: 76 additions & 0 deletions schemas/flatbuffer/foxglove/CameraCalibration.fbs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading