From e99239fecf00c2910d9c8f48cb3e40f7919b67fe Mon Sep 17 00:00:00 2001 From: TheBrokenRail Date: Thu, 4 Jan 2024 15:27:02 -0500 Subject: [PATCH] Initial Prototype --- .eslintignore | 2 + .eslintrc | 36 ++++++ .gitignore | 5 + .vscode/settings.json | 5 + LICENSE | 21 ++++ package.json | 22 ++++ src/common.ts | 49 ++++++++ src/index.ts | 176 +++++++++++++++++++++++++++ src/loader.ts | 229 ++++++++++++++++++++++++++++++++++ src/map.ts | 52 ++++++++ src/method.ts | 37 ++++++ src/property.ts | 129 ++++++++++++++++++++ src/struct.ts | 277 ++++++++++++++++++++++++++++++++++++++++++ src/vtable.ts | 176 +++++++++++++++++++++++++++ tsconfig.json | 13 ++ 15 files changed, 1229 insertions(+) create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 package.json create mode 100644 src/common.ts create mode 100644 src/index.ts create mode 100644 src/loader.ts create mode 100644 src/map.ts create mode 100644 src/method.ts create mode 100644 src/property.ts create mode 100644 src/struct.ts create mode 100644 src/vtable.ts create mode 100644 tsconfig.json diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..7d5b7a9 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +/build +/node_modules diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..e15abd9 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,36 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "rules": { + "no-constant-condition": [ + "error", + { + "checkLoops": false + } + ], + "quotes": [ + "error", + "single", + { + "allowTemplateLiterals": true + } + ], + "semi": "error", + "indent": [ + "error", + 4, + { + "SwitchCase": 1 + } + ], + "eqeqeq": "error" + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c1cb26d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/build +/package-lock.json +/node_modules +/out +/symbols diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bd23813 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "vtable" + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..938a0d8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 TheBrokenRail + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package.json b/package.json new file mode 100644 index 0000000..d91ade3 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "symbol-processor", + "version": "1.0.0", + "description": "", + "main": "build/index.js", + "scripts": { + "start": "tsc && node build/index.js", + "lint": "eslint . --ext .ts" + }, + "author": "TheBrokenRail", + "license": "MIT", + "devDependencies": { + "@tsconfig/node-lts": "^18.12.4", + "@tsconfig/strictest": "^2.0.1", + "@types/node": "^20.5.6", + "@typescript-eslint/eslint-plugin": "^6.4.1", + "@typescript-eslint/parser": "^6.4.1", + "eslint": "^8.48.0", + "openapi-types": "^12.1.3", + "typescript": "^5.2.2" + } +} diff --git a/src/common.ts b/src/common.ts new file mode 100644 index 0000000..36eb92d --- /dev/null +++ b/src/common.ts @@ -0,0 +1,49 @@ +import * as fs from 'node:fs'; + +export const INDENT = ' '; +export const POINTER_SIZE = 4; +export const EXTENSION = '.def'; +export function readDefinition(name: string) { + const file = `${__dirname}/../symbols/${name}${EXTENSION}`; + return fs.readFileSync(file, {encoding: 'utf8'}); +} +export function parseTypeAndName(parts: string[]) { + let type = parts[0]!; + let name = parts[1]!; + const index = name.lastIndexOf('*'); + if (index !== -1) { + type += ' ' + name.substring(0, index + 1); + name = name.substring(index + 1); + } + return {type, name}; +} +export const MIN_SIZE = 1; +export function toUpperSnakeCase(str: string) { + let wasUpper = false; + let nextIsUpper = false; + let out = ''; + for (let i = 0; i < str.length; i++) { + let character = str.charAt(i); + if (character === '_') { + wasUpper = false; + nextIsUpper = true; + continue; + } + const isUpper = character === character.toUpperCase() || nextIsUpper; + nextIsUpper = false; + character = character.toUpperCase(); + if (isUpper && i > 0 && !wasUpper) { + out += '_'; + } + out += character; + wasUpper = isUpper; + } + return out; +} +export function formatType(type: string) { + if (!type.endsWith('*')) { + type += ' '; + } + return type; +} +export const COMMENT = '//'; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..7b0083a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,176 @@ +import * as fs from 'node:fs'; +import { EXTENSION, INDENT } from './common'; +import { getStructure, setCppAllowed } from './map'; + +// Output Directory +let outputDirectory = process.argv[2]; +if (!outputDirectory) { + outputDirectory = `${__dirname}/../out`; +} +fs.rmSync(outputDirectory, {force: true, recursive: true}); +fs.mkdirSync(outputDirectory, {recursive: true}); + +// Main +function loadSymbols() { + // Load + const files = fs.readdirSync(`${__dirname}/../symbols`); + const structureObjects = []; + for (const file of files) { + if (file.endsWith(EXTENSION)) { + // Get Name + const parts = file.split('/'); + let name = parts[parts.length - 1]!; + name = name.substring(0, name.length - EXTENSION.length); + // Structures + structureObjects.push(getStructure(name)); + } + } + // Sort + structureObjects.sort((a, b) => { + if (a.getName() > b.getName()) { + return 1; + } else if (a.getName() < b.getName()) { + return -1; + } else { + return 0; + } + }); + // Return + return structureObjects; +} +function makeHeader(output: string, allowCpp: boolean) { + // Set Mode + setCppAllowed(allowCpp); + + // Load Symbols + const structureObjects = loadSymbols(); + + // Forward Declarations + let forwardDeclarations = '// Forward Declarations\n'; + for (const structure of structureObjects) { + const name = structure.getName(); + forwardDeclarations += `typedef struct ${name} ${name};\n`; + } + + // Sort Structures By Dependency + while (true) { + let valid = true; + // Loop Through Structures + for (const structure of structureObjects) { + // Loop Through Dependencies + const currentIndex = structureObjects.indexOf(structure); + for (const dependency of structure.getDependencies()) { + // Compare Current And Dependency Index + const obj = getStructure(dependency); + const dependencyIndex = structureObjects.indexOf(obj); + if (dependencyIndex > currentIndex) { + // Dependency Must Be Moved + structureObjects.splice(dependencyIndex, 1); + structureObjects.splice(currentIndex, 0, obj); + valid = false; + break; + } + } + // Check If Any Changes Were Made + if (!valid) { + break; + } + } + // Check If Any Changes Were Made + if (valid) { + break; + } + } + + // Generate Code + let structures = ''; + for (const structure of structureObjects) { + const name = structure.getName(); + structures += `\n// ${name}\n`; + try { + structures += structure.generate(); + } catch (e) { + console.log(`Error Generating Header: ${name}: ${e instanceof Error ? e.stack : e}`); + process.exit(1); + } + } + + // Write + let result = ''; + result += '#pragma once\n'; + result += '\n'; + result += forwardDeclarations; + result += '\n// Init\n'; + result += 'void init_symbols();'; + result += '\n// Extra Definitions\n'; + result += fs.readFileSync(`${__dirname}/../symbols/extra.h`, {encoding: 'utf8'}).trim() + '\n'; + result += structures; + fs.writeFileSync(output, result); +} + +// Run +makeHeader(`${outputDirectory}/minecraft_c.h`, false); +makeHeader(`${outputDirectory}/minecraft_cpp.h`, true); + +// Generate Compiled Code +function makeCompiledCode(output: string) { + // Set Mode + setCppAllowed(true); + // Load Symbols + const structureObjects = loadSymbols(); + + // Generate + let declarations = ''; + let init = ''; + for (const structure of structureObjects) { + const name = structure.getName(); + declarations += `\n// ${name}\n`; + init += `\n${INDENT}// ${name}\n`; + try { + const code = structure.generateCode(); + declarations += code.functions; + init += code.init; + } catch (e) { + console.log(`Error Generating Code: ${name}: ${e instanceof Error ? e.stack : e}`); + process.exit(1); + } + } + + // Write + let result = ''; + result += '#include "minecraft.h"\n'; + result += '\n// Init\n'; + result += 'void init_symbols() {'; + result += init; + result += '}\n'; + result += declarations; + fs.writeFileSync(output, result); +} +makeCompiledCode(`${outputDirectory}/minecraft.cpp`); + +// Create Main Header +function makeMainHeader(output: string) { + let result = ''; + result += '#pragma once\n'; + result += '#pragma GCC diagnostic push\n'; + result += '#pragma GCC diagnostic ignored "-Wunused-variable"\n'; + result += '#pragma GCC diagnostic ignored "-Wunused-function"\n'; + result += 'typedef unsigned char uchar;\n'; + result += 'typedef unsigned int uint;\n'; + result += '#include \n'; + result += '#include \n'; + result += '#include \n'; + result += '#ifndef __cplusplus\n'; + result += 'typedef unsigned char bool;\n'; + result += '#include "minecraft_c.h"\n'; + result += '#else\n'; + result += '#include \n'; + result += '#include \n'; + result += 'extern "C" {\n'; + result += '#include "minecraft_cpp.h"\n'; + result += '}\n'; + result += '#endif\n'; + result += '#pragma GCC diagnostic pop\n'; + fs.writeFileSync(output, result); +} +makeMainHeader(`${outputDirectory}/minecraft.h`); \ No newline at end of file diff --git a/src/loader.ts b/src/loader.ts new file mode 100644 index 0000000..0f5f18a --- /dev/null +++ b/src/loader.ts @@ -0,0 +1,229 @@ +import { COMMENT, EXTENSION, parseTypeAndName, readDefinition } from './common'; +import { isCppAllowed } from './map'; +import { Method } from './method'; +import { SimpleProperty, StaticProperty } from './property'; +import { Struct } from './struct'; + +function safeParseInt(str: string) { + const x = parseInt(str); + if (isNaN(x)) { + throw new Error('Invalid Integer: ' + str); + } + return x; +} +function syntaxError(message?: string) { + throw new Error('Syntax Error' + (message ? `: ${message}` : '')); +} + +export class ErrorOnLine { + readonly error: unknown; + readonly file: string; + readonly line: number; + constructor (error: unknown, file: string, line: number) { + this.error = error; + this.file = file; + this.line = line; + } +} + +// Parse Property +function parseProperty(args: string) { + const parts = args.split(' '); + if (parts.length !== 4) { + syntaxError('Invalid Piece Count'); + } + if (parts[2] !== '=') { + syntaxError(); + } + const {type, name} = parseTypeAndName([parts[0]!, parts[1]!]); + const offset = safeParseInt(parts[3]!); + return {type, name, offset}; +} + +// Parse Method +function parseMethod(args: string, self: string, insertSelfArg: boolean) { + const argsStart = args.indexOf(' ('); + if (argsStart === -1) { + syntaxError('Cannot Find Arguments'); + } + const start = args.substring(0, argsStart).split(' '); + if (start.length !== 2) { + syntaxError('Invalid Piece Count'); + } + const {type, name} = parseTypeAndName([start[0]!, start[1]!]); + const end = args.substring(argsStart + 1).split(' = '); + if (end.length !== 2) { + syntaxError('Invalid Piece Count'); + } + let methodArgs = end[0]!; + if (!methodArgs.startsWith('(') || !methodArgs.endsWith(')')) { + syntaxError('Invalid Method Arguments'); + } + if (insertSelfArg) { + let selfArg = `(${self} *self`; + if (methodArgs !== '()') { + selfArg += ', '; + } + methodArgs = selfArg + methodArgs.substring(1); + } + const address = safeParseInt(end[1]!); + return new Method(self, name, type, methodArgs, address); +} + +// Load Structure +export function load(target: Struct, name: string, isExtended: boolean) { + // Read File + let data = readDefinition(name); + + // Strip Comments + const lines = []; + for (let line of data.split('\n')) { + // Trim + line = line.trim(); + // Remove Comments + const index = line.indexOf(COMMENT); + if (index !== -1) { + line = line.substring(0, index); + } + // Store Line + lines.push(line); + } + data = lines.join('\n'); + + // Line-By-Line + let cursor = 0; + for (let piece of data.split(';')) { + // Find Start Of Command For Error Handling + const startOfCommand = cursor + piece.search(/\S|$/); + // Advance Cursor + cursor += piece.length + 1; + // Handle Errors + try { + // Trim + piece = piece.trim(); + // Replace Newlines With Spaces + piece = piece.replace(/\n/g, ' '); + // Simplify + piece = piece.replace(/\s+/g, ' '); + // Skip Empty Piece + if (piece.length === 0) { + continue; + } + // Skip C++ Types If Applicable + if (!isCppAllowed() && piece.includes('std::')) { + continue; + } + + // Handle Commands + let firstSpace = piece.indexOf(' '); + if (firstSpace === -1) { + firstSpace = piece.length; + } + const command = piece.substring(0, firstSpace); + const args = piece.substring(firstSpace + 1); + switch (command) { + case 'extends': { + // Load Parent + load(target, args, true); + break; + } + case 'size': { + // Set Size + if (!isExtended) { + target.setSize(safeParseInt(args)); + } + break; + } + case 'vtable-size': { + // Set VTable Size + if (!isExtended) { + target.setVTableSize(safeParseInt(args)); + } + break; + } + case 'vtable': { + // Set VTable Address + if (!isExtended) { + target.setVTableAddress(safeParseInt(args)); + } + break; + } + case 'property': { + // Add Property + const info = parseProperty(args); + target.addProperty(new SimpleProperty(info.offset, info.type, info.name)); + break; + } + case 'static-property': { + // Add Static Property + if (!isExtended) { + const info = parseProperty(args); + target.addStaticProperty(new StaticProperty(info.offset, info.type, info.name, target.getName())); + } + break; + } + case 'method': { + // Add Method + const method = parseMethod(args, target.getName(), true); + target.addMethod(method, false); + break; + } + case 'virtual-method': { + // Add Virtual Method + const method = parseMethod(args, target.getName(), true); + target.addMethod(method, true); + break; + } + case 'static-method': { + // Add Static Method + if (!isExtended) { + const method = parseMethod(args, target.getName(), false); + target.addMethod(method, false); + } + break; + } + case 'constructor': { + // Constructor + if (!isExtended) { + let data = `${target.getName()} *constructor`; + if (args.startsWith('(')) { + // No Custom Name + data += ' '; + } else { + // Use Custom Name + data += '_'; + } + data += args; + const method = parseMethod(data, target.getName(), true); + target.addMethod(method, false); + } + break; + } + case 'requires': { + // Add Dependency + if (args.length === 0) { + syntaxError('Missing Dependency Name'); + } + target.addDependency(args); + break; + } + default: { + throw new Error(`Invalid Command: ${command}`); + } + } + } catch (e) { + if (e instanceof ErrorOnLine) { + throw e; + } + // Find Line Number + let lineNumber = 1; + for (let i = 0; i <= startOfCommand; i++) { + if (data.charAt(i) === '\n') { + lineNumber++; + } + } + // Rethrow Error With Line Number + throw new ErrorOnLine(e, `${name}${EXTENSION}`, lineNumber); + } + } +} \ No newline at end of file diff --git a/src/map.ts b/src/map.ts new file mode 100644 index 0000000..d08a23f --- /dev/null +++ b/src/map.ts @@ -0,0 +1,52 @@ +import { Struct } from './struct'; +import { ErrorOnLine, load } from './loader'; + +// Track Mode +let allowCpp = false; +export function isCppAllowed() { + return allowCpp; +} +export function setCppAllowed(newMode: boolean) { + clearStructures(); + allowCpp = newMode; +} + +// Store Loaded Structures +const structures: {[id: string]: Struct} = {}; + +// Get Or Load Structure +export function getStructure(name: string) { + if (Object.prototype.hasOwnProperty.call(structures, name)) { + // Already Loaded + return structures[name]!; + } else { + // Load Structure + try { + // Create Structure + const target = new Struct(name); + structures[name] = target; + // Parse File + load(target, name, false); + // Return + return target; + } catch (e) { + let error = e; + let extra = ''; + if (e instanceof ErrorOnLine) { + extra = `${e.file}:${e.line}: `; + error = e.error; + } + console.log(`Error Loading ${name}: ${extra}${error instanceof Error ? error.stack : error}`); + process.exit(1); + } + } +} + +// Clear Loaded Structures +function clearStructures() { + for (const name in structures) { + if (Object.prototype.hasOwnProperty.call(structures, name)) { + delete structures[name]; + } + } +} \ No newline at end of file diff --git a/src/method.ts b/src/method.ts new file mode 100644 index 0000000..e616972 --- /dev/null +++ b/src/method.ts @@ -0,0 +1,37 @@ +import { formatType } from './common'; + +export class Method { + readonly self: string; + readonly shortName: string; + readonly returnType: string; + readonly args: string; + readonly address: number; + + // Constructor + constructor(self: string, name: string, returnType: string, args: string, address: number) { + this.self = self; + this.shortName = name; + this.returnType = returnType; + this.args = args; + this.address = address; + } + + // Get Type + getName() { + return `${this.self}_${this.shortName}`; + } + getType() { + return `${this.getName()}_t`; + } + + // Generate Type Definition + generateTypedef() { + const returnType = formatType(this.returnType); + return `typedef ${returnType}(*${this.getType()})${this.args};\n`; + } + + // Generate Variable Definition + generateDefinition() { + return `${this.getType()} ${this.getName()};\n`; + } +} \ No newline at end of file diff --git a/src/property.ts b/src/property.ts new file mode 100644 index 0000000..4b58cfc --- /dev/null +++ b/src/property.ts @@ -0,0 +1,129 @@ +import { POINTER_SIZE } from './common'; +import { getStructure } from './map'; + +export interface Property { + propertyOffset(): number; + propertySize(): number; + propertyType(): string; + propertyName(): string; + propertyAlignment(): number; +} + +export class SimpleProperty implements Property { + readonly #offset: number; + readonly #type: string; + readonly #name: string; + + // Constructor + constructor(offset: number, type: string, name: string) { + this.#offset = offset; + this.#type = type; + this.#name = name; + } + + // Getters + propertyOffset() { + return this.#offset; + } + propertyType() { + return this.#type; + } + propertyName() { + return this.#name; + } + + // Size + propertySize() { + if (this.#type.endsWith('*')) { + // Pointer + return POINTER_SIZE; + } else if (this.#type === 'int' || this.#type === 'uint') { + // Integer + return 4; + } else if (this.#type === 'float') { + // Float + return 4; + } else if (this.#type === 'bool') { + // Boolean + return 1; + } else if (this.#type === 'std::string') { + // C++ String + return 4; + } else if (this.#type === 'char' || this.#type === 'uchar') { + // Character + return 1; + } else if (this.#type.startsWith('std::vector<')) { + // C++ Vector + return 12; + } else { + // Structure + const structure = getStructure(this.#type); + return structure.getSize(); + } + } + + // Alignment + propertyAlignment() { + if (this.#type.endsWith('*')) { + // Pointer + return POINTER_SIZE; + } else if (this.#type === 'int' || this.#type === 'uint') { + // Integer + return 4; + } else if (this.#type === 'float') { + // Float + return 4; + } else if (this.#type === 'bool') { + // Boolean + return 1; + } else if (this.#type === 'std::string') { + // C++ String + return 4; + } else if (this.#type === 'char' || this.#type === 'uchar') { + // Character + return 1; + } else if (this.#type.startsWith('std::vector<')) { + // C++ Vector + return 4; + } else { + // Structure + const structure = getStructure(this.#type); + return structure.getAlignment(); + } + } +} + +export class StaticProperty { + readonly address: number; + readonly #type: string; + readonly #name: string; + readonly #self: string; + + // Constructor + constructor(address: number, type: string, name: string, self: string) { + this.address = address; + this.#type = type; + this.#name = name; + this.#self = self; + } + + // Name And Type + getName() { + return `${this.#self}_${this.#name}`; + } + getType() { + let type = this.#type; + // Convert To Pointer + if (!type.endsWith('*')) { + type += ' '; + } + type += '*'; + // Return + return type; + } + + // Generate Variable Definition + generateDefinition() { + return `${this.getType()}${this.getName()};\n`; + } +} \ No newline at end of file diff --git a/src/struct.ts b/src/struct.ts new file mode 100644 index 0000000..bcff7fa --- /dev/null +++ b/src/struct.ts @@ -0,0 +1,277 @@ +import { INDENT, MIN_SIZE, toUpperSnakeCase, formatType } 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 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()}`; + } + + // 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}; + } +} \ No newline at end of file diff --git a/src/vtable.ts b/src/vtable.ts new file mode 100644 index 0000000..728e993 --- /dev/null +++ b/src/vtable.ts @@ -0,0 +1,176 @@ +import { INDENT, POINTER_SIZE } 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 #methods: Method[]; + + // Constructor + constructor(name: string) { + this.#name = name; + this.#address = null; + this.#size = null; + this.#methods = []; + } + + // Property Information + propertyOffset() { + return 0; + } + propertySize() { + return POINTER_SIZE; + } + propertyType() { + return this.#getName() + ' *'; + } + propertyName() { + return 'vtable'; + } + propertyAlignment() { + return 4; + } + + // Setters + 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}`); + } + } + + // Add To VTable + add(method: Method) { + // 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}`); + } + // Add + const index = offset / POINTER_SIZE; + this.#methods[index] = method; + } + + // Get Structure Name + #getName() { + return this.#name + '_vtable'; + } + + // 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}`); + } + this.#methods.length = maxMethodCount; + } + } + + // Generate Code + generate() { + let out = ''; + + // Check + this.#check(); + + // Method Prototypes + for (let i = 0; i < this.#methods.length; i++) { + const info = this.#methods[i]; + if (info) { + out += info.generateTypedef(); + } + } + + // Structure + out += `typedef struct ${this.#getName()} ${this.#getName()};\n`; + out += `struct ${this.#getName()} {\n`; + for (let i = 0; i < this.#methods.length; i++) { + let name = `unknown${i}`; + let type = 'void *'; + const info = this.#methods[i]; + if (info) { + name = info.shortName; + type = info.getType() + ' '; + } + out += `${INDENT}${type}${name};\n`; + } + out += `};\n`; + + // Pointers + if (this.#address !== null) { + // Base + out += `extern ${this.#getName()} *${this.#getName()}_base;\n`; + // Methods + for (let i = 0; i < this.#methods.length; i++) { + const info = this.#methods[i]; + if (info) { + const type = `${info.getType()} *`; + out += `extern ${type}${info.getName()}_vtable_addr;\n`; + out += `extern ${info.generateDefinition()}`; + } + } + } + + // Duplication Method + if (this.#size !== null) { + out += `${this.#getName()} *dup_${this.#getName()}(${this.#getName()} *vtable);\n`; + } + + // Return + return out; + } + + // Generate Compiled Code + generateCode() { + let declarations = ''; + let init = ''; + + // Check + this.#check(); + + // Pointers + if (this.#address !== null) { + // Base + init += `${INDENT}${this.#getName()}_base = (${this.#getName()} *) ${this.#address};\n`; + declarations += `${this.#getName()} *${this.#getName()}_base;\n`; + // Methods + for (let i = 0; i < this.#methods.length; i++) { + const info = this.#methods[i]; + if (info) { + const vtableAddress = this.#address + (i * POINTER_SIZE); + const type = `${info.getType()} *`; + init += `${INDENT}${info.getName()}_vtable_addr = (${type}) ${vtableAddress};\n`; + declarations += `${type}${info.getName()}_vtable_addr;\n`; + init += `${INDENT}${info.getName()} = *${info.getName()}_vtable_addr;\n`; + declarations += info.generateDefinition(); + } + } + } + + // Duplication Method + if (this.#size !== null) { + declarations += `${this.#getName()} *dup_${this.#getName()}(${this.#getName()} *vtable) {\n`; + declarations += `${INDENT}${this.#getName()} *obj = (${this.#getName()} *) malloc(${this.#size});\n`; + declarations += `${INDENT}if (obj == NULL) {\n`; + declarations += `${INDENT}${INDENT}return NULL;\n`; + declarations += `${INDENT}}\n`; + declarations += `${INDENT}memcpy((void *) obj, (void *) vtable, ${this.#size});\n`; + declarations += `${INDENT}return obj;\n`; + declarations += '}\n'; + } + + // Return + return {declarations, init}; + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f9c9021 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": [ + "@tsconfig/strictest/tsconfig", + "@tsconfig/node-lts/tsconfig" + ], + "compilerOptions": { + "rootDir": "./src", + "outDir": "./build" + }, + "include": [ + "src" + ] +}