import { INDENT, MIN_SIZE, formatType, STRUCTURE_FILES, toHex, getAssertFunction, assertSize, stripArrayData, Size } from './common'; import { Method } from './method'; import { Property, StaticProperty } from './property'; import { VTable } from './vtable'; export class Struct { readonly #name: string; #vtable: VTable | null; readonly #methods: Method[]; readonly #properties: Property[]; readonly #size: Size; readonly #dependencies: string[]; readonly #staticProperties: StaticProperty[]; #directParent: string | null; #vtableDestructorOffset: number; // Constructor constructor(name: string) { this.#name = name; this.#methods = []; this.#properties = []; this.#vtable = null; this.#size = new Size(false); this.#dependencies = []; this.#staticProperties = []; this.#directParent = null; this.#vtableDestructorOffset = 0; } // Dependencies #addDependency(dependency: string) { this.#dependencies.push(dependency); } getDependencies() { return this.#dependencies; } // Ensure VTable Exists ensureVTable() { if (this.#vtable === null) { this.#vtable = new VTable(this.#name, this.#vtableDestructorOffset); this.addProperty(this.#vtable); } } // Set VTable Destructor Offset setVTableDestructorOffset(offset: number) { if (this.#vtable) { throw new Error('VTable Already Created'); } this.#vtableDestructorOffset = offset; } // Setters setSize(size: number, isExact: boolean) { this.#size.set(size, isExact); } // Getters #roundSize(size: number) { const alignment = this.getAlignment(); return Math.ceil(size / alignment) * alignment; } #getRealSize() { // Get Size Computed Purely From Properties let size = MIN_SIZE; for (const property of this.#properties) { const newSize = property.propertyOffset() + property.propertySize(); if (newSize > size) { size = newSize; } } return size; } getSize(round: boolean) { let size; if (this.#size.isExact()) { // Exact Size Is Specified size = this.#size.get(); } else { // Specified Size Is A Lower Bound size = this.#getRealSize(); if (this.#size.has()) { const specifiedSize = this.#size.get(); if (specifiedSize > size) { size = specifiedSize; } } } if (round) { size = this.#roundSize(size); } return size; } getName() { return this.#name; } getAlignment() { let alignment = 1; for (const property of this.#properties) { const x = property.propertyAlignment(); if (x > alignment) { alignment = x; } } return alignment; } // Add Method addMethod(method: Method, isVirtual: boolean) { if (method.self !== this.#name) { throw new Error(); } if (isVirtual) { this.ensureVTable(); this.#vtable!.add(method); } else { this.#methods.push(method); } } // Add Property addProperty(property: Property) { this.#properties.push(property); // Add Dependency If Needed const type = property.propertyType(); if (type in STRUCTURE_FILES) { this.#addDependency(type); } } // Add Static Property addStaticProperty(property: StaticProperty) { this.#staticProperties.push(property); } // Configure VTable setVTableSize(size: number, isExact: boolean) { this.ensureVTable(); this.#vtable!.setSize(size, isExact); } setVTableAddress(address: number) { this.ensureVTable(); this.#vtable!.setAddress(address); } // Check #check() { // Sort Properties this.#properties.sort((a, b) => a.propertyOffset() - b.propertyOffset()); // Check Size if (this.#size.isExact()) { const size = this.getSize(true); // Check Alignment const specifiedSize = this.#size.get()!; if (size !== specifiedSize) { throw new Error('Size Misaligned'); } // Check If Size Is Too Small let realSize = this.#getRealSize(); realSize = this.#roundSize(realSize); if (realSize > specifiedSize) { throw new Error(`Structure Size Too Small: ${toHex(specifiedSize)}`); } } } // Compute Padding Between Properties #computePadding(a: Property | null, b: Property | null) { // Null A = Start Of Structure // Null B = End Of Structure let neededPadding = 0; const size = this.getSize(true); if (a === null) { // Start Of Structure Padding if (b !== null) { neededPadding = b.propertyOffset(); } else { // Both A And B Are Null neededPadding = size; } } else if (b === null) { // End Of Structure Padding const realSize = this.#getRealSize(); const realRoundedSize = this.#roundSize(realSize); if (realRoundedSize !== size) { neededPadding = size - realSize; } } else { // Inner Structure Padding const realSizeSoFar = a.propertyOffset() + a.propertySize(); neededPadding = b.propertyOffset() - realSizeSoFar; } if (neededPadding < 0) { throw new Error('Overlapping Properties Detected!'); } return neededPadding; } // Generate Header generate() { let out = ''; // Check this.#check(); // Static Properties for (const property of this.#staticProperties) { out += `extern ${property.generateDefinition()}`; out += property.generateMacro(); } // Methods for (const method of this.#methods) { out += method.generateTypedef(); out += `extern ${method.generateDefinition()}`; out += `extern ${method.generateDefinition('_unedited')}`; out += method.generateNewMethodTest(this.#directParent, '', ''); } // VTable if (this.#vtable !== null) { out += this.#vtable.generate(this.#directParent); } // Structure out += `struct ${this.#name} {\n`; for (let i = 0; i <= this.#properties.length; i++) { const property = i < this.#properties.length ? this.#properties[i]! : null; // Padding const lastProperty = (i >= 1 && (i - 1) < this.#properties.length) ? this.#properties[i - 1]! : null; const neededPadding = this.#computePadding(lastProperty, property); if (neededPadding > 0) { out += `${INDENT}uchar padding${i}[${neededPadding}];\n`; } // Property if (property !== null) { // Check Offset const offset = property.propertyOffset(); const alignment = property.propertyAlignment(); if ((offset % alignment) !== 0) { throw new Error('Misaligned Property Offset'); } // Add const type = formatType(property.propertyType()); out += `${INDENT}${type}${property.propertyName()};\n`; } } out += `};\n`; // Sanity Check Offsets const assertFunction = getAssertFunction(); for (let i = 0; i < this.#properties.length; i++) { const property = this.#properties[i]!; const name = stripArrayData(property.propertyName()); const offset = property.propertyOffset(); out += `${assertFunction}(offsetof(${this.#name}, ${name}) == ${toHex(offset)}, "Invalid Offset");\n`; } // Sanity Check Size const size = this.getSize(true); const isSizeDefined = this.#size.isExact(); out += assertSize(this.#name, size, isSizeDefined); // Allocation Function if (isSizeDefined) { out += `${this.#name} *alloc_${this.#name}();\n`; } // Return return out; } // Generate Compiled Code generateCode() { let declarations = ''; let init = ''; // Check this.#check(); // Static Properties for (const property of this.#staticProperties) { init += `${INDENT}${property.getName()}_pointer = (${property.getPointerType()}) ${toHex(property.address)};\n`; declarations += property.generateDefinition(); } // Methods for (const method of this.#methods) { init += `${INDENT}${method.getName()} = (${method.getType()}) ${toHex(method.address)};\n`; declarations += method.generateDefinition(); init += `${INDENT}${method.getName()}_unedited = (${method.getType()}) ${toHex(method.address)};\n`; declarations += method.generateDefinition('_unedited'); } // VTable if (this.#vtable !== null) { const vtable = this.#vtable.generateCode(); declarations += vtable.declarations; init += vtable.init; } // Allocation Function if (this.#size.isExact()) { declarations += `${this.#name} *alloc_${this.#name}() {\n`; declarations += `${INDENT}return new ${this.#name};\n`; declarations += '}\n'; } // Return return {functions: declarations, init}; } // Get Method Symbols getMethodSymbols() { const ret = []; for (const method of this.#methods) { ret.push(method.getName()); } return ret; } // Set Direct Parent (Used For "New Method" Testing) setDirectParent(directParent: string) { this.#directParent = directParent; } }