symbol-processor/src/struct.ts

324 lines
9.9 KiB
TypeScript

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 += 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();
}
// 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;
}
}