flatbuffers
607 строк · 18.8 Кб
1import { ByteBuffer } from "./byte-buffer.js"2import { SIZEOF_SHORT, SIZE_PREFIX_LENGTH, SIZEOF_INT, FILE_IDENTIFIER_LENGTH } from "./constants.js"3import { Offset, IGeneratedObject } from "./types.js"4
5export class Builder {6private bb: ByteBuffer7/** Remaining space in the ByteBuffer. */8private space: number9/** Minimum alignment encountered so far. */10private minalign = 111/** The vtable for the current table. */12private vtable: number[] | null = null13/** The amount of fields we're actually using. */14private vtable_in_use = 015/** Whether we are currently serializing a table. */16private isNested = false;17/** Starting offset of the current struct/table. */18private object_start = 019/** List of offsets of all vtables. */20private vtables: number[] = []21/** For the current vector being built. */22private vector_num_elems = 023/** False omits default values from the serialized data */24private force_defaults = false;25
26private string_maps: Map<string | Uint8Array, number> | null = null;27private text_encoder = new TextEncoder();28
29/**30* Create a FlatBufferBuilder.
31*/
32constructor(opt_initial_size?: number) {33let initial_size: number;34
35if (!opt_initial_size) {36initial_size = 1024;37} else {38initial_size = opt_initial_size;39}40
41/**42* @type {ByteBuffer}
43* @private
44*/
45this.bb = ByteBuffer.allocate(initial_size);46this.space = initial_size;47}48
49
50clear(): void {51this.bb.clear();52this.space = this.bb.capacity();53this.minalign = 1;54this.vtable = null;55this.vtable_in_use = 0;56this.isNested = false;57this.object_start = 0;58this.vtables = [];59this.vector_num_elems = 0;60this.force_defaults = false;61this.string_maps = null;62}63
64/**65* In order to save space, fields that are set to their default value
66* don't get serialized into the buffer. Forcing defaults provides a
67* way to manually disable this optimization.
68*
69* @param forceDefaults true always serializes default values
70*/
71forceDefaults(forceDefaults: boolean): void {72this.force_defaults = forceDefaults;73}74
75/**76* Get the ByteBuffer representing the FlatBuffer. Only call this after you've
77* called finish(). The actual data starts at the ByteBuffer's current position,
78* not necessarily at 0.
79*/
80dataBuffer(): ByteBuffer {81return this.bb;82}83
84/**85* Get the bytes representing the FlatBuffer. Only call this after you've
86* called finish().
87*/
88asUint8Array(): Uint8Array {89return this.bb.bytes().subarray(this.bb.position(), this.bb.position() + this.offset());90}91
92/**93* Prepare to write an element of `size` after `additional_bytes` have been
94* written, e.g. if you write a string, you need to align such the int length
95* field is aligned to 4 bytes, and the string data follows it directly. If all
96* you need to do is alignment, `additional_bytes` will be 0.
97*
98* @param size This is the of the new element to write
99* @param additional_bytes The padding size
100*/
101prep(size: number, additional_bytes: number): void {102// Track the biggest thing we've ever aligned to.103if (size > this.minalign) {104this.minalign = size;105}106
107// Find the amount of alignment needed such that `size` is properly108// aligned after `additional_bytes`109const align_size = ((~(this.bb.capacity() - this.space + additional_bytes)) + 1) & (size - 1);110
111// Reallocate the buffer if needed.112while (this.space < align_size + size + additional_bytes) {113const old_buf_size = this.bb.capacity();114this.bb = Builder.growByteBuffer(this.bb);115this.space += this.bb.capacity() - old_buf_size;116}117
118this.pad(align_size);119}120
121pad(byte_size: number): void {122for (let i = 0; i < byte_size; i++) {123this.bb.writeInt8(--this.space, 0);124}125}126
127writeInt8(value: number): void {128this.bb.writeInt8(this.space -= 1, value);129}130
131writeInt16(value: number): void {132this.bb.writeInt16(this.space -= 2, value);133}134
135writeInt32(value: number): void {136this.bb.writeInt32(this.space -= 4, value);137}138
139writeInt64(value: bigint): void {140this.bb.writeInt64(this.space -= 8, value);141}142
143writeFloat32(value: number): void {144this.bb.writeFloat32(this.space -= 4, value);145}146
147writeFloat64(value: number): void {148this.bb.writeFloat64(this.space -= 8, value);149}150
151/**152* Add an `int8` to the buffer, properly aligned, and grows the buffer (if necessary).
153* @param value The `int8` to add the buffer.
154*/
155addInt8(value: number): void {156this.prep(1, 0);157this.writeInt8(value);158}159
160/**161* Add an `int16` to the buffer, properly aligned, and grows the buffer (if necessary).
162* @param value The `int16` to add the buffer.
163*/
164addInt16(value: number): void {165this.prep(2, 0);166this.writeInt16(value);167}168
169/**170* Add an `int32` to the buffer, properly aligned, and grows the buffer (if necessary).
171* @param value The `int32` to add the buffer.
172*/
173addInt32(value: number): void {174this.prep(4, 0);175this.writeInt32(value);176}177
178/**179* Add an `int64` to the buffer, properly aligned, and grows the buffer (if necessary).
180* @param value The `int64` to add the buffer.
181*/
182addInt64(value: bigint): void {183this.prep(8, 0);184this.writeInt64(value);185}186
187/**188* Add a `float32` to the buffer, properly aligned, and grows the buffer (if necessary).
189* @param value The `float32` to add the buffer.
190*/
191addFloat32(value: number): void {192this.prep(4, 0);193this.writeFloat32(value);194}195
196/**197* Add a `float64` to the buffer, properly aligned, and grows the buffer (if necessary).
198* @param value The `float64` to add the buffer.
199*/
200addFloat64(value: number): void {201this.prep(8, 0);202this.writeFloat64(value);203}204
205addFieldInt8(voffset: number, value: number, defaultValue: number|null): void {206if (this.force_defaults || value != defaultValue) {207this.addInt8(value);208this.slot(voffset);209}210}211
212addFieldInt16(voffset: number, value: number, defaultValue: number|null): void {213if (this.force_defaults || value != defaultValue) {214this.addInt16(value);215this.slot(voffset);216}217}218
219addFieldInt32(voffset: number, value: number, defaultValue: number|null): void {220if (this.force_defaults || value != defaultValue) {221this.addInt32(value);222this.slot(voffset);223}224}225
226addFieldInt64(voffset: number, value: bigint, defaultValue: bigint|null): void {227if (this.force_defaults || value !== defaultValue) {228this.addInt64(value);229this.slot(voffset);230}231}232
233addFieldFloat32(voffset: number, value: number, defaultValue: number|null): void {234if (this.force_defaults || value != defaultValue) {235this.addFloat32(value);236this.slot(voffset);237}238}239
240addFieldFloat64(voffset: number, value: number, defaultValue: number|null): void {241if (this.force_defaults || value != defaultValue) {242this.addFloat64(value);243this.slot(voffset);244}245}246
247addFieldOffset(voffset: number, value: Offset, defaultValue: Offset): void {248if (this.force_defaults || value != defaultValue) {249this.addOffset(value);250this.slot(voffset);251}252}253
254/**255* Structs are stored inline, so nothing additional is being added. `d` is always 0.
256*/
257addFieldStruct(voffset: number, value: Offset, defaultValue: Offset): void {258if (value != defaultValue) {259this.nested(value);260this.slot(voffset);261}262}263
264/**265* Structures are always stored inline, they need to be created right
266* where they're used. You'll get this assertion failure if you
267* created it elsewhere.
268*/
269nested(obj: Offset): void {270if (obj != this.offset()) {271throw new TypeError('FlatBuffers: struct must be serialized inline.');272}273}274
275/**276* Should not be creating any other object, string or vector
277* while an object is being constructed
278*/
279notNested(): void {280if (this.isNested) {281throw new TypeError('FlatBuffers: object serialization must not be nested.');282}283}284
285/**286* Set the current vtable at `voffset` to the current location in the buffer.
287*/
288slot(voffset: number): void {289if (this.vtable !== null)290this.vtable[voffset] = this.offset();291}292
293/**294* @returns Offset relative to the end of the buffer.
295*/
296offset(): Offset {297return this.bb.capacity() - this.space;298}299
300/**301* Doubles the size of the backing ByteBuffer and copies the old data towards
302* the end of the new buffer (since we build the buffer backwards).
303*
304* @param bb The current buffer with the existing data
305* @returns A new byte buffer with the old data copied
306* to it. The data is located at the end of the buffer.
307*
308* uint8Array.set() formally takes {Array<number>|ArrayBufferView}, so to pass
309* it a uint8Array we need to suppress the type check:
310* @suppress {checkTypes}
311*/
312static growByteBuffer(bb: ByteBuffer): ByteBuffer {313const old_buf_size = bb.capacity();314
315// Ensure we don't grow beyond what fits in an int.316if (old_buf_size & 0xC0000000) {317throw new Error('FlatBuffers: cannot grow buffer beyond 2 gigabytes.');318}319
320const new_buf_size = old_buf_size << 1;321const nbb = ByteBuffer.allocate(new_buf_size);322nbb.setPosition(new_buf_size - old_buf_size);323nbb.bytes().set(bb.bytes(), new_buf_size - old_buf_size);324return nbb;325}326
327/**328* Adds on offset, relative to where it will be written.
329*
330* @param offset The offset to add.
331*/
332addOffset(offset: Offset): void {333this.prep(SIZEOF_INT, 0); // Ensure alignment is already done.334this.writeInt32(this.offset() - offset + SIZEOF_INT);335}336
337/**338* Start encoding a new object in the buffer. Users will not usually need to
339* call this directly. The FlatBuffers compiler will generate helper methods
340* that call this method internally.
341*/
342startObject(numfields: number): void {343this.notNested();344if (this.vtable == null) {345this.vtable = [];346}347this.vtable_in_use = numfields;348for (let i = 0; i < numfields; i++) {349this.vtable[i] = 0; // This will push additional elements as needed350}351this.isNested = true;352this.object_start = this.offset();353}354
355/**356* Finish off writing the object that is under construction.
357*
358* @returns The offset to the object inside `dataBuffer`
359*/
360endObject(): Offset {361if (this.vtable == null || !this.isNested) {362throw new Error('FlatBuffers: endObject called without startObject');363}364
365this.addInt32(0);366const vtableloc = this.offset();367
368// Trim trailing zeroes.369let i = this.vtable_in_use - 1;370// eslint-disable-next-line no-empty371for (; i >= 0 && this.vtable[i] == 0; i--) {}372const trimmed_size = i + 1;373
374// Write out the current vtable.375for (; i >= 0; i--) {376// Offset relative to the start of the table.377this.addInt16(this.vtable[i] != 0 ? vtableloc - this.vtable[i] : 0);378}379
380const standard_fields = 2; // The fields below:381this.addInt16(vtableloc - this.object_start);382const len = (trimmed_size + standard_fields) * SIZEOF_SHORT;383this.addInt16(len);384
385// Search for an existing vtable that matches the current one.386let existing_vtable = 0;387const vt1 = this.space;388outer_loop:389for (i = 0; i < this.vtables.length; i++) {390const vt2 = this.bb.capacity() - this.vtables[i];391if (len == this.bb.readInt16(vt2)) {392for (let j = SIZEOF_SHORT; j < len; j += SIZEOF_SHORT) {393if (this.bb.readInt16(vt1 + j) != this.bb.readInt16(vt2 + j)) {394continue outer_loop;395}396}397existing_vtable = this.vtables[i];398break;399}400}401
402if (existing_vtable) {403// Found a match:404// Remove the current vtable.405this.space = this.bb.capacity() - vtableloc;406
407// Point table to existing vtable.408this.bb.writeInt32(this.space, existing_vtable - vtableloc);409} else {410// No match:411// Add the location of the current vtable to the list of vtables.412this.vtables.push(this.offset());413
414// Point table to current vtable.415this.bb.writeInt32(this.bb.capacity() - vtableloc, this.offset() - vtableloc);416}417
418this.isNested = false;419return vtableloc as Offset;420}421
422/**423* Finalize a buffer, poiting to the given `root_table`.
424*/
425finish(root_table: Offset, opt_file_identifier?: string, opt_size_prefix?: boolean): void {426const size_prefix = opt_size_prefix ? SIZE_PREFIX_LENGTH : 0;427if (opt_file_identifier) {428const file_identifier = opt_file_identifier;429this.prep(this.minalign, SIZEOF_INT +430FILE_IDENTIFIER_LENGTH + size_prefix);431if (file_identifier.length != FILE_IDENTIFIER_LENGTH) {432throw new TypeError('FlatBuffers: file identifier must be length ' +433FILE_IDENTIFIER_LENGTH);434}435for (let i = FILE_IDENTIFIER_LENGTH - 1; i >= 0; i--) {436this.writeInt8(file_identifier.charCodeAt(i));437}438}439this.prep(this.minalign, SIZEOF_INT + size_prefix);440this.addOffset(root_table);441if (size_prefix) {442this.addInt32(this.bb.capacity() - this.space);443}444this.bb.setPosition(this.space);445}446
447/**448* Finalize a size prefixed buffer, pointing to the given `root_table`.
449*/
450finishSizePrefixed(this: Builder, root_table: Offset, opt_file_identifier?: string): void {451this.finish(root_table, opt_file_identifier, true);452}453
454/**455* This checks a required field has been set in a given table that has
456* just been constructed.
457*/
458requiredField(table: Offset, field: number): void {459const table_start = this.bb.capacity() - table;460const vtable_start = table_start - this.bb.readInt32(table_start);461const ok = field < this.bb.readInt16(vtable_start) &&462this.bb.readInt16(vtable_start + field) != 0;463
464// If this fails, the caller will show what field needs to be set.465if (!ok) {466throw new TypeError('FlatBuffers: field ' + field + ' must be set');467}468}469
470/**471* Start a new array/vector of objects. Users usually will not call
472* this directly. The FlatBuffers compiler will create a start/end
473* method for vector types in generated code.
474*
475* @param elem_size The size of each element in the array
476* @param num_elems The number of elements in the array
477* @param alignment The alignment of the array
478*/
479startVector(elem_size: number, num_elems: number, alignment: number): void {480this.notNested();481this.vector_num_elems = num_elems;482this.prep(SIZEOF_INT, elem_size * num_elems);483this.prep(alignment, elem_size * num_elems); // Just in case alignment > int.484}485
486/**487* Finish off the creation of an array and all its elements. The array must be
488* created with `startVector`.
489*
490* @returns The offset at which the newly created array
491* starts.
492*/
493endVector(): Offset {494this.writeInt32(this.vector_num_elems);495return this.offset();496}497
498/**499* Encode the string `s` in the buffer using UTF-8. If the string passed has
500* already been seen, we return the offset of the already written string
501*
502* @param s The string to encode
503* @return The offset in the buffer where the encoded string starts
504*/
505createSharedString(s: string | Uint8Array): Offset {506if (!s) { return 0 }507
508if (!this.string_maps) {509this.string_maps = new Map();510}511
512if (this.string_maps.has(s)) {513return this.string_maps.get(s) as Offset514}515const offset = this.createString(s)516this.string_maps.set(s, offset)517return offset518}519
520/**521* Encode the string `s` in the buffer using UTF-8. If a Uint8Array is passed
522* instead of a string, it is assumed to contain valid UTF-8 encoded data.
523*
524* @param s The string to encode
525* @return The offset in the buffer where the encoded string starts
526*/
527createString(s: string | Uint8Array | null | undefined): Offset {528if (s === null || s === undefined) {529return 0;530}531
532let utf8: string | Uint8Array | number[];533if (s instanceof Uint8Array) {534utf8 = s;535} else {536utf8 = this.text_encoder.encode(s);537}538
539this.addInt8(0);540this.startVector(1, utf8.length, 1);541this.bb.setPosition(this.space -= utf8.length);542this.bb.bytes().set(utf8, this.space);543return this.endVector();544}545
546/**547* Create a byte vector.
548*
549* @param v The bytes to add
550* @returns The offset in the buffer where the byte vector starts
551*/
552createByteVector(v: Uint8Array | null | undefined): Offset {553if (v === null || v === undefined) {554return 0;555}556
557this.startVector(1, v.length, 1);558this.bb.setPosition(this.space -= v.length);559this.bb.bytes().set(v, this.space);560return this.endVector();561}562
563/**564* A helper function to pack an object
565*
566* @returns offset of obj
567*/
568createObjectOffset(obj: string | IGeneratedObject | null): Offset {569if(obj === null) {570return 0571}572
573if(typeof obj === 'string') {574return this.createString(obj);575} else {576return obj.pack(this);577}578}579
580/**581* A helper function to pack a list of object
582*
583* @returns list of offsets of each non null object
584*/
585createObjectOffsetList(list: (string | IGeneratedObject)[]): Offset[] {586const ret: number[] = [];587
588for(let i = 0; i < list.length; ++i) {589const val = list[i];590
591if(val !== null) {592ret.push(this.createObjectOffset(val));593} else {594throw new TypeError(595'FlatBuffers: Argument for createObjectOffsetList cannot contain null.');596}597}598
599return ret;600}601
602createStructOffsetList(list: (string | IGeneratedObject)[], startFunc: (builder: Builder, length: number) => void): Offset {603startFunc(this, list.length);604this.createObjectOffsetList(list.slice().reverse());605return this.endVector();606}607}608