blob: a2fe3e44eda8350d9def4ebb0b253ce79abfa442 [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 }
Philipp Schrader47445a02020-11-14 17:31:04 -0800153 throw new Error('Unsupported message type ' + fieldType);
James Kuszmaulabb77132020-08-01 19:56:16 -0700154 }
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 }
Philipp Schrader47445a02020-11-14 17:31:04 -0800242 throw new Error('Couldn\'t find field ' + fieldName + '.');
James Kuszmaulabb77132020-08-01 19:56:16 -0700243 }
244
245 // Reads a scalar with the given field name from a Table. If readDefaults
246 // is set to false and the field is unset, we will return null. If
247 // readDefaults is true and the field is unset, we will look-up the default
248 // value for the field and return that.
249 // For 64-bit fields, returns a flatbuffer Long rather than a standard number.
250 // TODO(james): For this and other accessors, determine if there is a
251 // significant performance gain to be had by using readScalar to construct
252 // an accessor method rather than having to redo the schema inspection on
253 // every call.
254 readScalar(table: Table, fieldName: string, readDefaults: boolean = false) {
255 const field = this.getField(fieldName, table.typeIndex);
256 const fieldType = field.type();
257 const isStruct = this.getType(table.typeIndex).isStruct();
258 if (!isScalar(fieldType.baseType())) {
259 throw new Error('Field ' + fieldName + ' is not a scalar type.');
260 }
261
262 if (isStruct) {
263 return table.readScalar(
264 fieldType.baseType(), table.offset + field.offset());
265 }
266
267 const offset =
268 table.offset + table.bb.__offset(table.offset, field.offset());
269 if (offset === table.offset) {
270 if (!readDefaults) {
271 return null;
272 }
273 if (isInteger(fieldType.baseType())) {
274 if (isLong(fieldType.baseType())) {
275 return field.defaultInteger();
276 } else {
277 if (field.defaultInteger().high != 0) {
278 throw new Error(
279 '<=4 byte integer types should not use 64-bit default values.');
280 }
281 return field.defaultInteger().low;
282 }
283 } else {
284 return field.defaultReal();
285 }
286 }
287 return table.readScalar(fieldType.baseType(), offset);
288 }
289 // Reads a string with the given field name from the provided Table.
290 // If the field is unset, returns null.
291 readString(table: Table, fieldName: string): string|null {
292 const field = this.getField(fieldName, table.typeIndex);
293 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800294 if (fieldType.baseType() !== reflection.BaseType.String) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700295 throw new Error('Field ' + fieldName + ' is not a string.');
296 }
297
298 const offsetToOffset =
299 table.offset + table.bb.__offset(table.offset, field.offset());
300 if (offsetToOffset === table.offset) {
301 return null;
302 }
303 return table.bb.__string(offsetToOffset);
304 }
305 // Reads a sub-message from the given Table. The sub-message may either be
306 // a struct or a Table. Returns null if the sub-message is not set.
307 readTable(table: Table, fieldName: string): Table|null {
308 const field = this.getField(fieldName, table.typeIndex);
309 const fieldType = field.type();
310 const parentIsStruct = this.getType(table.typeIndex).isStruct();
Austin Schuh7c75e582020-11-14 16:41:18 -0800311 if (fieldType.baseType() !== reflection.BaseType.Obj) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700312 throw new Error('Field ' + fieldName + ' is not an object type.');
313 }
314
315 if (parentIsStruct) {
316 return new Table(
317 table.bb, fieldType.index(), table.offset + field.offset());
318 }
319
320 const offsetToOffset =
321 table.offset + table.bb.__offset(table.offset, field.offset());
322 if (offsetToOffset === table.offset) {
323 return null;
324 }
325
326 const elementIsStruct = this.getType(fieldType.index()).isStruct();
327
328 const objectStart =
329 elementIsStruct ? offsetToOffset : table.bb.__indirect(offsetToOffset);
330 return new Table(table.bb, fieldType.index(), objectStart);
331 }
332 // Reads a vector of scalars (like readScalar, may return a vector of Long's
333 // instead). Also, will return null if the vector is not set.
334 readVectorOfScalars(table: Table, fieldName: string): number[]|null {
335 const field = this.getField(fieldName, table.typeIndex);
336 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800337 if (fieldType.baseType() !== reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700338 throw new Error('Field ' + fieldName + ' is not an vector.');
339 }
340 if (!isScalar(fieldType.element())) {
341 throw new Error('Field ' + fieldName + ' is not an vector of scalars.');
342 }
343
344 const offsetToOffset =
345 table.offset + table.bb.__offset(table.offset, field.offset());
346 if (offsetToOffset === table.offset) {
347 return null;
348 }
349 const numElements = table.bb.__vector_len(offsetToOffset);
350 const result = [];
351 const baseOffset = table.bb.__vector(offsetToOffset);
352 const scalarSize = typeSize(fieldType.element());
353 for (let ii = 0; ii < numElements; ++ii) {
354 result.push(
355 table.readScalar(fieldType.element(), baseOffset + scalarSize * ii));
356 }
357 return result;
358 }
359 // Reads a vector of tables. Returns null if vector is not set.
360 readVectorOfTables(table: Table, fieldName: string) {
361 const field = this.getField(fieldName, table.typeIndex);
362 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800363 if (fieldType.baseType() !== reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700364 throw new Error('Field ' + fieldName + ' is not an vector.');
365 }
Austin Schuh7c75e582020-11-14 16:41:18 -0800366 if (fieldType.element() !== reflection.BaseType.Obj) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700367 throw new Error('Field ' + fieldName + ' is not an vector of objects.');
368 }
369
370 const offsetToOffset =
371 table.offset + table.bb.__offset(table.offset, field.offset());
372 if (offsetToOffset === table.offset) {
373 return null;
374 }
375 const numElements = table.bb.__vector_len(offsetToOffset);
376 const result = [];
377 const baseOffset = table.bb.__vector(offsetToOffset);
378 const elementSchema = this.getType(fieldType.index());
379 const elementIsStruct = elementSchema.isStruct();
380 const elementSize = elementIsStruct ? elementSchema.bytesize() :
381 typeSize(fieldType.element());
382 for (let ii = 0; ii < numElements; ++ii) {
383 const elementOffset = baseOffset + elementSize * ii;
384 result.push(new Table(
385 table.bb, fieldType.index(),
386 elementIsStruct ? elementOffset :
387 table.bb.__indirect(elementOffset)));
388 }
389 return result;
390 }
391 // Reads a vector of strings. Returns null if not set.
392 readVectorOfStrings(table: Table, fieldName: string): string[]|null {
393 const field = this.getField(fieldName, table.typeIndex);
394 const fieldType = field.type();
Austin Schuh7c75e582020-11-14 16:41:18 -0800395 if (fieldType.baseType() !== reflection.BaseType.Vector) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700396 throw new Error('Field ' + fieldName + ' is not an vector.');
397 }
Austin Schuh7c75e582020-11-14 16:41:18 -0800398 if (fieldType.element() !== reflection.BaseType.String) {
James Kuszmaulabb77132020-08-01 19:56:16 -0700399 throw new Error('Field ' + fieldName + ' is not an vector of strings.');
400 }
401
402 const offsetToOffset =
403 table.offset + table.bb.__offset(table.offset, field.offset());
404 if (offsetToOffset === table.offset) {
405 return null;
406 }
407 const numElements = table.bb.__vector_len(offsetToOffset);
408 const result = [];
409 const baseOffset = table.bb.__vector(offsetToOffset);
410 const offsetSize = typeSize(fieldType.element());
411 for (let ii = 0; ii < numElements; ++ii) {
412 result.push(table.bb.__string(baseOffset + offsetSize * ii));
413 }
414 return result;
415 }
416}