diff --git a/src/common.ts b/src/common.ts index 40650bb..a79439a 100644 --- a/src/common.ts +++ b/src/common.ts @@ -119,4 +119,42 @@ export function getSizeMultiplierFromArrayData(propertyName: string) { } // Return return multiplier; +} +export function getSelfArg(type: string) { + return `${type} *self`; +} +export class Size { + #size: number | null; + #isExact: boolean; + readonly #pointerAligned: boolean; + + constructor(pointerAligned: boolean) { + this.#size = null; + this.#isExact = false; + this.#pointerAligned = pointerAligned; + } + + get() { + return this.#size; + } + set(size: number, isExact: boolean) { + if (size < MIN_SIZE) { + throw new Error(`Size Too Small: ${toHex(size)}`); + } + if (this.#isExact) { + throw new Error('Cannot Change Size After Being Set By Base Structure'); + } + if (this.#size !== null && size < this.#size) { + throw new Error('Cannot Decrease VTable Size'); + } + this.#size = size; + this.#isExact = isExact; + if (this.#pointerAligned && (this.#size % POINTER_SIZE) !== 0) { + throw new Error(`Invalid Size: ${toHex(this.#size)}`); + } + } + // Whether This Is An Exact Limit Or Just A Lower Bound + isExact() { + return this.#isExact; + } } \ No newline at end of file diff --git a/src/loader.ts b/src/loader.ts index af19d5f..f7e09c9 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -1,4 +1,4 @@ -import { COMMENT, EXTENSION, parseTypeAndName, readDefinition, safeParseInt, syntaxError } from './common'; +import { COMMENT, EXTENSION, getSelfArg, parseTypeAndName, readDefinition, safeParseInt, syntaxError } from './common'; import { isCppAllowed } from './map'; import { Method } from './method'; import { SimpleProperty, StaticProperty } from './property'; @@ -44,7 +44,7 @@ function parseMethod(args: string, self: string, insertSelfArg: boolean) { syntaxError('Invalid Method Arguments'); } if (insertSelfArg) { - let selfArg = `(${self} *self`; + let selfArg = `(${getSelfArg(self)}`; if (methodArgs !== '()') { selfArg += ', '; } @@ -113,16 +113,12 @@ export function load(target: Struct, name: string, isExtended: boolean) { } case 'size': { // Set Size - if (!isExtended) { - target.setSize(safeParseInt(args)); - } + target.setSize(safeParseInt(args), !isExtended); break; } case 'vtable-size': { // Set VTable Size - if (!isExtended) { - target.setVTableSize(safeParseInt(args)); - } + target.setVTableSize(safeParseInt(args), !isExtended); break; } case 'vtable': { diff --git a/src/struct.ts b/src/struct.ts index a27ea0f..44c0963 100644 --- a/src/struct.ts +++ b/src/struct.ts @@ -1,4 +1,4 @@ -import { INDENT, MIN_SIZE, formatType, STRUCTURE_FILES, toHex, getAssertFunction, assertSize, stripArrayData } from './common'; +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'; @@ -8,7 +8,7 @@ export class Struct { #vtable: VTable | null; readonly #methods: Method[]; readonly #properties: Property[]; - #size: number | null; + readonly #size: Size; readonly #dependencies: string[]; readonly #staticProperties: StaticProperty[]; @@ -18,7 +18,7 @@ export class Struct { this.#methods = []; this.#properties = []; this.#vtable = null; - this.#size = null; + this.#size = new Size(false); this.#dependencies = []; this.#staticProperties = []; } @@ -40,28 +40,36 @@ export class Struct { } // Setters - setSize(size: number) { - this.#size = size; + setSize(size: number, isExact: boolean) { + this.#size.set(size, isExact); } // Getters #roundSize(size: number) { const alignment = this.getAlignment(); return Math.ceil(size / alignment) * alignment; } - getSize(round: boolean) { - if (this.#isUndefined()) { - return 0; + #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 !== null) { - size = this.#size; + if (this.#size.isExact()) { + // Exact Size Is Specified + size = this.#size.get()!; } else { - size = MIN_SIZE; - for (const property of this.#properties) { - const newSize = property.propertyOffset() + property.propertySize(); - if (newSize > size) { - size = newSize; - } + // Specified Size Is A Lower Bound + size = this.#getRealSize(); + const specifiedSize = this.#size.get(); + if (specifiedSize !== null && specifiedSize > size) { + size = specifiedSize; } } if (round) { @@ -110,40 +118,33 @@ export class Struct { } // Configure VTable - setVTableSize(size: number) { + setVTableSize(size: number, isExact: boolean) { this.#ensureVTable(); - this.#vtable!.setSize(size); + this.#vtable!.setSize(size, isExact); } setVTableAddress(address: number) { this.#ensureVTable(); this.#vtable!.setAddress(address); } - // Check If Structure Is Undefined - #isUndefined() { - return this.#properties.length === 0 && this.#size === null; - } - // Check #check() { // Sort Properties this.#properties.sort((a, b) => a.propertyOffset() - b.propertyOffset()); // Check Size - const size = this.getSize(true); - if (this.#size !== null) { + if (this.#size.isExact()) { + const size = this.getSize(true); // Check Alignment - if (size !== this.#size) { + const specifiedSize = this.#size.get()!; + if (size !== specifiedSize) { 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}`); - } + let realSize = this.#getRealSize(); + realSize = this.#roundSize(realSize); + if (realSize > specifiedSize) { + throw new Error(`Structure Size Too Small: ${toHex(specifiedSize)}`); } } } @@ -153,26 +154,21 @@ export class Struct { // 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 - if (this.#size !== null) { - neededPadding = this.#size; - } else { - neededPadding = MIN_SIZE; - } + neededPadding = size; } } else if (b === null) { // End Of Structure Padding - if (this.#size !== null) { - const realSize = a.propertyOffset() + a.propertySize(); - const realRoundedSize = this.#roundSize(realSize); - if (realRoundedSize !== this.#size) { - neededPadding = this.#size - realSize; - } + const realSize = this.#getRealSize(); + const realRoundedSize = this.#roundSize(realSize); + if (realRoundedSize !== size) { + neededPadding = size - realSize; } } else { // Inner Structure Padding @@ -180,7 +176,7 @@ export class Struct { neededPadding = b.propertyOffset() - realSizeSoFar; } if (neededPadding < 0) { - throw new Error('Overlapping properties detected!'); + throw new Error('Overlapping Properties Detected!'); } return neededPadding; } @@ -204,11 +200,6 @@ export class Struct { out += `extern ${method.generateDefinition()}`; } - // Early Exit For Undefined Structures - if (this.#isUndefined()) { - return out; - } - // VTable if (this.#vtable !== null) { out += this.#vtable.generate(); @@ -252,7 +243,7 @@ export class Struct { // Sanity Check Size const size = this.getSize(true); - const isSizeDefined = this.#size !== null; + const isSizeDefined = this.#size.isExact(); out += assertSize(this.#name, size, isSizeDefined); // Allocation Function @@ -292,7 +283,7 @@ export class Struct { } // Allocation Function - if (this.#size !== null) { + if (this.#size.isExact()) { declarations += `${this.#name} *alloc_${this.#name}() {\n`; declarations += `${INDENT}return new ${this.#name};\n`; declarations += '}\n'; diff --git a/src/vtable.ts b/src/vtable.ts index 5a93f70..002b2a6 100644 --- a/src/vtable.ts +++ b/src/vtable.ts @@ -1,19 +1,24 @@ -import { INDENT, POINTER_SIZE, assertSize, toHex } from './common'; +import { INDENT, POINTER_SIZE, Size, assertSize, getSelfArg, toHex } from './common'; import { Method } from './method'; import { Property } from './property'; export class VTable implements Property { readonly #name: string; #address: number | null; - #size: number | null; + readonly #size: Size; readonly #methods: Method[]; // Constructor constructor(name: string) { this.#name = name; this.#address = null; - this.#size = null; + this.#size = new Size(true); this.#methods = []; + // Add Destructors (https://stackoverflow.com/a/17960941) + const destructor_return = `${name} *`; + const destructor_args = `(${getSelfArg(name)})`; + this.add(new Method(name, 'destructor_complete', destructor_return, destructor_args, 0x0)); + this.add(new Method(name, 'destructor_deleting', destructor_return, destructor_args, 0x4)); } // Property Information @@ -37,11 +42,8 @@ export class VTable implements Property { setAddress(address: number) { this.#address = address; } - setSize(size: number) { - this.#size = size; - if ((this.#size % POINTER_SIZE) !== 0) { - throw new Error(`Invalid VTable Size: ${this.#size}`); - } + setSize(size: number, isExact: boolean) { + this.#size.set(size, isExact); } // Add To VTable @@ -49,14 +51,13 @@ export class VTable implements Property { // Check Offset const offset = method.address; if ((offset % POINTER_SIZE) !== 0) { - throw new Error(`Invalid VTable Offset: ${offset}`); - } - // Check Size - if (this.#size !== null && (offset + POINTER_SIZE) > this.#size) { - throw new Error(`VTable Offset Too Large: ${offset}`); + throw new Error(`Invalid VTable Offset: ${toHex(offset)}`); } // Add const index = offset / POINTER_SIZE; + if (this.#methods[index]) { + throw new Error(`Duplicate Virtual Method At Offset: ${toHex(offset)}`); + } this.#methods[index] = method; } @@ -68,12 +69,15 @@ export class VTable implements Property { // Check #check() { // Check Size - if (this.#size !== null) { - const maxMethodCount = this.#size / POINTER_SIZE; - if (maxMethodCount < this.#methods.length) { - throw new Error(`VTable Size Too Small: ${this.#size}`); + const size = this.#size.get(); + if (size !== null) { + const maxMethodCount = size / POINTER_SIZE; + if (this.#size.isExact() && maxMethodCount < this.#methods.length) { + throw new Error(`VTable Size Too Small: ${toHex(size)}`); + } + if (maxMethodCount > this.#methods.length) { + this.#methods.length = maxMethodCount; } - this.#methods.length = maxMethodCount; } } @@ -108,8 +112,9 @@ export class VTable implements Property { out += `};\n`; // Sanity Check Size - const isSizeDefined = this.#size !== null; - const size = isSizeDefined ? this.#size! : (this.#methods.length * POINTER_SIZE); + const rawSize = this.#size.get(); + const isSizeDefined = this.#size.isExact(); + const size = isSizeDefined ? rawSize! : (this.#methods.length * POINTER_SIZE); out += assertSize(this.#getName(), size, isSizeDefined); // Pointers @@ -164,7 +169,7 @@ export class VTable implements Property { } // Duplication Method - if (this.#size !== null) { + if (this.#size.isExact()) { declarations += `${this.#getName()} *dup_${this.#getName()}(${this.#getName()} *vtable) {\n`; declarations += `${INDENT}${this.#getName()} *obj = new ${this.#getName()};\n`; declarations += `${INDENT}if (obj == NULL) {\n`;