blob: 9c21a17ac5bee32ca63a21989407c9af01173408 [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 }
123 // Reads a scalar of a given type at a given offset.
Austin Schuh7c75e582020-11-14 16:41:18 -0800124 readScalar(fieldType: reflection.BaseType, offset: number) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700125 switch (fieldType) {
Austin Schuh7c75e582020-11-14 16:41:18 -0800126 case reflection.BaseType.UType:
127 case reflection.BaseType.Bool:
James Kuszmaulabb77132020-08-01 19:56:16 -0700128 return this.bb.readUint8(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800129 case reflection.BaseType.Byte:
James Kuszmaulabb77132020-08-01 19:56:16 -0700130 return this.bb.readInt8(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800131 case reflection.BaseType.UByte:
James Kuszmaulabb77132020-08-01 19:56:16 -0700132 return this.bb.readUint8(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800133 case reflection.BaseType.Short:
James Kuszmaulabb77132020-08-01 19:56:16 -0700134 return this.bb.readInt16(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800135 case reflection.BaseType.UShort:
James Kuszmaulabb77132020-08-01 19:56:16 -0700136 return this.bb.readUint16(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800137 case reflection.BaseType.Int:
James Kuszmaulabb77132020-08-01 19:56:16 -0700138 return this.bb.readInt32(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800139 case reflection.BaseType.UInt:
James Kuszmaulabb77132020-08-01 19:56:16 -0700140 return this.bb.readUint32(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800141 case reflection.BaseType.Long:
James Kuszmaulabb77132020-08-01 19:56:16 -0700142 return this.bb.readInt64(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800143 case reflection.BaseType.ULong:
James Kuszmaulabb77132020-08-01 19:56:16 -0700144 return this.bb.readUint64(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800145 case reflection.BaseType.Float:
James Kuszmaulabb77132020-08-01 19:56:16 -0700146 return this.bb.readFloat32(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800147 case reflection.BaseType.Double:
James Kuszmaulabb77132020-08-01 19:56:16 -0700148 return this.bb.readFloat64(offset);
149 }
Philipp Schrader47445a02020-11-14 17:31:04 -0800150 throw new Error('Unsupported message type ' + fieldType);
James Kuszmaulabb77132020-08-01 19:56:16 -0700151 }
152};
153
154// The Parser class uses a Schema to provide all the utilities required to
155// parse flatbuffers that have a type that is the same as the root_type defined
156// by the Schema.
157// The classical usage would be to, e.g., be reading a channel with a type of
158// "aos.FooBar". At startup, you would construct a Parser from the channel's
159// Schema. When a message is received on the channel , you would then use
160// Table.getRootTable() on the received buffer to construct the Table, and
161// then access the members using the various methods of the Parser (or just
162// convert the entire object to a javascript Object/JSON using toObject()).
163export class Parser {
Austin Schuh7c75e582020-11-14 16:41:18 -0800164 constructor(private readonly schema: reflection.Schema) {}
James Kuszmaulabb77132020-08-01 19:56:16 -0700165
166 // Parse a Table to a javascript object. This is can be used, e.g., to convert
167 // a flatbuffer Table to JSON.
168 // If readDefaults is set to true, then scalar fields will be filled out with
169 // their default values if not populated; if readDefaults is false and the
170 // field is not populated, the resulting object will not populate the field.
171 toObject(table: Table, readDefaults: boolean = false) {
172 const result = {};
173 const schema = this.getType(table.typeIndex);
174 const numFields = schema.fieldsLength();
175 for (let ii = 0; ii < numFields; ++ii) {
176 const field = schema.fields(ii);
177 const baseType = field.type().baseType();
178 let fieldValue = null;
179 if (isScalar(baseType)) {
180 fieldValue = this.readScalar(table, field.name(), readDefaults);
Austin Schuh7c75e582020-11-14 16:41:18 -0800181 } else if (baseType === reflection.BaseType.String) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700182 fieldValue = this.readString(table, field.name());
Austin Schuh7c75e582020-11-14 16:41:18 -0800183 } else if (baseType === reflection.BaseType.Obj) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700184 const subTable = this.readTable(table, field.name());
185 if (subTable !== null) {
186 fieldValue = this.toObject(subTable, readDefaults);
187 }
Austin Schuh7c75e582020-11-14 16:41:18 -0800188 } else if (baseType === reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700189 const elementType = field.type().element();
190 if (isScalar(elementType)) {
191 fieldValue = this.readVectorOfScalars(table, field.name());
Austin Schuh7c75e582020-11-14 16:41:18 -0800192 } else if (elementType === reflection.BaseType.String) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700193 fieldValue = this.readVectorOfStrings(table, field.name());
Austin Schuh7c75e582020-11-14 16:41:18 -0800194 } else if (elementType === reflection.BaseType.Obj) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700195 const tables = this.readVectorOfTables(table, field.name());
196 if (tables !== null) {
197 fieldValue = [];
198 for (const table of tables) {
199 fieldValue.push(this.toObject(table, readDefaults));
200 }
201 }
202 } else {
203 throw new Error('Vectors of Unions and Arrays are not supported.');
204 }
205 } else {
206 throw new Error(
207 'Unions and Arrays are not supported in field ' + field.name());
208 }
209 if (fieldValue !== null) {
210 result[field.name()] = fieldValue;
211 }
212 }
213 return result;
214 }
215
216 // Returns the Object definition associated with the given type index.
Philipp Schradere625ba22020-11-16 20:11:37 -0800217 getType(typeIndex: number): reflection.Object {
James Kuszmaulabb77132020-08-01 19:56:16 -0700218 if (typeIndex === -1) {
219 return this.schema.rootTable();
220 }
221 if (typeIndex < 0 || typeIndex > this.schema.objectsLength()) {
222 throw new Error("Type index out-of-range.");
223 }
224 return this.schema.objects(typeIndex);
225 }
226
227 // Retrieves the Field schema for the given field name within a given
228 // type index.
Austin Schuh7c75e582020-11-14 16:41:18 -0800229 getField(fieldName: string, typeIndex: number): reflection.Field {
Philipp Schradere625ba22020-11-16 20:11:37 -0800230 const schema: reflection.Object = this.getType(typeIndex);
James Kuszmaulabb77132020-08-01 19:56:16 -0700231 const numFields = schema.fieldsLength();
232 for (let ii = 0; ii < numFields; ++ii) {
233 const field = schema.fields(ii);
234 const name = field.name();
235 if (fieldName === name) {
236 return field;
237 }
238 }
Philipp Schrader47445a02020-11-14 17:31:04 -0800239 throw new Error('Couldn\'t find field ' + fieldName + '.');
James Kuszmaulabb77132020-08-01 19:56:16 -0700240 }
241
242 // Reads a scalar with the given field name from a Table. If readDefaults
243 // is set to false and the field is unset, we will return null. If
244 // readDefaults is true and the field is unset, we will look-up the default
245 // value for the field and return that.
246 // For 64-bit fields, returns a flatbuffer Long rather than a standard number.
247 // TODO(james): For this and other accessors, determine if there is a
248 // significant performance gain to be had by using readScalar to construct
249 // an accessor method rather than having to redo the schema inspection on
250 // every call.
251 readScalar(table: Table, fieldName: string, readDefaults: boolean = false) {
252 const field = this.getField(fieldName, table.typeIndex);
253 const fieldType = field.type();
254 const isStruct = this.getType(table.typeIndex).isStruct();
255 if (!isScalar(fieldType.baseType())) {
256 throw new Error('Field ' + fieldName + ' is not a scalar type.');
257 }
258
259 if (isStruct) {
260 return table.readScalar(
261 fieldType.baseType(), table.offset + field.offset());
262 }
263
264 const offset =
265 table.offset + table.bb.__offset(table.offset, field.offset());
266 if (offset === table.offset) {
267 if (!readDefaults) {
268 return null;
269 }
270 if (isInteger(fieldType.baseType())) {
271 if (isLong(fieldType.baseType())) {
272 return field.defaultInteger();
273 } else {
274 if (field.defaultInteger().high != 0) {
275 throw new Error(
276 '<=4 byte integer types should not use 64-bit default values.');
277 }
278 return field.defaultInteger().low;
279 }
280 } else {
281 return field.defaultReal();
282 }
283 }
284 return table.readScalar(fieldType.baseType(), offset);
285 }
286 // Reads a string with the given field name from the provided Table.
287 // If the field is unset, returns null.
288 readString(table: Table, fieldName: string): string|null {
289 const field = this.getField(fieldName, table.typeIndex);
290 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800291 if (fieldType.baseType() !== reflection.BaseType.String) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700292 throw new Error('Field ' + fieldName + ' is not a string.');
293 }
294
295 const offsetToOffset =
296 table.offset + table.bb.__offset(table.offset, field.offset());
297 if (offsetToOffset === table.offset) {
298 return null;
299 }
Philipp Schradere625ba22020-11-16 20:11:37 -0800300 return table.bb.__string(offsetToOffset) as string;
James Kuszmaulabb77132020-08-01 19:56:16 -0700301 }
302 // Reads a sub-message from the given Table. The sub-message may either be
303 // a struct or a Table. Returns null if the sub-message is not set.
304 readTable(table: Table, fieldName: string): Table|null {
305 const field = this.getField(fieldName, table.typeIndex);
306 const fieldType = field.type();
307 const parentIsStruct = this.getType(table.typeIndex).isStruct();
Austin Schuh7c75e582020-11-14 16:41:18 -0800308 if (fieldType.baseType() !== reflection.BaseType.Obj) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700309 throw new Error('Field ' + fieldName + ' is not an object type.');
310 }
311
312 if (parentIsStruct) {
313 return new Table(
314 table.bb, fieldType.index(), table.offset + field.offset());
315 }
316
317 const offsetToOffset =
318 table.offset + table.bb.__offset(table.offset, field.offset());
319 if (offsetToOffset === table.offset) {
320 return null;
321 }
322
323 const elementIsStruct = this.getType(fieldType.index()).isStruct();
324
325 const objectStart =
326 elementIsStruct ? offsetToOffset : table.bb.__indirect(offsetToOffset);
327 return new Table(table.bb, fieldType.index(), objectStart);
328 }
329 // Reads a vector of scalars (like readScalar, may return a vector of Long's
330 // instead). Also, will return null if the vector is not set.
331 readVectorOfScalars(table: Table, fieldName: string): number[]|null {
332 const field = this.getField(fieldName, table.typeIndex);
333 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800334 if (fieldType.baseType() !== reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700335 throw new Error('Field ' + fieldName + ' is not an vector.');
336 }
337 if (!isScalar(fieldType.element())) {
338 throw new Error('Field ' + fieldName + ' is not an vector of scalars.');
339 }
340
341 const offsetToOffset =
342 table.offset + table.bb.__offset(table.offset, field.offset());
343 if (offsetToOffset === table.offset) {
344 return null;
345 }
346 const numElements = table.bb.__vector_len(offsetToOffset);
347 const result = [];
348 const baseOffset = table.bb.__vector(offsetToOffset);
349 const scalarSize = typeSize(fieldType.element());
350 for (let ii = 0; ii < numElements; ++ii) {
351 result.push(
352 table.readScalar(fieldType.element(), baseOffset + scalarSize * ii));
353 }
354 return result;
355 }
356 // Reads a vector of tables. Returns null if vector is not set.
357 readVectorOfTables(table: Table, fieldName: string) {
358 const field = this.getField(fieldName, table.typeIndex);
359 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800360 if (fieldType.baseType() !== reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700361 throw new Error('Field ' + fieldName + ' is not an vector.');
362 }
Austin Schuh7c75e582020-11-14 16:41:18 -0800363 if (fieldType.element() !== reflection.BaseType.Obj) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700364 throw new Error('Field ' + fieldName + ' is not an vector of objects.');
365 }
366
367 const offsetToOffset =
368 table.offset + table.bb.__offset(table.offset, field.offset());
369 if (offsetToOffset === table.offset) {
370 return null;
371 }
372 const numElements = table.bb.__vector_len(offsetToOffset);
373 const result = [];
374 const baseOffset = table.bb.__vector(offsetToOffset);
375 const elementSchema = this.getType(fieldType.index());
376 const elementIsStruct = elementSchema.isStruct();
377 const elementSize = elementIsStruct ? elementSchema.bytesize() :
378 typeSize(fieldType.element());
379 for (let ii = 0; ii < numElements; ++ii) {
380 const elementOffset = baseOffset + elementSize * ii;
381 result.push(new Table(
382 table.bb, fieldType.index(),
383 elementIsStruct ? elementOffset :
384 table.bb.__indirect(elementOffset)));
385 }
386 return result;
387 }
388 // Reads a vector of strings. Returns null if not set.
389 readVectorOfStrings(table: Table, fieldName: string): string[]|null {
390 const field = this.getField(fieldName, table.typeIndex);
391 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800392 if (fieldType.baseType() !== reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700393 throw new Error('Field ' + fieldName + ' is not an vector.');
394 }
Austin Schuh7c75e582020-11-14 16:41:18 -0800395 if (fieldType.element() !== reflection.BaseType.String) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700396 throw new Error('Field ' + fieldName + ' is not an vector of strings.');
397 }
398
399 const offsetToOffset =
400 table.offset + table.bb.__offset(table.offset, field.offset());
401 if (offsetToOffset === table.offset) {
402 return null;
403 }
404 const numElements = table.bb.__vector_len(offsetToOffset);
405 const result = [];
406 const baseOffset = table.bb.__vector(offsetToOffset);
407 const offsetSize = typeSize(fieldType.element());
408 for (let ii = 0; ii < numElements; ++ii) {
409 result.push(table.bb.__string(baseOffset + offsetSize * ii));
410 }
411 return result;
412 }
413}