blob: 62cfa6105a3a6766edef559e4277778e68a8a6c2 [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
Austin Schuh7c75e582020-11-14 16:41:18 -08009import {reflection, aos} from 'aos/configuration_generated';
James Kuszmaulabb77132020-08-01 19:56:16 -070010
11// Returns the size, in bytes, of the given type. For vectors/strings/etc.
12// returns the size of the offset.
Austin Schuh7c75e582020-11-14 16:41:18 -080013function typeSize(baseType: reflection.BaseType): number {
James Kuszmaulabb77132020-08-01 19:56:16 -070014 switch (baseType) {
Austin Schuh7c75e582020-11-14 16:41:18 -080015 case reflection.BaseType.None:
16 case reflection.BaseType.UType:
17 case reflection.BaseType.Bool:
18 case reflection.BaseType.Byte:
19 case reflection.BaseType.UByte:
James Kuszmaulabb77132020-08-01 19:56:16 -070020 return 1;
Austin Schuh7c75e582020-11-14 16:41:18 -080021 case reflection.BaseType.Short:
22 case reflection.BaseType.UShort:
James Kuszmaulabb77132020-08-01 19:56:16 -070023 return 2;
Austin Schuh7c75e582020-11-14 16:41:18 -080024 case reflection.BaseType.Int:
25 case reflection.BaseType.UInt:
James Kuszmaulabb77132020-08-01 19:56:16 -070026 return 4;
Austin Schuh7c75e582020-11-14 16:41:18 -080027 case reflection.BaseType.Long:
28 case reflection.BaseType.ULong:
James Kuszmaulabb77132020-08-01 19:56:16 -070029 return 8;
Austin Schuh7c75e582020-11-14 16:41:18 -080030 case reflection.BaseType.Float:
James Kuszmaulabb77132020-08-01 19:56:16 -070031 return 4;
Austin Schuh7c75e582020-11-14 16:41:18 -080032 case reflection.BaseType.Double:
James Kuszmaulabb77132020-08-01 19:56:16 -070033 return 8;
Austin Schuh7c75e582020-11-14 16:41:18 -080034 case reflection.BaseType.String:
35 case reflection.BaseType.Vector:
36 case reflection.BaseType.Obj:
37 case reflection.BaseType.Union:
38 case reflection.BaseType.Array:
James Kuszmaulabb77132020-08-01 19:56:16 -070039 return 4;
40 }
41}
42
43// Returns whether the given type is a scalar type.
Austin Schuh7c75e582020-11-14 16:41:18 -080044function isScalar(baseType: reflection.BaseType): boolean {
James Kuszmaulabb77132020-08-01 19:56:16 -070045 switch (baseType) {
Austin Schuh7c75e582020-11-14 16:41:18 -080046 case reflection.BaseType.UType:
47 case reflection.BaseType.Bool:
48 case reflection.BaseType.Byte:
49 case reflection.BaseType.UByte:
50 case reflection.BaseType.Short:
51 case reflection.BaseType.UShort:
52 case reflection.BaseType.Int:
53 case reflection.BaseType.UInt:
54 case reflection.BaseType.Long:
55 case reflection.BaseType.ULong:
56 case reflection.BaseType.Float:
57 case reflection.BaseType.Double:
James Kuszmaulabb77132020-08-01 19:56:16 -070058 return true;
Austin Schuh7c75e582020-11-14 16:41:18 -080059 case reflection.BaseType.None:
60 case reflection.BaseType.String:
61 case reflection.BaseType.Vector:
62 case reflection.BaseType.Obj:
63 case reflection.BaseType.Union:
64 case reflection.BaseType.Array:
James Kuszmaulabb77132020-08-01 19:56:16 -070065 return false;
66 }
67}
68
69// Returns whether the given type is integer or not.
Austin Schuh7c75e582020-11-14 16:41:18 -080070function isInteger(baseType: reflection.BaseType): boolean {
James Kuszmaulabb77132020-08-01 19:56:16 -070071 switch (baseType) {
Austin Schuh7c75e582020-11-14 16:41:18 -080072 case reflection.BaseType.UType:
73 case reflection.BaseType.Bool:
74 case reflection.BaseType.Byte:
75 case reflection.BaseType.UByte:
76 case reflection.BaseType.Short:
77 case reflection.BaseType.UShort:
78 case reflection.BaseType.Int:
79 case reflection.BaseType.UInt:
80 case reflection.BaseType.Long:
81 case reflection.BaseType.ULong:
James Kuszmaulabb77132020-08-01 19:56:16 -070082 return true;
Austin Schuh7c75e582020-11-14 16:41:18 -080083 case reflection.BaseType.Float:
84 case reflection.BaseType.Double:
85 case reflection.BaseType.None:
86 case reflection.BaseType.String:
87 case reflection.BaseType.Vector:
88 case reflection.BaseType.Obj:
89 case reflection.BaseType.Union:
90 case reflection.BaseType.Array:
James Kuszmaulabb77132020-08-01 19:56:16 -070091 return false;
92 }
93}
94
95// Returns whether the given type is a long--this is needed to know whether it
96// can be represented by the normal javascript number (8-byte integers require a
97// special type, since the native number type is an 8-byte double, which won't
98// represent 8-byte integers to full precision).
Austin Schuh7c75e582020-11-14 16:41:18 -080099function isLong(baseType: reflection.BaseType): boolean {
James Kuszmaulabb77132020-08-01 19:56:16 -0700100 return isInteger(baseType) && (typeSize(baseType) > 4);
101}
102
103// TODO(james): Use the actual flatbuffers.ByteBuffer object; this is just
104// to prevent the typescript compiler from complaining.
105class ByteBuffer {}
106
107// Stores the data associated with a Table within a given buffer.
108export class Table {
109 // Wrapper to represent an object (Table or Struct) within a ByteBuffer.
110 // The ByteBuffer is the raw data associated with the object.
111 // typeIndex is an index into the schema object vector for the parser
112 // object that this is associated with.
113 // offset is the absolute location within the buffer where the root of the
114 // object is.
115 // Note that a given Table assumes that it is being used with a particular
116 // Schema object.
117 // External users should generally not be using this constructor directly.
118 constructor(
119 public readonly bb: ByteBuffer,
120 public readonly typeIndex: number, public readonly offset: number) {}
121 // Constructs a Table object for the root of a ByteBuffer--this assumes that
122 // the type of the Table is the root table of the Parser that you are using.
123 static getRootTable(bb: ByteBuffer): Table {
124 return new Table(bb, -1, bb.readInt32(bb.position()) + bb.position());
125 }
126 // Reads a scalar of a given type at a given offset.
Austin Schuh7c75e582020-11-14 16:41:18 -0800127 readScalar(fieldType: reflection.BaseType, offset: number) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700128 switch (fieldType) {
Austin Schuh7c75e582020-11-14 16:41:18 -0800129 case reflection.BaseType.UType:
130 case reflection.BaseType.Bool:
James Kuszmaulabb77132020-08-01 19:56:16 -0700131 return this.bb.readUint8(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800132 case reflection.BaseType.Byte:
James Kuszmaulabb77132020-08-01 19:56:16 -0700133 return this.bb.readInt8(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800134 case reflection.BaseType.UByte:
James Kuszmaulabb77132020-08-01 19:56:16 -0700135 return this.bb.readUint8(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800136 case reflection.BaseType.Short:
James Kuszmaulabb77132020-08-01 19:56:16 -0700137 return this.bb.readInt16(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800138 case reflection.BaseType.UShort:
James Kuszmaulabb77132020-08-01 19:56:16 -0700139 return this.bb.readUint16(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800140 case reflection.BaseType.Int:
James Kuszmaulabb77132020-08-01 19:56:16 -0700141 return this.bb.readInt32(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800142 case reflection.BaseType.UInt:
James Kuszmaulabb77132020-08-01 19:56:16 -0700143 return this.bb.readUint32(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800144 case reflection.BaseType.Long:
James Kuszmaulabb77132020-08-01 19:56:16 -0700145 return this.bb.readInt64(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800146 case reflection.BaseType.ULong:
James Kuszmaulabb77132020-08-01 19:56:16 -0700147 return this.bb.readUint64(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800148 case reflection.BaseType.Float:
James Kuszmaulabb77132020-08-01 19:56:16 -0700149 return this.bb.readFloat32(offset);
Austin Schuh7c75e582020-11-14 16:41:18 -0800150 case reflection.BaseType.Double:
James Kuszmaulabb77132020-08-01 19:56:16 -0700151 return this.bb.readFloat64(offset);
152 }
153 throw new Error('Unsupported message type ' + baseType);
154 }
155};
156
157// The Parser class uses a Schema to provide all the utilities required to
158// parse flatbuffers that have a type that is the same as the root_type defined
159// by the Schema.
160// The classical usage would be to, e.g., be reading a channel with a type of
161// "aos.FooBar". At startup, you would construct a Parser from the channel's
162// Schema. When a message is received on the channel , you would then use
163// Table.getRootTable() on the received buffer to construct the Table, and
164// then access the members using the various methods of the Parser (or just
165// convert the entire object to a javascript Object/JSON using toObject()).
166export class Parser {
Austin Schuh7c75e582020-11-14 16:41:18 -0800167 constructor(private readonly schema: reflection.Schema) {}
James Kuszmaulabb77132020-08-01 19:56:16 -0700168
169 // Parse a Table to a javascript object. This is can be used, e.g., to convert
170 // a flatbuffer Table to JSON.
171 // If readDefaults is set to true, then scalar fields will be filled out with
172 // their default values if not populated; if readDefaults is false and the
173 // field is not populated, the resulting object will not populate the field.
174 toObject(table: Table, readDefaults: boolean = false) {
175 const result = {};
176 const schema = this.getType(table.typeIndex);
177 const numFields = schema.fieldsLength();
178 for (let ii = 0; ii < numFields; ++ii) {
179 const field = schema.fields(ii);
180 const baseType = field.type().baseType();
181 let fieldValue = null;
182 if (isScalar(baseType)) {
183 fieldValue = this.readScalar(table, field.name(), readDefaults);
Austin Schuh7c75e582020-11-14 16:41:18 -0800184 } else if (baseType === reflection.BaseType.String) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700185 fieldValue = this.readString(table, field.name());
Austin Schuh7c75e582020-11-14 16:41:18 -0800186 } else if (baseType === reflection.BaseType.Obj) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700187 const subTable = this.readTable(table, field.name());
188 if (subTable !== null) {
189 fieldValue = this.toObject(subTable, readDefaults);
190 }
Austin Schuh7c75e582020-11-14 16:41:18 -0800191 } else if (baseType === reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700192 const elementType = field.type().element();
193 if (isScalar(elementType)) {
194 fieldValue = this.readVectorOfScalars(table, field.name());
Austin Schuh7c75e582020-11-14 16:41:18 -0800195 } else if (elementType === reflection.BaseType.String) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700196 fieldValue = this.readVectorOfStrings(table, field.name());
Austin Schuh7c75e582020-11-14 16:41:18 -0800197 } else if (elementType === reflection.BaseType.Obj) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700198 const tables = this.readVectorOfTables(table, field.name());
199 if (tables !== null) {
200 fieldValue = [];
201 for (const table of tables) {
202 fieldValue.push(this.toObject(table, readDefaults));
203 }
204 }
205 } else {
206 throw new Error('Vectors of Unions and Arrays are not supported.');
207 }
208 } else {
209 throw new Error(
210 'Unions and Arrays are not supported in field ' + field.name());
211 }
212 if (fieldValue !== null) {
213 result[field.name()] = fieldValue;
214 }
215 }
216 return result;
217 }
218
219 // Returns the Object definition associated with the given type index.
220 getType(typeIndex: number): Object {
221 if (typeIndex === -1) {
222 return this.schema.rootTable();
223 }
224 if (typeIndex < 0 || typeIndex > this.schema.objectsLength()) {
225 throw new Error("Type index out-of-range.");
226 }
227 return this.schema.objects(typeIndex);
228 }
229
230 // Retrieves the Field schema for the given field name within a given
231 // type index.
Austin Schuh7c75e582020-11-14 16:41:18 -0800232 getField(fieldName: string, typeIndex: number): reflection.Field {
James Kuszmaulabb77132020-08-01 19:56:16 -0700233 const schema: Object = this.getType(typeIndex);
234 const numFields = schema.fieldsLength();
235 for (let ii = 0; ii < numFields; ++ii) {
236 const field = schema.fields(ii);
237 const name = field.name();
238 if (fieldName === name) {
239 return field;
240 }
241 }
242 throw new Error(
243 'Couldn\'t find field ' + fieldName + ' options are ' + fields);
244 }
245
246 // Reads a scalar with the given field name from a Table. If readDefaults
247 // is set to false and the field is unset, we will return null. If
248 // readDefaults is true and the field is unset, we will look-up the default
249 // value for the field and return that.
250 // For 64-bit fields, returns a flatbuffer Long rather than a standard number.
251 // TODO(james): For this and other accessors, determine if there is a
252 // significant performance gain to be had by using readScalar to construct
253 // an accessor method rather than having to redo the schema inspection on
254 // every call.
255 readScalar(table: Table, fieldName: string, readDefaults: boolean = false) {
256 const field = this.getField(fieldName, table.typeIndex);
257 const fieldType = field.type();
258 const isStruct = this.getType(table.typeIndex).isStruct();
259 if (!isScalar(fieldType.baseType())) {
260 throw new Error('Field ' + fieldName + ' is not a scalar type.');
261 }
262
263 if (isStruct) {
264 return table.readScalar(
265 fieldType.baseType(), table.offset + field.offset());
266 }
267
268 const offset =
269 table.offset + table.bb.__offset(table.offset, field.offset());
270 if (offset === table.offset) {
271 if (!readDefaults) {
272 return null;
273 }
274 if (isInteger(fieldType.baseType())) {
275 if (isLong(fieldType.baseType())) {
276 return field.defaultInteger();
277 } else {
278 if (field.defaultInteger().high != 0) {
279 throw new Error(
280 '<=4 byte integer types should not use 64-bit default values.');
281 }
282 return field.defaultInteger().low;
283 }
284 } else {
285 return field.defaultReal();
286 }
287 }
288 return table.readScalar(fieldType.baseType(), offset);
289 }
290 // Reads a string with the given field name from the provided Table.
291 // If the field is unset, returns null.
292 readString(table: Table, fieldName: string): string|null {
293 const field = this.getField(fieldName, table.typeIndex);
294 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800295 if (fieldType.baseType() !== reflection.BaseType.String) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700296 throw new Error('Field ' + fieldName + ' is not a string.');
297 }
298
299 const offsetToOffset =
300 table.offset + table.bb.__offset(table.offset, field.offset());
301 if (offsetToOffset === table.offset) {
302 return null;
303 }
304 return table.bb.__string(offsetToOffset);
305 }
306 // Reads a sub-message from the given Table. The sub-message may either be
307 // a struct or a Table. Returns null if the sub-message is not set.
308 readTable(table: Table, fieldName: string): Table|null {
309 const field = this.getField(fieldName, table.typeIndex);
310 const fieldType = field.type();
311 const parentIsStruct = this.getType(table.typeIndex).isStruct();
Austin Schuh7c75e582020-11-14 16:41:18 -0800312 if (fieldType.baseType() !== reflection.BaseType.Obj) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700313 throw new Error('Field ' + fieldName + ' is not an object type.');
314 }
315
316 if (parentIsStruct) {
317 return new Table(
318 table.bb, fieldType.index(), table.offset + field.offset());
319 }
320
321 const offsetToOffset =
322 table.offset + table.bb.__offset(table.offset, field.offset());
323 if (offsetToOffset === table.offset) {
324 return null;
325 }
326
327 const elementIsStruct = this.getType(fieldType.index()).isStruct();
328
329 const objectStart =
330 elementIsStruct ? offsetToOffset : table.bb.__indirect(offsetToOffset);
331 return new Table(table.bb, fieldType.index(), objectStart);
332 }
333 // Reads a vector of scalars (like readScalar, may return a vector of Long's
334 // instead). Also, will return null if the vector is not set.
335 readVectorOfScalars(table: Table, fieldName: string): number[]|null {
336 const field = this.getField(fieldName, table.typeIndex);
337 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800338 if (fieldType.baseType() !== reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700339 throw new Error('Field ' + fieldName + ' is not an vector.');
340 }
341 if (!isScalar(fieldType.element())) {
342 throw new Error('Field ' + fieldName + ' is not an vector of scalars.');
343 }
344
345 const offsetToOffset =
346 table.offset + table.bb.__offset(table.offset, field.offset());
347 if (offsetToOffset === table.offset) {
348 return null;
349 }
350 const numElements = table.bb.__vector_len(offsetToOffset);
351 const result = [];
352 const baseOffset = table.bb.__vector(offsetToOffset);
353 const scalarSize = typeSize(fieldType.element());
354 for (let ii = 0; ii < numElements; ++ii) {
355 result.push(
356 table.readScalar(fieldType.element(), baseOffset + scalarSize * ii));
357 }
358 return result;
359 }
360 // Reads a vector of tables. Returns null if vector is not set.
361 readVectorOfTables(table: Table, fieldName: string) {
362 const field = this.getField(fieldName, table.typeIndex);
363 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800364 if (fieldType.baseType() !== reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700365 throw new Error('Field ' + fieldName + ' is not an vector.');
366 }
Austin Schuh7c75e582020-11-14 16:41:18 -0800367 if (fieldType.element() !== reflection.BaseType.Obj) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700368 throw new Error('Field ' + fieldName + ' is not an vector of objects.');
369 }
370
371 const offsetToOffset =
372 table.offset + table.bb.__offset(table.offset, field.offset());
373 if (offsetToOffset === table.offset) {
374 return null;
375 }
376 const numElements = table.bb.__vector_len(offsetToOffset);
377 const result = [];
378 const baseOffset = table.bb.__vector(offsetToOffset);
379 const elementSchema = this.getType(fieldType.index());
380 const elementIsStruct = elementSchema.isStruct();
381 const elementSize = elementIsStruct ? elementSchema.bytesize() :
382 typeSize(fieldType.element());
383 for (let ii = 0; ii < numElements; ++ii) {
384 const elementOffset = baseOffset + elementSize * ii;
385 result.push(new Table(
386 table.bb, fieldType.index(),
387 elementIsStruct ? elementOffset :
388 table.bb.__indirect(elementOffset)));
389 }
390 return result;
391 }
392 // Reads a vector of strings. Returns null if not set.
393 readVectorOfStrings(table: Table, fieldName: string): string[]|null {
394 const field = this.getField(fieldName, table.typeIndex);
395 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800396 if (fieldType.baseType() !== reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700397 throw new Error('Field ' + fieldName + ' is not an vector.');
398 }
Austin Schuh7c75e582020-11-14 16:41:18 -0800399 if (fieldType.element() !== reflection.BaseType.String) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700400 throw new Error('Field ' + fieldName + ' is not an vector of strings.');
401 }
402
403 const offsetToOffset =
404 table.offset + table.bb.__offset(table.offset, field.offset());
405 if (offsetToOffset === table.offset) {
406 return null;
407 }
408 const numElements = table.bb.__vector_len(offsetToOffset);
409 const result = [];
410 const baseOffset = table.bb.__vector(offsetToOffset);
411 const offsetSize = typeSize(fieldType.element());
412 for (let ii = 0; ii < numElements; ++ii) {
413 result.push(table.bb.__string(baseOffset + offsetSize * ii));
414 }
415 return result;
416 }
417}