blob: 2e1ed8fec2daac8f2f203c81484acb936c761754 [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 }
Philipp Schrader47445a02020-11-14 17:31:04 -0800249 throw new Error('Couldn\'t find field ' + fieldName + '.');
James Kuszmaulabb77132020-08-01 19:56:16 -0700250 }
251
252 // Reads a scalar with the given field name from a Table. If readDefaults
253 // is set to false and the field is unset, we will return null. If
254 // readDefaults is true and the field is unset, we will look-up the default
255 // value for the field and return that.
256 // For 64-bit fields, returns a flatbuffer Long rather than a standard number.
257 // TODO(james): For this and other accessors, determine if there is a
258 // significant performance gain to be had by using readScalar to construct
259 // an accessor method rather than having to redo the schema inspection on
260 // every call.
261 readScalar(table: Table, fieldName: string, readDefaults: boolean = false) {
262 const field = this.getField(fieldName, table.typeIndex);
263 const fieldType = field.type();
264 const isStruct = this.getType(table.typeIndex).isStruct();
265 if (!isScalar(fieldType.baseType())) {
266 throw new Error('Field ' + fieldName + ' is not a scalar type.');
267 }
268
269 if (isStruct) {
270 return table.readScalar(
271 fieldType.baseType(), table.offset + field.offset());
272 }
273
274 const offset =
275 table.offset + table.bb.__offset(table.offset, field.offset());
276 if (offset === table.offset) {
277 if (!readDefaults) {
278 return null;
279 }
280 if (isInteger(fieldType.baseType())) {
281 if (isLong(fieldType.baseType())) {
282 return field.defaultInteger();
283 } else {
284 if (field.defaultInteger().high != 0) {
285 throw new Error(
286 '<=4 byte integer types should not use 64-bit default values.');
287 }
288 return field.defaultInteger().low;
289 }
290 } else {
291 return field.defaultReal();
292 }
293 }
294 return table.readScalar(fieldType.baseType(), offset);
295 }
296 // Reads a string with the given field name from the provided Table.
297 // If the field is unset, returns null.
298 readString(table: Table, fieldName: string): string|null {
299 const field = this.getField(fieldName, table.typeIndex);
300 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800301 if (fieldType.baseType() !== reflection.BaseType.String) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700302 throw new Error('Field ' + fieldName + ' is not a string.');
303 }
304
305 const offsetToOffset =
306 table.offset + table.bb.__offset(table.offset, field.offset());
307 if (offsetToOffset === table.offset) {
308 return null;
309 }
Philipp Schradere625ba22020-11-16 20:11:37 -0800310 return table.bb.__string(offsetToOffset) as string;
James Kuszmaulabb77132020-08-01 19:56:16 -0700311 }
312 // Reads a sub-message from the given Table. The sub-message may either be
313 // a struct or a Table. Returns null if the sub-message is not set.
314 readTable(table: Table, fieldName: string): Table|null {
315 const field = this.getField(fieldName, table.typeIndex);
316 const fieldType = field.type();
317 const parentIsStruct = this.getType(table.typeIndex).isStruct();
Austin Schuh7c75e582020-11-14 16:41:18 -0800318 if (fieldType.baseType() !== reflection.BaseType.Obj) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700319 throw new Error('Field ' + fieldName + ' is not an object type.');
320 }
321
322 if (parentIsStruct) {
323 return new Table(
324 table.bb, fieldType.index(), table.offset + field.offset());
325 }
326
327 const offsetToOffset =
328 table.offset + table.bb.__offset(table.offset, field.offset());
329 if (offsetToOffset === table.offset) {
330 return null;
331 }
332
333 const elementIsStruct = this.getType(fieldType.index()).isStruct();
334
335 const objectStart =
336 elementIsStruct ? offsetToOffset : table.bb.__indirect(offsetToOffset);
337 return new Table(table.bb, fieldType.index(), objectStart);
338 }
339 // Reads a vector of scalars (like readScalar, may return a vector of Long's
340 // instead). Also, will return null if the vector is not set.
341 readVectorOfScalars(table: Table, fieldName: string): number[]|null {
342 const field = this.getField(fieldName, table.typeIndex);
343 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800344 if (fieldType.baseType() !== reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700345 throw new Error('Field ' + fieldName + ' is not an vector.');
346 }
347 if (!isScalar(fieldType.element())) {
348 throw new Error('Field ' + fieldName + ' is not an vector of scalars.');
349 }
350
351 const offsetToOffset =
352 table.offset + table.bb.__offset(table.offset, field.offset());
353 if (offsetToOffset === table.offset) {
354 return null;
355 }
356 const numElements = table.bb.__vector_len(offsetToOffset);
357 const result = [];
358 const baseOffset = table.bb.__vector(offsetToOffset);
359 const scalarSize = typeSize(fieldType.element());
360 for (let ii = 0; ii < numElements; ++ii) {
361 result.push(
362 table.readScalar(fieldType.element(), baseOffset + scalarSize * ii));
363 }
364 return result;
365 }
366 // Reads a vector of tables. Returns null if vector is not set.
367 readVectorOfTables(table: Table, fieldName: string) {
368 const field = this.getField(fieldName, table.typeIndex);
369 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800370 if (fieldType.baseType() !== reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700371 throw new Error('Field ' + fieldName + ' is not an vector.');
372 }
Austin Schuh7c75e582020-11-14 16:41:18 -0800373 if (fieldType.element() !== reflection.BaseType.Obj) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700374 throw new Error('Field ' + fieldName + ' is not an vector of objects.');
375 }
376
377 const offsetToOffset =
378 table.offset + table.bb.__offset(table.offset, field.offset());
379 if (offsetToOffset === table.offset) {
380 return null;
381 }
382 const numElements = table.bb.__vector_len(offsetToOffset);
383 const result = [];
384 const baseOffset = table.bb.__vector(offsetToOffset);
385 const elementSchema = this.getType(fieldType.index());
386 const elementIsStruct = elementSchema.isStruct();
387 const elementSize = elementIsStruct ? elementSchema.bytesize() :
388 typeSize(fieldType.element());
389 for (let ii = 0; ii < numElements; ++ii) {
390 const elementOffset = baseOffset + elementSize * ii;
391 result.push(new Table(
392 table.bb, fieldType.index(),
393 elementIsStruct ? elementOffset :
394 table.bb.__indirect(elementOffset)));
395 }
396 return result;
397 }
398 // Reads a vector of strings. Returns null if not set.
399 readVectorOfStrings(table: Table, fieldName: string): string[]|null {
400 const field = this.getField(fieldName, table.typeIndex);
401 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800402 if (fieldType.baseType() !== reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700403 throw new Error('Field ' + fieldName + ' is not an vector.');
404 }
Austin Schuh7c75e582020-11-14 16:41:18 -0800405 if (fieldType.element() !== reflection.BaseType.String) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700406 throw new Error('Field ' + fieldName + ' is not an vector of strings.');
407 }
408
409 const offsetToOffset =
410 table.offset + table.bb.__offset(table.offset, field.offset());
411 if (offsetToOffset === table.offset) {
412 return null;
413 }
414 const numElements = table.bb.__vector_len(offsetToOffset);
415 const result = [];
416 const baseOffset = table.bb.__vector(offsetToOffset);
417 const offsetSize = typeSize(fieldType.element());
418 for (let ii = 0; ii < numElements; ++ii) {
419 result.push(table.bb.__string(baseOffset + offsetSize * ii));
420 }
421 return result;
422 }
423}