blob: 09206e0a32135ad70fce35eb84be5311bb6e0a40 [file] [log] [blame]
James Kuszmaulabb77132020-08-01 19:56:16 -07001// This library provides a few basic reflection utilities for Flatbuffers.
2// Currently, this only really supports the level of reflection that would
3// be necessary to convert a serialized flatbuffer to JSON using just a
4// reflection.Schema flatbuffer describing the type.
5// The current implementation is also not necessarily robust to invalidly
6// constructed flatbuffers.
7// See reflection_test_main.ts for sample usage.
8
Philipp Schradere625ba22020-11-16 20:11:37 -08009import {reflection, aos} from 'org_frc971/aos/configuration_generated';
10import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
James Kuszmaulabb77132020-08-01 19:56:16 -070011
12// Returns the size, in bytes, of the given type. For vectors/strings/etc.
13// returns the size of the offset.
Austin Schuh7c75e582020-11-14 16:41:18 -080014function typeSize(baseType: reflection.BaseType): number {
James Kuszmaulabb77132020-08-01 19:56:16 -070015 switch (baseType) {
Austin Schuh7c75e582020-11-14 16:41:18 -080016 case reflection.BaseType.None:
17 case reflection.BaseType.UType:
18 case reflection.BaseType.Bool:
19 case reflection.BaseType.Byte:
20 case reflection.BaseType.UByte:
James Kuszmaulabb77132020-08-01 19:56:16 -070021 return 1;
Austin Schuh7c75e582020-11-14 16:41:18 -080022 case reflection.BaseType.Short:
23 case reflection.BaseType.UShort:
James Kuszmaulabb77132020-08-01 19:56:16 -070024 return 2;
Austin Schuh7c75e582020-11-14 16:41:18 -080025 case reflection.BaseType.Int:
26 case reflection.BaseType.UInt:
James Kuszmaulabb77132020-08-01 19:56:16 -070027 return 4;
Austin Schuh7c75e582020-11-14 16:41:18 -080028 case reflection.BaseType.Long:
29 case reflection.BaseType.ULong:
James Kuszmaulabb77132020-08-01 19:56:16 -070030 return 8;
Austin Schuh7c75e582020-11-14 16:41:18 -080031 case reflection.BaseType.Float:
James Kuszmaulabb77132020-08-01 19:56:16 -070032 return 4;
Austin Schuh7c75e582020-11-14 16:41:18 -080033 case reflection.BaseType.Double:
James Kuszmaulabb77132020-08-01 19:56:16 -070034 return 8;
Austin Schuh7c75e582020-11-14 16:41:18 -080035 case reflection.BaseType.String:
36 case reflection.BaseType.Vector:
37 case reflection.BaseType.Obj:
38 case reflection.BaseType.Union:
39 case reflection.BaseType.Array:
James Kuszmaulabb77132020-08-01 19:56:16 -070040 return 4;
41 }
42}
43
44// Returns whether the given type is a scalar type.
Austin Schuh7c75e582020-11-14 16:41:18 -080045function isScalar(baseType: reflection.BaseType): boolean {
James Kuszmaulabb77132020-08-01 19:56:16 -070046 switch (baseType) {
Austin Schuh7c75e582020-11-14 16:41:18 -080047 case reflection.BaseType.UType:
48 case reflection.BaseType.Bool:
49 case reflection.BaseType.Byte:
50 case reflection.BaseType.UByte:
51 case reflection.BaseType.Short:
52 case reflection.BaseType.UShort:
53 case reflection.BaseType.Int:
54 case reflection.BaseType.UInt:
55 case reflection.BaseType.Long:
56 case reflection.BaseType.ULong:
57 case reflection.BaseType.Float:
58 case reflection.BaseType.Double:
James Kuszmaulabb77132020-08-01 19:56:16 -070059 return true;
Austin Schuh7c75e582020-11-14 16:41:18 -080060 case reflection.BaseType.None:
61 case reflection.BaseType.String:
62 case reflection.BaseType.Vector:
63 case reflection.BaseType.Obj:
64 case reflection.BaseType.Union:
65 case reflection.BaseType.Array:
James Kuszmaulabb77132020-08-01 19:56:16 -070066 return false;
67 }
68}
69
70// Returns whether the given type is integer or not.
Austin Schuh7c75e582020-11-14 16:41:18 -080071function isInteger(baseType: reflection.BaseType): boolean {
James Kuszmaulabb77132020-08-01 19:56:16 -070072 switch (baseType) {
Austin Schuh7c75e582020-11-14 16:41:18 -080073 case reflection.BaseType.UType:
74 case reflection.BaseType.Bool:
75 case reflection.BaseType.Byte:
76 case reflection.BaseType.UByte:
77 case reflection.BaseType.Short:
78 case reflection.BaseType.UShort:
79 case reflection.BaseType.Int:
80 case reflection.BaseType.UInt:
81 case reflection.BaseType.Long:
82 case reflection.BaseType.ULong:
James Kuszmaulabb77132020-08-01 19:56:16 -070083 return true;
Austin Schuh7c75e582020-11-14 16:41:18 -080084 case reflection.BaseType.Float:
85 case reflection.BaseType.Double:
86 case reflection.BaseType.None:
87 case reflection.BaseType.String:
88 case reflection.BaseType.Vector:
89 case reflection.BaseType.Obj:
90 case reflection.BaseType.Union:
91 case reflection.BaseType.Array:
James Kuszmaulabb77132020-08-01 19:56:16 -070092 return false;
93 }
94}
95
96// Returns whether the given type is a long--this is needed to know whether it
97// can be represented by the normal javascript number (8-byte integers require a
98// special type, since the native number type is an 8-byte double, which won't
99// represent 8-byte integers to full precision).
Austin Schuh7c75e582020-11-14 16:41:18 -0800100function isLong(baseType: reflection.BaseType): boolean {
James Kuszmaulabb77132020-08-01 19:56:16 -0700101 return isInteger(baseType) && (typeSize(baseType) > 4);
102}
103
James Kuszmaulabb77132020-08-01 19:56:16 -0700104// Stores the data associated with a Table within a given buffer.
105export class Table {
106 // Wrapper to represent an object (Table or Struct) within a ByteBuffer.
107 // The ByteBuffer is the raw data associated with the object.
108 // typeIndex is an index into the schema object vector for the parser
109 // object that this is associated with.
110 // offset is the absolute location within the buffer where the root of the
111 // object is.
112 // Note that a given Table assumes that it is being used with a particular
113 // Schema object.
114 // External users should generally not be using this constructor directly.
115 constructor(
116 public readonly bb: ByteBuffer,
117 public readonly typeIndex: number, public readonly offset: number) {}
118 // Constructs a Table object for the root of a ByteBuffer--this assumes that
119 // the type of the Table is the root table of the Parser that you are using.
120 static getRootTable(bb: ByteBuffer): Table {
121 return new Table(bb, -1, bb.readInt32(bb.position()) + bb.position());
122 }
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800123 static getNamedTable(
124 bb: ByteBuffer, schema: reflection.Schema, type: string,
125 offset: number): Table {
126 for (let ii = 0; ii < schema.objectsLength(); ++ii) {
127 if (schema.objects(ii).name() == type) {
128 return new Table(bb, ii, offset);
129 }
130 }
131 throw new Error('Unable to find type ' + type + ' in schema.');
132 }
James Kuszmaulabb77132020-08-01 19:56:16 -0700133 // Reads a scalar of a given type at a given offset.
Austin Schuh7c75e582020-11-14 16:41:18 -0800134 readScalar(fieldType: reflection.BaseType, offset: number) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700135 switch (fieldType) {
Austin Schuh7c75e582020-11-14 16:41:18 -0800136 case reflection.BaseType.UType:
137 case reflection.BaseType.Bool:
James Kuszmaulabb77132020-08-01 19:56:16 -0700138 return this.bb.readUint8(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800139 case reflection.BaseType.Byte:
James Kuszmaulabb77132020-08-01 19:56:16 -0700140 return this.bb.readInt8(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800141 case reflection.BaseType.UByte:
James Kuszmaulabb77132020-08-01 19:56:16 -0700142 return this.bb.readUint8(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800143 case reflection.BaseType.Short:
James Kuszmaulabb77132020-08-01 19:56:16 -0700144 return this.bb.readInt16(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800145 case reflection.BaseType.UShort:
James Kuszmaulabb77132020-08-01 19:56:16 -0700146 return this.bb.readUint16(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800147 case reflection.BaseType.Int:
James Kuszmaulabb77132020-08-01 19:56:16 -0700148 return this.bb.readInt32(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800149 case reflection.BaseType.UInt:
James Kuszmaulabb77132020-08-01 19:56:16 -0700150 return this.bb.readUint32(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800151 case reflection.BaseType.Long:
James Kuszmaulabb77132020-08-01 19:56:16 -0700152 return this.bb.readInt64(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800153 case reflection.BaseType.ULong:
James Kuszmaulabb77132020-08-01 19:56:16 -0700154 return this.bb.readUint64(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800155 case reflection.BaseType.Float:
James Kuszmaulabb77132020-08-01 19:56:16 -0700156 return this.bb.readFloat32(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800157 case reflection.BaseType.Double:
James Kuszmaulabb77132020-08-01 19:56:16 -0700158 return this.bb.readFloat64(offset);
159 }
Philipp Schrader47445a02020-11-14 17:31:04 -0800160 throw new Error('Unsupported message type ' + fieldType);
James Kuszmaulabb77132020-08-01 19:56:16 -0700161 }
162};
163
164// The Parser class uses a Schema to provide all the utilities required to
165// parse flatbuffers that have a type that is the same as the root_type defined
166// by the Schema.
167// The classical usage would be to, e.g., be reading a channel with a type of
168// "aos.FooBar". At startup, you would construct a Parser from the channel's
169// Schema. When a message is received on the channel , you would then use
170// Table.getRootTable() on the received buffer to construct the Table, and
171// then access the members using the various methods of the Parser (or just
172// convert the entire object to a javascript Object/JSON using toObject()).
173export class Parser {
Austin Schuh7c75e582020-11-14 16:41:18 -0800174 constructor(private readonly schema: reflection.Schema) {}
James Kuszmaulabb77132020-08-01 19:56:16 -0700175
176 // Parse a Table to a javascript object. This is can be used, e.g., to convert
177 // a flatbuffer Table to JSON.
178 // If readDefaults is set to true, then scalar fields will be filled out with
179 // their default values if not populated; if readDefaults is false and the
180 // field is not populated, the resulting object will not populate the field.
181 toObject(table: Table, readDefaults: boolean = false) {
182 const result = {};
183 const schema = this.getType(table.typeIndex);
184 const numFields = schema.fieldsLength();
185 for (let ii = 0; ii < numFields; ++ii) {
186 const field = schema.fields(ii);
187 const baseType = field.type().baseType();
188 let fieldValue = null;
189 if (isScalar(baseType)) {
190 fieldValue = this.readScalar(table, field.name(), readDefaults);
Austin Schuh7c75e582020-11-14 16:41:18 -0800191 } else if (baseType === reflection.BaseType.String) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700192 fieldValue = this.readString(table, field.name());
Austin Schuh7c75e582020-11-14 16:41:18 -0800193 } else if (baseType === reflection.BaseType.Obj) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700194 const subTable = this.readTable(table, field.name());
195 if (subTable !== null) {
196 fieldValue = this.toObject(subTable, readDefaults);
197 }
Austin Schuh7c75e582020-11-14 16:41:18 -0800198 } else if (baseType === reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700199 const elementType = field.type().element();
200 if (isScalar(elementType)) {
201 fieldValue = this.readVectorOfScalars(table, field.name());
Austin Schuh7c75e582020-11-14 16:41:18 -0800202 } else if (elementType === reflection.BaseType.String) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700203 fieldValue = this.readVectorOfStrings(table, field.name());
Austin Schuh7c75e582020-11-14 16:41:18 -0800204 } else if (elementType === reflection.BaseType.Obj) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700205 const tables = this.readVectorOfTables(table, field.name());
206 if (tables !== null) {
207 fieldValue = [];
208 for (const table of tables) {
209 fieldValue.push(this.toObject(table, readDefaults));
210 }
211 }
212 } else {
213 throw new Error('Vectors of Unions and Arrays are not supported.');
214 }
215 } else {
216 throw new Error(
217 'Unions and Arrays are not supported in field ' + field.name());
218 }
219 if (fieldValue !== null) {
220 result[field.name()] = fieldValue;
221 }
222 }
223 return result;
224 }
225
226 // Returns the Object definition associated with the given type index.
Philipp Schradere625ba22020-11-16 20:11:37 -0800227 getType(typeIndex: number): reflection.Object {
James Kuszmaulabb77132020-08-01 19:56:16 -0700228 if (typeIndex === -1) {
229 return this.schema.rootTable();
230 }
231 if (typeIndex < 0 || typeIndex > this.schema.objectsLength()) {
232 throw new Error("Type index out-of-range.");
233 }
234 return this.schema.objects(typeIndex);
235 }
236
237 // Retrieves the Field schema for the given field name within a given
238 // type index.
Austin Schuh7c75e582020-11-14 16:41:18 -0800239 getField(fieldName: string, typeIndex: number): reflection.Field {
Philipp Schradere625ba22020-11-16 20:11:37 -0800240 const schema: reflection.Object = this.getType(typeIndex);
James Kuszmaulabb77132020-08-01 19:56:16 -0700241 const numFields = schema.fieldsLength();
242 for (let ii = 0; ii < numFields; ++ii) {
243 const field = schema.fields(ii);
244 const name = field.name();
245 if (fieldName === name) {
246 return field;
247 }
248 }
James Kuszmaulc4ae11c2020-12-26 16:26:58 -0800249 throw new Error(
250 'Couldn\'t find field ' + fieldName + ' in object ' + schema.name() +
251 '.');
James Kuszmaulabb77132020-08-01 19:56:16 -0700252 }
253
254 // Reads a scalar with the given field name from a Table. If readDefaults
255 // is set to false and the field is unset, we will return null. If
256 // readDefaults is true and the field is unset, we will look-up the default
257 // value for the field and return that.
258 // For 64-bit fields, returns a flatbuffer Long rather than a standard number.
259 // TODO(james): For this and other accessors, determine if there is a
260 // significant performance gain to be had by using readScalar to construct
261 // an accessor method rather than having to redo the schema inspection on
262 // every call.
263 readScalar(table: Table, fieldName: string, readDefaults: boolean = false) {
264 const field = this.getField(fieldName, table.typeIndex);
265 const fieldType = field.type();
266 const isStruct = this.getType(table.typeIndex).isStruct();
267 if (!isScalar(fieldType.baseType())) {
268 throw new Error('Field ' + fieldName + ' is not a scalar type.');
269 }
270
271 if (isStruct) {
272 return table.readScalar(
273 fieldType.baseType(), table.offset + field.offset());
274 }
275
276 const offset =
277 table.offset + table.bb.__offset(table.offset, field.offset());
278 if (offset === table.offset) {
279 if (!readDefaults) {
280 return null;
281 }
282 if (isInteger(fieldType.baseType())) {
283 if (isLong(fieldType.baseType())) {
284 return field.defaultInteger();
285 } else {
286 if (field.defaultInteger().high != 0) {
287 throw new Error(
288 '<=4 byte integer types should not use 64-bit default values.');
289 }
290 return field.defaultInteger().low;
291 }
292 } else {
293 return field.defaultReal();
294 }
295 }
296 return table.readScalar(fieldType.baseType(), offset);
297 }
298 // Reads a string with the given field name from the provided Table.
299 // If the field is unset, returns null.
300 readString(table: Table, fieldName: string): string|null {
301 const field = this.getField(fieldName, table.typeIndex);
302 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800303 if (fieldType.baseType() !== reflection.BaseType.String) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700304 throw new Error('Field ' + fieldName + ' is not a string.');
305 }
306
307 const offsetToOffset =
308 table.offset + table.bb.__offset(table.offset, field.offset());
309 if (offsetToOffset === table.offset) {
310 return null;
311 }
Philipp Schradere625ba22020-11-16 20:11:37 -0800312 return table.bb.__string(offsetToOffset) as string;
James Kuszmaulabb77132020-08-01 19:56:16 -0700313 }
314 // Reads a sub-message from the given Table. The sub-message may either be
315 // a struct or a Table. Returns null if the sub-message is not set.
316 readTable(table: Table, fieldName: string): Table|null {
317 const field = this.getField(fieldName, table.typeIndex);
318 const fieldType = field.type();
319 const parentIsStruct = this.getType(table.typeIndex).isStruct();
Austin Schuh7c75e582020-11-14 16:41:18 -0800320 if (fieldType.baseType() !== reflection.BaseType.Obj) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700321 throw new Error('Field ' + fieldName + ' is not an object type.');
322 }
323
324 if (parentIsStruct) {
325 return new Table(
326 table.bb, fieldType.index(), table.offset + field.offset());
327 }
328
329 const offsetToOffset =
330 table.offset + table.bb.__offset(table.offset, field.offset());
331 if (offsetToOffset === table.offset) {
332 return null;
333 }
334
335 const elementIsStruct = this.getType(fieldType.index()).isStruct();
336
337 const objectStart =
338 elementIsStruct ? offsetToOffset : table.bb.__indirect(offsetToOffset);
339 return new Table(table.bb, fieldType.index(), objectStart);
340 }
341 // Reads a vector of scalars (like readScalar, may return a vector of Long's
342 // instead). Also, will return null if the vector is not set.
343 readVectorOfScalars(table: Table, fieldName: string): number[]|null {
344 const field = this.getField(fieldName, table.typeIndex);
345 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800346 if (fieldType.baseType() !== reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700347 throw new Error('Field ' + fieldName + ' is not an vector.');
348 }
349 if (!isScalar(fieldType.element())) {
350 throw new Error('Field ' + fieldName + ' is not an vector of scalars.');
351 }
352
353 const offsetToOffset =
354 table.offset + table.bb.__offset(table.offset, field.offset());
355 if (offsetToOffset === table.offset) {
356 return null;
357 }
358 const numElements = table.bb.__vector_len(offsetToOffset);
359 const result = [];
360 const baseOffset = table.bb.__vector(offsetToOffset);
361 const scalarSize = typeSize(fieldType.element());
362 for (let ii = 0; ii < numElements; ++ii) {
363 result.push(
364 table.readScalar(fieldType.element(), baseOffset + scalarSize * ii));
365 }
366 return result;
367 }
368 // Reads a vector of tables. Returns null if vector is not set.
369 readVectorOfTables(table: Table, fieldName: string) {
370 const field = this.getField(fieldName, table.typeIndex);
371 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800372 if (fieldType.baseType() !== reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700373 throw new Error('Field ' + fieldName + ' is not an vector.');
374 }
Austin Schuh7c75e582020-11-14 16:41:18 -0800375 if (fieldType.element() !== reflection.BaseType.Obj) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700376 throw new Error('Field ' + fieldName + ' is not an vector of objects.');
377 }
378
379 const offsetToOffset =
380 table.offset + table.bb.__offset(table.offset, field.offset());
381 if (offsetToOffset === table.offset) {
382 return null;
383 }
384 const numElements = table.bb.__vector_len(offsetToOffset);
385 const result = [];
386 const baseOffset = table.bb.__vector(offsetToOffset);
387 const elementSchema = this.getType(fieldType.index());
388 const elementIsStruct = elementSchema.isStruct();
389 const elementSize = elementIsStruct ? elementSchema.bytesize() :
390 typeSize(fieldType.element());
391 for (let ii = 0; ii < numElements; ++ii) {
392 const elementOffset = baseOffset + elementSize * ii;
393 result.push(new Table(
394 table.bb, fieldType.index(),
395 elementIsStruct ? elementOffset :
396 table.bb.__indirect(elementOffset)));
397 }
398 return result;
399 }
400 // Reads a vector of strings. Returns null if not set.
401 readVectorOfStrings(table: Table, fieldName: string): string[]|null {
402 const field = this.getField(fieldName, table.typeIndex);
403 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800404 if (fieldType.baseType() !== reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700405 throw new Error('Field ' + fieldName + ' is not an vector.');
406 }
Austin Schuh7c75e582020-11-14 16:41:18 -0800407 if (fieldType.element() !== reflection.BaseType.String) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700408 throw new Error('Field ' + fieldName + ' is not an vector of strings.');
409 }
410
411 const offsetToOffset =
412 table.offset + table.bb.__offset(table.offset, field.offset());
413 if (offsetToOffset === table.offset) {
414 return null;
415 }
416 const numElements = table.bb.__vector_len(offsetToOffset);
417 const result = [];
418 const baseOffset = table.bb.__vector(offsetToOffset);
419 const offsetSize = typeSize(fieldType.element());
420 for (let ii = 0; ii < numElements; ++ii) {
421 result.push(table.bb.__string(baseOffset + offsetSize * ii));
422 }
423 return result;
424 }
425}