Initial Prototype

This commit is contained in:
TheBrokenRail 2024-01-04 15:27:02 -05:00
commit e99239fecf
15 changed files with 1229 additions and 0 deletions

2
.eslintignore Normal file
View File

@ -0,0 +1,2 @@
/build
/node_modules

36
.eslintrc Normal file
View File

@ -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"
}
}

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/build
/package-lock.json
/node_modules
/out
/symbols

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"cSpell.words": [
"vtable"
]
}

21
LICENSE Normal file
View File

@ -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.

22
package.json Normal file
View File

@ -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"
}
}

49
src/common.ts Normal file
View File

@ -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 = '//';

176
src/index.ts Normal file
View File

@ -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 <string.h>\n';
result += '#include <stdlib.h>\n';
result += '#include <stddef.h>\n';
result += '#ifndef __cplusplus\n';
result += 'typedef unsigned char bool;\n';
result += '#include "minecraft_c.h"\n';
result += '#else\n';
result += '#include <string>\n';
result += '#include <vector>\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`);

229
src/loader.ts Normal file
View File

@ -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);
}
}
}

52
src/map.ts Normal file
View File

@ -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];
}
}
}

37
src/method.ts Normal file
View File

@ -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`;
}
}

129
src/property.ts Normal file
View File

@ -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`;
}
}

277
src/struct.ts Normal file
View File

@ -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};
}
}

176
src/vtable.ts Normal file
View File

@ -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};
}
}

13
tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": [
"@tsconfig/strictest/tsconfig",
"@tsconfig/node-lts/tsconfig"
],
"compilerOptions": {
"rootDir": "./src",
"outDir": "./build"
},
"include": [
"src"
]
}