import { INDENT, MIN_SIZE, toUpperSnakeCase, formatType, STRUCTURE_FILES } from './common'; import { isCppAllowed } from './map'; 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[]; #size: number | null; readonly #dependencies: string[]; readonly #staticProperties: StaticProperty[]; // Constructor constructor(name: string) { this.#name = name; this.#methods = []; this.#properties = []; this.#vtable = null; this.#size = null; this.#dependencies = []; this.#staticProperties = []; } // 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.addProperty(this.#vtable); } } // Setters setSize(size: number) { this.#size = size; } // Getters #roundSize(size: number) { const alignment = this.getAlignment(); return Math.ceil(size / alignment) * alignment; } getSize() { let size; if (this.#size !== null) { size = this.#size; } else { size = MIN_SIZE; for (const property of this.#properties) { const newSize = property.propertyOffset() + property.propertySize(); if (newSize > size) { size = newSize; } } } 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) { this.#ensureVTable(); this.#vtable!.setSize(size); } setVTableAddress(address: number) { this.#ensureVTable(); this.#vtable!.setAddress(address); } // Check #check() { // Sort Properties this.#properties.sort((a, b) => a.propertyOffset() - b.propertyOffset()); // Check Size const size = this.getSize(); if (this.#size !== null) { // Check Alignment if (size !== this.#size) { throw new Error('Size Misaligned'); } // Check If Size Is Too Small const lastProperty = this.#properties[this.#properties.length - 1]; if (lastProperty) { let realSize = lastProperty.propertyOffset() + lastProperty.propertySize(); realSize = this.#roundSize(realSize); if (realSize > this.#size) { throw new Error(`Structure Size Too Small: ${this.#size}`); } } } } // Generate Header generate() { let out = ''; // Check this.#check(); // Static Properties for (const property of this.#staticProperties) { out += `extern ${property.generateDefinition()}`; } // Methods for (const method of this.#methods) { out += method.generateTypedef(); out += `extern ${method.generateDefinition()}`; } // Early Exit For Undefined Structures if (this.#properties.length === 0 && this.#size === null) { return out; } // VTable if (this.#vtable !== null) { out += this.#vtable.generate(); } // Structure out += `struct ${this.#name} {\n`; for (let i = 0; i <= this.#properties.length; i++) { const property = this.#properties[i]; // Padding const lastProperty = this.#properties[i - 1]; let neededPadding = 0; if (i === 0) { // Start Of Structure Padding if (property) { neededPadding = property.propertyOffset(); } else if (this.#properties.length === 0) { if (this.#size !== null) { neededPadding = this.#size; } else { neededPadding = MIN_SIZE; } } } else if (i === this.#properties.length) { // End Of Structure Padding if (this.#size !== null && lastProperty) { const realSize = lastProperty.propertyOffset() + lastProperty.propertySize(); neededPadding = this.#size - realSize; } } else { // Inner Structure Padding if (property && lastProperty) { const realSize = lastProperty.propertyOffset() + lastProperty.propertySize(); neededPadding = property.propertyOffset() - realSize; } } if (neededPadding > 0) { out += `${INDENT}uchar padding${i}[${neededPadding}];\n`; } else if (neededPadding < 0) { throw new Error('Overlapping properties detected!'); } // Property if (property) { // 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 = isCppAllowed() ? 'static_assert' : '_Static_assert'; for (let i = 0; i < this.#properties.length; i++) { const property = this.#properties[i]!; const name = property.propertyName(); const offset = property.propertyOffset(); out += `${assertFunction}(offsetof(${this.#name}, ${name}) == ${offset}, "Invalid Offset");\n`; } // Sanity Check Size const size = this.getSize(); out += `#define ${toUpperSnakeCase(this.#name)}_SIZE ${size}\n`; out += `${assertFunction}(sizeof (${this.#name}) == ${toUpperSnakeCase(this.#name)}_SIZE, "Invalid Structure Size");\n`; if (this.#size === null) { // Hide Structure Size As The Real Size Is Unknown out += `#undef ${toUpperSnakeCase(this.#name)}_SIZE\n`; } // Allocation Function if (this.#size !== null) { 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()} = (${property.getType()}) ${property.address};\n`; declarations += property.generateDefinition(); } // Methods for (const method of this.#methods) { init += `${INDENT}${method.getName()} = (${method.getType()}) ${method.address};\n`; declarations += method.generateDefinition(); } // VTable if (this.#vtable !== null) { const vtable = this.#vtable.generateCode(); declarations += vtable.declarations; init += vtable.init; } // Allocation Function if (this.#size !== null) { declarations += `${this.#name} *alloc_${this.#name}() {\n`; declarations += `${INDENT}return (${this.#name} *) ::operator new(${this.#size});\n`; declarations += '}\n'; } // Return return {functions: declarations, init}; } }