[c] Port type extractor to TypeScript and improve codegen

- Ported extract-spine-cpp-types.js to TypeScript in type-extractor.ts
- Improved type interfaces with discriminated unions for better type safety
- Added proper isConst tracking for const-qualified methods
- Fixed exclusions to check method.isConst instead of return type
- Removed special type mappings (utf8, spine_void) - primitives pass through unchanged
- Made toCTypeName strict with proper error handling
- Documented all conversion functions with examples
- Excluded SpineObject from extraction (matches JS behavior)
- Removed original JS extractor as it's now replaced by TypeScript version

The TypeScript extractor produces identical output (107 files, 164 types) while providing better type information including isConst for methods and consistent isStatic fields.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mario Zechner 2025-07-09 01:50:41 +02:00
parent 0a33247f44
commit 22ea76db1b
13 changed files with 966 additions and 1830 deletions

View File

@ -48,7 +48,7 @@ method: EventData::setAudioPath
method: EventData::setVolume method: EventData::setVolume
method: EventData::setBalance method: EventData::setBalance
# Vector<String> methods need special handling # Array<String> methods need special handling
method: AttachmentTimeline.getAttachmentNames method: AttachmentTimeline.getAttachmentNames
# BoneLocal/BonePose setScale is overloaded in a confusing way # BoneLocal/BonePose setScale is overloaded in a confusing way

View File

@ -41,14 +41,21 @@ export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[
if (!type.members) continue; if (!type.members) continue;
for (const member of type.members) { for (const member of type.members) {
switch (member.kind) {
case 'method':
extractArrayTypes(member.returnType); extractArrayTypes(member.returnType);
extractArrayTypes(member.type);
if (member.parameters) { if (member.parameters) {
for (const param of member.parameters) { for (const param of member.parameters) {
extractArrayTypes(param.type); extractArrayTypes(param.type);
} }
} }
break;
case 'field':
extractArrayTypes(member.type);
break;
default:
break;
}
} }
} }
} }

View File

@ -1,93 +0,0 @@
import * as fs from 'fs';
import * as path from 'path';
// Load the spine-cpp types
const typesJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../spine-cpp-types.json'), 'utf8'));
// Set to store unique Array<T> types
const arrayTypes = new Set<string>();
// Function to extract Array<T> from a type string
function extractArrayTypes(typeStr: string | undefined) {
if (!typeStr) return;
// Find all Array<...> patterns
const regex = /Array<([^>]+)>/g;
let match;
while ((match = regex.exec(typeStr)) !== null) {
arrayTypes.add(match[0]); // Full Array<T> string
}
}
// Process all types
for (const header of Object.keys(typesJson)) {
for (const type of typesJson[header]) {
if (!type.members) continue;
for (const member of type.members) {
// Check return types
extractArrayTypes(member.returnType);
// Check field types
extractArrayTypes(member.type);
// Check parameter types
if (member.parameters) {
for (const param of member.parameters) {
extractArrayTypes(param.type);
}
}
}
}
}
// Sort and display results
const sortedArrayTypes = Array.from(arrayTypes).sort();
console.log(`Found ${sortedArrayTypes.length} Array specializations:\n`);
// Group by element type
const primitiveArrays: string[] = [];
const pointerArrays: string[] = [];
for (const arrayType of sortedArrayTypes) {
const elementType = arrayType.match(/Array<(.+)>/)![1];
if (elementType.includes('*')) {
pointerArrays.push(arrayType);
} else {
primitiveArrays.push(arrayType);
}
}
console.log('Primitive Arrays:');
for (const type of primitiveArrays) {
console.log(` ${type}`);
}
console.log('\nPointer Arrays:');
for (const type of pointerArrays) {
console.log(` ${type}`);
}
// Generate C type names
console.log('\nC Type Names:');
console.log('\nPrimitive Arrays:');
for (const type of primitiveArrays) {
const elementType = type.match(/Array<(.+)>/)![1];
const cType = elementType === 'float' ? 'float' :
elementType === 'int' ? 'int32_t' :
elementType === 'unsigned short' ? 'uint16_t' :
elementType;
console.log(` ${type} -> spine_array_${cType.replace(/ /g, '_')}`);
}
console.log('\nPointer Arrays:');
for (const type of pointerArrays) {
const elementType = type.match(/Array<(.+?)\s*\*/)![1].trim();
// Remove 'class ' prefix if present
const cleanType = elementType.replace(/^class\s+/, '');
// Convert to snake case
const snakeCase = cleanType.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
console.log(` ${type} -> spine_array_${snakeCase}`);
}

View File

@ -1,4 +1,4 @@
import { Type, Member, toSnakeCase, toCTypeName } from '../types'; import { Type, Member, toSnakeCase, Method } from '../types';
import { ArraySpecialization } from '../array-scanner'; import { ArraySpecialization } from '../array-scanner';
export class ArrayGenerator { export class ArrayGenerator {
@ -71,10 +71,8 @@ export class ArrayGenerator {
// Get Array methods to wrap // Get Array methods to wrap
const methods = this.arrayType!.members?.filter(m => const methods = this.arrayType!.members?.filter(m =>
m.kind === 'method' && m.kind === 'method'
!m.isStatic && ).filter(m => !m.isStatic && !m.name.includes('operator')) || [];
!m.name.includes('operator')
) || [];
// Generate create method (constructor) // Generate create method (constructor)
header.push(`SPINE_C_EXPORT ${spec.cTypeName} ${spec.cTypeName}_create();`); header.push(`SPINE_C_EXPORT ${spec.cTypeName} ${spec.cTypeName}_create();`);
@ -123,7 +121,7 @@ export class ArrayGenerator {
header.push(''); header.push('');
} }
private generateMethodWrapper(spec: ArraySpecialization, method: Member, header: string[], source: string[]) { private generateMethodWrapper(spec: ArraySpecialization, method: Method, header: string[], source: string[]) {
// Skip constructors and destructors // Skip constructors and destructors
if (method.name === 'Array' || method.name === '~Array') return; if (method.name === 'Array' || method.name === '~Array') return;

View File

@ -1,4 +1,4 @@
import { Type, Member, toSnakeCase, toCFunctionName, toCTypeName } from '../types'; import { Type, Member, toSnakeCase, toCFunctionName, toCTypeName, Constructor } from '../types';
export interface GeneratorResult { export interface GeneratorResult {
declarations: string[]; declarations: string[];
@ -6,6 +6,8 @@ export interface GeneratorResult {
} }
export class ConstructorGenerator { export class ConstructorGenerator {
constructor(private validTypes: Set<string>) {}
generate(type: Type): GeneratorResult { generate(type: Type): GeneratorResult {
const declarations: string[] = []; const declarations: string[] = [];
const implementations: string[] = []; const implementations: string[] = [];
@ -49,7 +51,7 @@ export class ConstructorGenerator {
return { declarations, implementations }; return { declarations, implementations };
} }
private getCreateFunctionName(typeName: string, constructor: Member, index: number): string { private getCreateFunctionName(typeName: string, constructor: Constructor, index: number): string {
const baseName = `spine_${toSnakeCase(typeName)}_create`; const baseName = `spine_${toSnakeCase(typeName)}_create`;
if (!constructor.parameters || constructor.parameters.length === 0) { if (!constructor.parameters || constructor.parameters.length === 0) {
@ -83,7 +85,7 @@ export class ConstructorGenerator {
return 'param'; return 'param';
} }
private generateParameters(constructor: Member): { declaration: string; call: string } { private generateParameters(constructor: Constructor): { declaration: string; call: string } {
if (!constructor.parameters || constructor.parameters.length === 0) { if (!constructor.parameters || constructor.parameters.length === 0) {
return { declaration: 'void', call: '' }; return { declaration: 'void', call: '' };
} }
@ -92,7 +94,7 @@ export class ConstructorGenerator {
const callParts: string[] = []; const callParts: string[] = [];
for (const param of constructor.parameters) { for (const param of constructor.parameters) {
const cType = toCTypeName(param.type); const cType = toCTypeName(param.type, this.validTypes);
declParts.push(`${cType} ${param.name}`); declParts.push(`${cType} ${param.name}`);
// Convert C type back to C++ for the call // Convert C type back to C++ for the call

View File

@ -1,9 +1,9 @@
import { Type, Member, toSnakeCase, toCFunctionName, toCTypeName, Exclusion } from '../types'; import { Type, Member, toSnakeCase, toCFunctionName, toCTypeName, Exclusion, Method } from '../types';
import { isMethodExcluded } from '../exclusions'; import { isMethodExcluded } from '../exclusions';
import { GeneratorResult } from './constructor-generator'; import { GeneratorResult } from './constructor-generator';
export class MethodGenerator { export class MethodGenerator {
constructor(private exclusions: Exclusion[]) {} constructor(private exclusions: Exclusion[], private validTypes: Set<string>) {}
generate(type: Type): GeneratorResult { generate(type: Type): GeneratorResult {
const declarations: string[] = []; const declarations: string[] = [];
@ -12,13 +12,12 @@ export class MethodGenerator {
if (!type.members) return { declarations, implementations }; if (!type.members) return { declarations, implementations };
const methods = type.members.filter(m => const methods = type.members.filter(m =>
m.kind === 'method' && m.kind === 'method'
!m.isStatic && ).filter(m => !isMethodExcluded(type.name, m.name, this.exclusions, m))
!isMethodExcluded(type.name, m.name, this.exclusions, m) .filter(m => !m.isStatic);
);
// Check for const/non-const method pairs // Check for const/non-const method pairs
const methodGroups = new Map<string, Member[]>(); const methodGroups = new Map<string, Method[]>();
for (const method of methods) { for (const method of methods) {
const key = method.name + '(' + (method.parameters?.map(p => p.type).join(',') || '') + ')'; const key = method.name + '(' + (method.parameters?.map(p => p.type).join(',') || '') + ')';
if (!methodGroups.has(key)) { if (!methodGroups.has(key)) {
@ -91,9 +90,9 @@ export class MethodGenerator {
if (method.name.startsWith('get') && (!method.parameters || method.parameters.length === 0)) { if (method.name.startsWith('get') && (!method.parameters || method.parameters.length === 0)) {
const propName = method.name.substring(3); const propName = method.name.substring(3);
// Check if return type is Vector // Check if return type is Array
if (method.returnType && method.returnType.includes('Vector<')) { if (method.returnType && method.returnType.includes('Array<')) {
// For Vector types, only generate collection accessors // For Array types, only generate collection accessors
this.generateCollectionAccessors(type, method, propName, declarations, implementations); this.generateCollectionAccessors(type, method, propName, declarations, implementations);
} else if (method.name === 'getRTTI') { } else if (method.name === 'getRTTI') {
// Special handling for getRTTI - make it static // Special handling for getRTTI - make it static
@ -107,9 +106,9 @@ export class MethodGenerator {
implementations.push(`}`); implementations.push(`}`);
implementations.push(''); implementations.push('');
} else { } else {
// For non-Vector types, generate regular getter // For non-Array types, generate regular getter
const funcName = toCFunctionName(type.name, method.name); const funcName = toCFunctionName(type.name, method.name);
const returnType = toCTypeName(method.returnType || 'void'); const returnType = toCTypeName(method.returnType || 'void', this.validTypes);
declarations.push(`SPINE_C_EXPORT ${returnType} ${funcName}(${cTypeName} obj);`); declarations.push(`SPINE_C_EXPORT ${returnType} ${funcName}(${cTypeName} obj);`);
@ -126,7 +125,7 @@ export class MethodGenerator {
// Handle setters // Handle setters
else if (method.name.startsWith('set') && method.parameters && method.parameters.length === 1) { else if (method.name.startsWith('set') && method.parameters && method.parameters.length === 1) {
const funcName = toCFunctionName(type.name, method.name); const funcName = toCFunctionName(type.name, method.name);
const paramType = toCTypeName(method.parameters[0].type); const paramType = toCTypeName(method.parameters[0].type, this.validTypes);
declarations.push(`SPINE_C_EXPORT void ${funcName}(${cTypeName} obj, ${paramType} value);`); declarations.push(`SPINE_C_EXPORT void ${funcName}(${cTypeName} obj, ${paramType} value);`);
@ -161,7 +160,7 @@ export class MethodGenerator {
funcName = `${funcName}_${paramCount}`; funcName = `${funcName}_${paramCount}`;
} }
const returnType = toCTypeName(method.returnType || 'void'); const returnType = toCTypeName(method.returnType || 'void', this.validTypes);
const params = this.generateMethodParameters(cTypeName, method); const params = this.generateMethodParameters(cTypeName, method);
declarations.push(`SPINE_C_EXPORT ${returnType} ${funcName}(${params.declaration});`); declarations.push(`SPINE_C_EXPORT ${returnType} ${funcName}(${params.declaration});`);
@ -184,14 +183,14 @@ export class MethodGenerator {
return { declarations, implementations }; return { declarations, implementations };
} }
private generateCollectionAccessors(type: Type, method: Member, propName: string, private generateCollectionAccessors(type: Type, method: Method, propName: string,
declarations: string[], implementations: string[]) { declarations: string[], implementations: string[]) {
const cTypeName = `spine_${toSnakeCase(type.name)}`; const cTypeName = `spine_${toSnakeCase(type.name)}`;
const propSnake = toSnakeCase(propName); const propSnake = toSnakeCase(propName);
const vectorMatch = method.returnType!.match(/Vector<(.+?)>/); const arrayMatch = method.returnType!.match(/Array<(.+?)>/);
if (!vectorMatch) return; if (!arrayMatch) return;
const elementType = vectorMatch[1].trim().replace(/\s*\*$/, ''); const elementType = arrayMatch[1].trim().replace(/\s*\*$/, '');
let cElementType: string; let cElementType: string;
if (elementType === 'int') { if (elementType === 'int') {
cElementType = 'int32_t'; cElementType = 'int32_t';
@ -226,9 +225,9 @@ export class MethodGenerator {
implementations.push(` if (!obj) return nullptr;`); implementations.push(` if (!obj) return nullptr;`);
implementations.push(` ${type.name} *_obj = (${type.name} *) obj;`); implementations.push(` ${type.name} *_obj = (${type.name} *) obj;`);
// Handle const vs non-const vectors // Handle const vs non-const arrays
if (method.isConst || method.returnType!.includes('const')) { if (method.isConst || method.returnType!.includes('const')) {
// For const vectors, we need to copy the data or use data() method // For const arrays, we need to copy the data or use data() method
implementations.push(` auto& vec = _obj->get${propName}();`); implementations.push(` auto& vec = _obj->get${propName}();`);
implementations.push(` if (vec.size() == 0) return nullptr;`); implementations.push(` if (vec.size() == 0) return nullptr;`);
implementations.push(` return (${cElementType} *) &vec[0];`); implementations.push(` return (${cElementType} *) &vec[0];`);
@ -240,7 +239,7 @@ export class MethodGenerator {
implementations.push(''); implementations.push('');
} }
private generateMethodCall(objName: string, method: Member, args?: string): string { private generateMethodCall(objName: string, method: Method, args?: string): string {
let call = `${objName}->${method.name}(${args || ''})`; let call = `${objName}->${method.name}(${args || ''})`;
// Handle return type conversions // Handle return type conversions
@ -251,21 +250,21 @@ export class MethodGenerator {
// RTTI needs special handling - return as opaque pointer // RTTI needs special handling - return as opaque pointer
call = `(spine_rtti) &${call}`; call = `(spine_rtti) &${call}`;
} else if (method.returnType.includes('*')) { } else if (method.returnType.includes('*')) {
const cType = toCTypeName(method.returnType); const cType = toCTypeName(method.returnType, this.validTypes);
call = `(${cType}) ${call}`; call = `(${cType}) ${call}`;
} else if (method.returnType === 'Color &' || method.returnType === 'const Color &') { } else if (method.returnType === 'Color &' || method.returnType === 'const Color &') {
call = `(spine_color) &${call}`; call = `(spine_color) &${call}`;
} else if (method.returnType.includes('&')) { } else if (method.returnType.includes('&')) {
// For reference returns, take the address // For reference returns, take the address
const cType = toCTypeName(method.returnType); const cType = toCTypeName(method.returnType, this.validTypes);
call = `(${cType}) &${call}`; call = `(${cType}) &${call}`;
} else if (!this.isPrimitiveType(method.returnType) && !this.isEnumType(method.returnType)) { } else if (!this.isPrimitiveType(method.returnType) && !this.isEnumType(method.returnType)) {
// For non-primitive value returns (e.g., BoneLocal), take the address // For non-primitive value returns (e.g., BoneLocal), take the address
const cType = toCTypeName(method.returnType); const cType = toCTypeName(method.returnType, this.validTypes);
call = `(${cType}) &${call}`; call = `(${cType}) &${call}`;
} else if (this.isEnumType(method.returnType)) { } else if (this.isEnumType(method.returnType)) {
// Cast enum return values // Cast enum return values
const cType = toCTypeName(method.returnType); const cType = toCTypeName(method.returnType, this.validTypes);
call = `(${cType}) ${call}`; call = `(${cType}) ${call}`;
} }
} }
@ -273,15 +272,15 @@ export class MethodGenerator {
return call; return call;
} }
private generateSetterCall(objName: string, method: Member, valueName: string): string { private generateSetterCall(objName: string, method: Method, valueName: string): string {
const param = method.parameters![0]; const param = method.parameters![0];
let value = valueName; let value = valueName;
// Convert from C type to C++ // Convert from C type to C++
if (param.type === 'const String &' || param.type === 'String') { if (param.type === 'const String &' || param.type === 'String') {
value = `String(${valueName})`; value = `String(${valueName})`;
} else if (param.type.includes('Vector<')) { } else if (param.type.includes('Array<')) {
// Vector types are passed as void* and need to be cast back // Array types are passed as void* and need to be cast back
value = `(${param.type}) ${valueName}`; value = `(${param.type}) ${valueName}`;
} else if (param.type.includes('*')) { } else if (param.type.includes('*')) {
value = `(${param.type}) ${valueName}`; value = `(${param.type}) ${valueName}`;
@ -297,21 +296,21 @@ export class MethodGenerator {
return `${objName}->${method.name}(${value})`; return `${objName}->${method.name}(${value})`;
} }
private generateMethodParameters(objTypeName: string, method: Member): { declaration: string; call: string } { private generateMethodParameters(objTypeName: string, method: Method): { declaration: string; call: string } {
const declParts = [`${objTypeName} obj`]; const declParts = [`${objTypeName} obj`];
const callParts: string[] = []; const callParts: string[] = [];
if (method.parameters) { if (method.parameters) {
for (const param of method.parameters) { for (const param of method.parameters) {
const cType = toCTypeName(param.type); const cType = toCTypeName(param.type, this.validTypes);
declParts.push(`${cType} ${param.name}`); declParts.push(`${cType} ${param.name}`);
// Convert C type back to C++ for the call // Convert C type back to C++ for the call
let callExpr = param.name; let callExpr = param.name;
if (param.type === 'const String &' || param.type === 'String') { if (param.type === 'const String &' || param.type === 'String') {
callExpr = `String(${param.name})`; callExpr = `String(${param.name})`;
} else if (param.type.includes('Vector<')) { } else if (param.type.includes('Array<')) {
// Vector types are passed as void* and need to be cast back // Array types are passed as void* and need to be cast back
callExpr = `(${param.type}) ${param.name}`; callExpr = `(${param.type}) ${param.name}`;
} else if (param.type.includes('*')) { } else if (param.type.includes('*')) {
callExpr = `(${param.type}) ${param.name}`; callExpr = `(${param.type}) ${param.name}`;

View File

@ -1,8 +0,0 @@
import { Type, toSnakeCase } from '../types';
export class OpaqueTypeGenerator {
generate(type: Type): string {
const cName = `spine_${toSnakeCase(type.name)}`;
return `SPINE_OPAQUE_TYPE(${cName})`;
}
}

View File

@ -30,20 +30,21 @@ function checkConstNonConstConflicts(classes: Type[], exclusions: Exclusion[]):
const conflicts: Array<{ type: string, method: string }> = []; const conflicts: Array<{ type: string, method: string }> = [];
for (const type of classes) { for (const type of classes) {
if (type.members === undefined) {
continue;
}
// Get all non-static methods first // Get all non-static methods first
const allMethods = type.members?.filter(m => const allMethods = type.members?.filter(m => m.kind === 'method').filter(m => !m.isStatic);
m.kind === 'method' &&
!m.isStatic
);
if (allMethods) { if (allMethods) {
const methodGroups = new Map<string, Member[]>(); const methodGroups = new Map<string, Array<Member & { kind: 'method' }>>();
for (const method of allMethods) { for (const method of allMethods) {
// Skip if this specific const/non-const version is excluded
if (isMethodExcluded(type.name, method.name, exclusions, method)) {
if (method.name === 'getSetupPose') { if (method.name === 'getSetupPose') {
console.log(`Skipping excluded method: ${type.name}::${method.name}${method.isConst ? ' const' : ''}`); console.log(`Skipping excluded method: ${type.name}::${method.name}${method.isConst ? ' const' : ''}`);
} }
// Skip if this specific const/non-const version is excluded
if (isMethodExcluded(type.name, method.name, exclusions, method)) {
continue; continue;
} }
const key = method.name + '(' + (method.parameters?.map(p => p.type).join(',') || '') + ')'; const key = method.name + '(' + (method.parameters?.map(p => p.type).join(',') || '') + ')';
@ -133,9 +134,12 @@ async function main() {
// Check for const/non-const conflicts // Check for const/non-const conflicts
checkConstNonConstConflicts(classes, exclusions); checkConstNonConstConflicts(classes, exclusions);
// Create a set of valid type names for type checking
const validTypes = new Set<string>(includedTypes.map(t => t.name));
// Initialize generators // Initialize generators
const constructorGen = new ConstructorGenerator(); const constructorGen = new ConstructorGenerator(validTypes);
const methodGen = new MethodGenerator(exclusions); const methodGen = new MethodGenerator(exclusions, validTypes);
const enumGen = new EnumGenerator(); const enumGen = new EnumGenerator();
const fileWriter = new FileWriter(path.join(__dirname, '../../src/generated')); const fileWriter = new FileWriter(path.join(__dirname, '../../src/generated'));

View File

@ -1,11 +1,547 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { Type, Member, Method, Field, Constructor, Destructor, Parameter, EnumValue, SpineTypes } from './types';
const SPINE_CPP_PATH = path.join(__dirname, '../../../spine-cpp'); const SPINE_CPP_PATH = path.join(__dirname, '../../../spine-cpp');
const EXTRACTOR_SCRIPT = path.join(SPINE_CPP_PATH, 'extract-spine-cpp-types.js'); const SPINE_INCLUDE_DIR = path.join(SPINE_CPP_PATH, 'spine-cpp/include');
const OUTPUT_FILE = path.join(__dirname, '../spine-cpp-types.json'); const OUTPUT_FILE = path.join(__dirname, '../spine-cpp-types.json');
const HEADERS_DIR = path.join(SPINE_CPP_PATH, 'spine-cpp/include/spine');
/**
* Extracts the value of an enum constant from source code
*/
function extractEnumValueFromSource(
enumConstNode: any,
sourceLines: string[]
): string | null | undefined {
if (!enumConstNode.loc) return undefined;
const line = sourceLines[enumConstNode.loc.line - 1];
if (!line) return undefined;
// Find enum name and check for '='
const nameMatch = line.match(new RegExp(`\\b${enumConstNode.name}\\b`));
if (!nameMatch) return undefined;
const afterName = line.substring(nameMatch.index! + enumConstNode.name.length);
const equalIndex = afterName.indexOf('=');
if (equalIndex === -1) return null; // No explicit value
// Extract value expression
let valueText = afterName.substring(equalIndex + 1);
// Handle multi-line values
let currentLine = enumConstNode.loc.line;
while (currentLine < sourceLines.length && !valueText.match(/[,}]/)) {
valueText += '\n' + sourceLines[currentLine++];
}
// Extract up to comma or brace
const endMatch = valueText.match(/^(.*?)([,}])/s);
if (endMatch) valueText = endMatch[1];
// Clean up
return valueText
.replace(/\/\/.*$/gm, '') // Remove single-line comments
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
.replace(/\s+/g, ' ') // Normalize whitespace
.trim();
}
/**
* Extracts return type from a method node
*/
function extractReturnType(methodNode: any): string {
const fullType = methodNode.type?.qualType || '';
const match = fullType.match(/^(.+?)\s*\(/);
return match ? match[1].trim() : 'void';
}
/**
* Extracts parameters from a method node
*/
function extractParameters(methodNode: any): Parameter[] {
return (methodNode.inner || [])
.filter((n: any) => n.kind === 'ParmVarDecl')
.map((n: any) => ({
name: n.name || '',
type: n.type?.qualType || ''
}));
}
/**
* Checks if a node is in the target file
*/
function isInTargetFile(node: any, targetPath: string): boolean {
if (!node.loc) return false;
const loc = node.loc;
// Check direct file location
if (loc.file) return path.resolve(loc.file) === targetPath;
// Check macro locations
for (const locType of ['spellingLoc', 'expansionLoc']) {
if (loc[locType]) {
if (loc[locType].includedFrom) return false;
if (loc[locType].file) return path.resolve(loc[locType].file) === targetPath;
}
}
// If included from another file, reject
if (loc.includedFrom) return false;
// No location info - assume it's in the main file
return true;
}
/**
* Extracts member information from an AST node
*/
function extractMember(inner: any, parent: any): Member | null {
if (inner.isImplicit) return null;
switch (inner.kind) {
case 'FieldDecl':
const field: Field = {
kind: 'field',
name: inner.name || '',
type: inner.type?.qualType || '',
isStatic: inner.storageClass === 'static'
};
return field;
case 'CXXMethodDecl':
if (!inner.name) return null;
// Skip operators - not needed for C wrapper generation
if (inner.name.startsWith('operator')) return null;
const method: Method = {
kind: 'method',
name: inner.name,
returnType: extractReturnType(inner),
parameters: extractParameters(inner),
isStatic: inner.storageClass === 'static',
isVirtual: inner.virtual || false,
isPure: inner.pure || false,
isConst: inner.constQualifier || false
};
return method;
case 'CXXConstructorDecl':
const constructor: Constructor = {
kind: 'constructor',
name: inner.name || parent.name || '',
parameters: extractParameters(inner)
};
return constructor;
case 'CXXDestructorDecl':
// Include destructors for completeness
const destructor: Destructor = {
kind: 'destructor',
name: inner.name || `~${parent.name}`,
isVirtual: inner.virtual || false,
isPure: inner.pure || false
};
return destructor;
default:
return null;
}
}
/**
* Extracts type information from an AST node
*/
function extractTypeInfo(node: any, sourceLines: string[]): Type {
const info: Type = {
name: node.name || '',
kind: node.kind === 'EnumDecl' ? 'enum' : (node.tagUsed || 'class') as 'class' | 'struct' | 'enum',
loc: {
line: node.loc?.line || 0,
col: node.loc?.col || 0
}
};
// Extract base classes
if (node.bases?.length > 0) {
info.superTypes = node.bases.map((b: any) => b.type?.qualType || '').filter(Boolean);
}
// For enums, extract the values
if (node.kind === 'EnumDecl') {
info.values = (node.inner || [])
.filter((n: any) => n.kind === 'EnumConstantDecl')
.map((n: any) => {
const enumValue: EnumValue = { name: n.name || '' };
const sourceValue = extractEnumValueFromSource(n, sourceLines);
if (sourceValue === null) {
// Implicit value - no value property
} else if (sourceValue) {
enumValue.value = sourceValue;
} else if (n.inner?.length > 0) {
enumValue.value = "<<extraction failed>>";
}
return enumValue;
});
return info;
}
// For classes/structs, extract public members
info.members = [];
let currentAccess = node.tagUsed === 'struct' ? 'public' : 'private';
let hasPureVirtual = false;
for (const inner of node.inner || []) {
if (inner.kind === 'AccessSpecDecl') {
currentAccess = inner.access || 'private';
continue;
}
if (inner.kind === 'FriendDecl' || currentAccess !== 'public') continue;
const member = extractMember(inner, node);
if (member) {
info.members.push(member);
// Check if this is a pure virtual method
if (member.kind === 'method' && member.isPure) {
hasPureVirtual = true;
}
}
}
// Always set isAbstract to a boolean value
info.isAbstract = hasPureVirtual;
return info;
}
/**
* Processes an AST node to extract types
*/
function processNode(
node: any,
types: Type[],
targetPath: string,
sourceLines: string[],
inSpineNamespace: boolean = false
): void {
if (!node || typeof node !== 'object') return;
// Handle spine namespace
if (node.kind === 'NamespaceDecl' && node.name === 'spine') {
(node.inner || []).forEach((n: any) => processNode(n, types, targetPath, sourceLines, true));
return;
}
// Recurse to find spine namespace
if (!inSpineNamespace) {
(node.inner || []).forEach((n: any) => processNode(n, types, targetPath, sourceLines, false));
return;
}
// Process type declarations
const typeKinds = ['CXXRecordDecl', 'ClassTemplateDecl', 'EnumDecl', 'TypedefDecl', 'TypeAliasDecl'];
if (!typeKinds.includes(node.kind)) return;
// Skip if not in target file or invalid
if (!isInTargetFile(node, targetPath) ||
node.isImplicit ||
!node.name ||
node.name.startsWith('_') ||
node.name.includes('<')) return;
// Skip forward declarations
if (node.previousDecl && (!node.inner || node.inner.length === 0)) return;
// Extract type info
if (node.kind === 'ClassTemplateDecl') {
const classNode = (node.inner || []).find((n: any) => n.kind === 'CXXRecordDecl');
if (classNode) {
const typeInfo = extractTypeInfo(classNode, sourceLines);
typeInfo.isTemplate = true;
// Extract template parameters
const templateParams: string[] = [];
for (const inner of node.inner || []) {
if (inner.kind === 'TemplateTypeParmDecl' && inner.name) {
templateParams.push(inner.name);
}
}
if (templateParams.length > 0) {
typeInfo.templateParams = templateParams;
}
types.push(typeInfo);
}
} else if (node.kind === 'CXXRecordDecl' && node.inner?.length > 0) {
const typeInfo = extractTypeInfo(node, sourceLines);
// Ensure isTemplate is always set for non-template classes
if (typeInfo.isTemplate === undefined) {
typeInfo.isTemplate = false;
}
types.push(typeInfo);
} else if (node.kind === 'EnumDecl') {
types.push(extractTypeInfo(node, sourceLines));
} else if (node.kind === 'TypedefDecl' || node.kind === 'TypeAliasDecl') {
types.push(extractTypeInfo(node, sourceLines));
}
}
/**
* Extracts types from a single header file
*/
function extractLocalTypes(headerFile: string, typeMap: Map<string, Type> | null = null): Type[] {
const absHeaderPath = path.resolve(headerFile);
const sourceContent = fs.readFileSync(absHeaderPath, 'utf8');
const sourceLines = sourceContent.split('\n');
// Get AST from clang
const cmd = `clang++ -Xclang -ast-dump=json -fsyntax-only -std=c++11 -I "${SPINE_INCLUDE_DIR}" "${absHeaderPath}" 2>/dev/null`;
const maxBuffer = headerFile.includes('Debug.h') ? 500 : 200; // MB
let astJson: any;
try {
const output = execSync(cmd, { encoding: 'utf8', maxBuffer: maxBuffer * 1024 * 1024 });
astJson = JSON.parse(output);
} catch (error: any) {
throw new Error(error.code === 'ENOBUFS'
? `AST output too large (>${maxBuffer}MB)`
: error.message);
}
const types: Type[] = [];
processNode(astJson, types, absHeaderPath, sourceLines);
// Filter out forward declarations and SpineObject
const filteredTypes = types
.filter(t => {
// Skip types with no members (forward declarations)
if (t.members && t.members.length === 0) return false;
// Skip SpineObject - it's not needed for C wrapper generation
if (t.name === 'SpineObject') return false;
return true;
})
.sort((a, b) => (a.loc?.line || 0) - (b.loc?.line || 0));
// Add inherited methods if we have a type map
if (typeMap) {
for (const type of filteredTypes) {
if (type.superTypes && type.members) {
addInheritedMethods(type, typeMap);
}
}
}
return filteredTypes;
}
/**
* Creates a method signature for comparison
*/
function getMethodSignature(method: Method): string {
let sig = method.name;
if (method.parameters && method.parameters.length > 0) {
sig += '(' + method.parameters.map(p => p.type).join(',') + ')';
} else {
sig += '()';
}
// Add const qualifier if present
if (method.isConst) {
sig += ' const';
}
return sig;
}
/**
* Substitutes template parameters in a type string
*/
function substituteTemplateParams(typeStr: string, paramMap: Map<string, string>): string {
let result = typeStr;
// Replace template parameters in order of length (longest first)
// to avoid replacing substrings (e.g., V before V1)
const sortedParams = Array.from(paramMap.keys()).sort((a, b) => b.length - a.length);
for (const param of sortedParams) {
const regex = new RegExp(`\\b${param}\\b`, 'g');
result = result.replace(regex, paramMap.get(param)!);
}
return result;
}
/**
* Adds methods inherited from template supertypes
*/
function addTemplateInheritedMethods(
_type: Type,
templateType: Type,
templateClassName: string,
templateArgs: string,
inheritedMethods: Member[],
ownMethodSignatures: Set<string>
): void {
// Parse template arguments (handle multiple args)
const argsList: string[] = [];
let depth = 0;
let currentArg = '';
for (const char of templateArgs) {
if (char === '<') depth++;
else if (char === '>') depth--;
if (char === ',' && depth === 0) {
argsList.push(currentArg.trim());
currentArg = '';
} else {
currentArg += char;
}
}
if (currentArg.trim()) {
argsList.push(currentArg.trim());
}
// Build a mapping of template params to actual types
const paramMap = new Map<string, string>();
// Use the actual template parameters if we have them
if (templateType.templateParams && templateType.templateParams.length === argsList.length) {
templateType.templateParams.forEach((param, i) => {
paramMap.set(param, argsList[i]);
});
} else {
// Fallback: if we don't have template param info, skip substitution
console.error(`Warning: Template ${templateClassName} missing parameter info, skipping substitution`);
return;
}
// Process each member of the template
for (const member of templateType.members || []) {
if (member.kind === 'method') {
const method = member as Method;
// Skip template constructors - they have weird names like "Pose<P>"
if (method.name.includes('<')) continue;
// Clone the member and substitute template parameters
const inheritedMember: Method = JSON.parse(JSON.stringify(method));
inheritedMember.fromSupertype = `${templateClassName}<${templateArgs}>`;
// Replace template parameters in return type
if (inheritedMember.returnType) {
inheritedMember.returnType = substituteTemplateParams(
inheritedMember.returnType, paramMap
);
}
// Replace template parameters in parameters
if (inheritedMember.parameters) {
for (const param of inheritedMember.parameters) {
param.type = substituteTemplateParams(param.type, paramMap);
}
}
// Check if this method is overridden
const sig = getMethodSignature(inheritedMember);
if (!ownMethodSignatures.has(sig)) {
inheritedMethods.push(inheritedMember);
ownMethodSignatures.add(sig);
}
}
}
}
/**
* Adds inherited methods to a type
*/
function addInheritedMethods(type: Type, typeMap: Map<string, Type>): void {
const inheritedMethods: Member[] = [];
const ownMethodSignatures = new Set<string>();
// Build a set of method signatures from this type
for (const member of type.members || []) {
if (member.kind === 'method') {
const sig = getMethodSignature(member as Method);
ownMethodSignatures.add(sig);
}
}
// Process each supertype
for (const superTypeName of type.superTypes || []) {
// Clean up the supertype name (remove namespaces, etc)
const cleanName = superTypeName.replace(/^.*::/, '');
// Skip SpineObject inheritance - it's just noise
if (cleanName === 'SpineObject') continue;
// Check if this is a template supertype
const templateMatch = cleanName.match(/^([^<]+)<(.+)>$/);
if (templateMatch) {
const templateClassName = templateMatch[1];
const templateArgs = templateMatch[2];
const templateType = typeMap.get(templateClassName);
if (templateType && templateType.members) {
// Process template inheritance
addTemplateInheritedMethods(
type, templateType, templateClassName, templateArgs,
inheritedMethods, ownMethodSignatures
);
}
} else {
// Non-template supertype
const superType = typeMap.get(cleanName);
if (!superType || !superType.members) continue;
// Add non-overridden methods from supertype
for (const member of superType.members) {
if (member.kind === 'method') {
const method = member as Method;
const sig = getMethodSignature(method);
if (!ownMethodSignatures.has(sig)) {
const inheritedMember = { ...method, fromSupertype: cleanName };
inheritedMethods.push(inheritedMember);
ownMethodSignatures.add(sig); // Prevent duplicates from multiple inheritance
}
}
}
}
}
// Add inherited methods to the type
if (type.members) {
type.members.push(...inheritedMethods);
}
}
/**
* Finds all header files in the spine include directory
*/
function findAllHeaderFiles(): string[] {
const headers: string[] = [];
function walkDir(dir: string): void {
fs.readdirSync(dir).forEach(file => {
const fullPath = path.join(dir, file);
if (fs.statSync(fullPath).isDirectory()) {
walkDir(fullPath);
} else if (file.endsWith('.h') && file !== 'spine.h') {
headers.push(fullPath);
}
});
}
walkDir(SPINE_INCLUDE_DIR);
return headers.sort();
}
/** /**
* Checks if type extraction is needed based on file timestamps * Checks if type extraction is needed based on file timestamps
@ -21,10 +557,11 @@ function isExtractionNeeded(): boolean {
const outputStats = fs.statSync(OUTPUT_FILE); const outputStats = fs.statSync(OUTPUT_FILE);
const outputTime = outputStats.mtime.getTime(); const outputTime = outputStats.mtime.getTime();
// Check all header files // Check all header files in the spine subdirectory
const headerFiles = fs.readdirSync(HEADERS_DIR) const spineDir = path.join(SPINE_INCLUDE_DIR, 'spine');
.filter(f => f.endsWith('.h')) const headerFiles = fs.readdirSync(spineDir, { recursive: true })
.map(f => path.join(HEADERS_DIR, f)); .filter((f: string | Buffer<ArrayBufferLike>) => f.toString().endsWith('.h'))
.map((f: string | Buffer<ArrayBufferLike>) => path.join(spineDir, f.toString()));
// Find newest header modification time // Find newest header modification time
let newestHeaderTime = 0; let newestHeaderTime = 0;
@ -49,7 +586,7 @@ function isExtractionNeeded(): boolean {
} }
/** /**
* Runs the extract-spine-cpp-types.js script to generate type information * Runs the type extraction process and generates the output file
*/ */
export function extractTypes(): void { export function extractTypes(): void {
if (!isExtractionNeeded()) { if (!isExtractionNeeded()) {
@ -59,16 +596,66 @@ export function extractTypes(): void {
console.log('Running type extraction...'); console.log('Running type extraction...');
try { try {
// Run the extractor script const allHeaders = findAllHeaderFiles();
const output = execSync(`node "${EXTRACTOR_SCRIPT}"`, { const allTypes: SpineTypes = {};
cwd: SPINE_CPP_PATH, let processed = 0, errors = 0;
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024 // 10MB buffer for large output console.error(`Processing ${allHeaders.length} header files...`);
});
// First pass: extract all types without inheritance
const typeMap = new Map<string, Type>();
for (const headerFile of allHeaders) {
const relPath = path.relative(SPINE_INCLUDE_DIR, headerFile);
process.stderr.write(`\r\x1b[K Pass 1 - Processing ${++processed}/${allHeaders.length}: ${relPath}...`);
try {
const types = extractLocalTypes(headerFile);
if (types.length > 0) {
allTypes[relPath] = types;
// Build type map
for (const type of types) {
typeMap.set(type.name, type);
}
}
} catch (error: any) {
errors++;
console.error(`\n ERROR processing ${relPath}: ${error.message}`);
}
}
// Second pass: add inherited methods
console.error(`\n Pass 2 - Adding inherited methods...`);
processed = 0;
for (const headerFile of allHeaders) {
const relPath = path.relative(SPINE_INCLUDE_DIR, headerFile);
if (!allTypes[relPath]) continue;
process.stderr.write(`\r\x1b[K Pass 2 - Processing ${++processed}/${Object.keys(allTypes).length}: ${relPath}...`);
for (const type of allTypes[relPath]) {
if (type.superTypes && type.members) {
addInheritedMethods(type, typeMap);
// Check if any inherited methods are pure virtual
// If so, and the class doesn't override them, it's abstract
if (!type.isAbstract) {
const hasPureVirtual = type.members.some(m =>
m.kind === 'method' && m.isPure === true
);
if (hasPureVirtual) {
type.isAbstract = true;
}
}
}
}
}
console.error(`\n Completed: ${Object.keys(allTypes).length} files processed, ${errors} errors`);
// Write to output file // Write to output file
fs.writeFileSync(OUTPUT_FILE, output); fs.writeFileSync(OUTPUT_FILE, JSON.stringify(allTypes, null, 2));
console.log(`Type extraction complete, wrote ${OUTPUT_FILE}`); console.log(`Type extraction complete, wrote ${OUTPUT_FILE}`);
} catch (error: any) { } catch (error: any) {
console.error('Failed to extract types:', error.message); console.error('Failed to extract types:', error.message);
@ -79,7 +666,7 @@ export function extractTypes(): void {
/** /**
* Loads the extracted type information * Loads the extracted type information
*/ */
export function loadTypes(): any { export function loadTypes(): SpineTypes {
if (!fs.existsSync(OUTPUT_FILE)) { if (!fs.existsSync(OUTPUT_FILE)) {
throw new Error(`Type information not found at ${OUTPUT_FILE}. Run extraction first.`); throw new Error(`Type information not found at ${OUTPUT_FILE}. Run extraction first.`);
} }

View File

@ -1,22 +1,49 @@
export interface Parameter { export interface Parameter {
name: string; name: string;
type: string; type: string;
defaultValue?: string;
} }
export interface Member { export type Field = {
kind: 'field' | 'method' | 'constructor' | 'destructor'; kind: 'field';
name: string; name: string;
type?: string; // For fields type: string;
returnType?: string; // For methods isStatic?: boolean;
fromSupertype?: string;
}
export type Method = {
kind: 'method';
name: string;
returnType: string;
parameters?: Parameter[]; parameters?: Parameter[];
isStatic?: boolean; isStatic?: boolean;
isVirtual?: boolean; isVirtual?: boolean;
isPure?: boolean; isPure?: boolean;
isConst?: boolean; isConst?: boolean;
fromSupertype?: string; // Indicates this member was inherited fromSupertype?: string;
} }
export type Constructor = {
kind: 'constructor';
name: string;
parameters?: Parameter[];
fromSupertype?: string;
}
export type Destructor = {
kind: 'destructor';
name: string;
isVirtual?: boolean;
isPure?: boolean;
fromSupertype?: string;
};
export type Member =
| Field
| Method
| Constructor
| Destructor
export interface EnumValue { export interface EnumValue {
name: string; name: string;
value?: string; value?: string;
@ -53,6 +80,17 @@ export type Exclusion =
isConst?: boolean; // Whether the method is const (e.g., void foo() const), NOT whether return type is const isConst?: boolean; // Whether the method is const (e.g., void foo() const), NOT whether return type is const
}; };
/**
* Converts a PascalCase or camelCase name to snake_case.
*
* @param name The name to convert
* @returns The snake_case version
*
* Examples:
* - "AnimationState" "animation_state"
* - "getRTTI" "get_rtti"
* - "IKConstraint" "ik_constraint"
*/
export function toSnakeCase(name: string): string { export function toSnakeCase(name: string): string {
// Handle acronyms and consecutive capitals // Handle acronyms and consecutive capitals
let result = name.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2'); let result = name.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2');
@ -61,140 +99,135 @@ export function toSnakeCase(name: string): string {
return result.toLowerCase(); return result.toLowerCase();
} }
/**
* Generates a C function name from a type and method name.
*
* @param typeName The C++ class name
* @param methodName The method name
* @returns The C function name
*
* Examples:
* - ("Skeleton", "updateCache") "spine_skeleton_update_cache"
* - ("AnimationState", "apply") "spine_animation_state_apply"
*/
export function toCFunctionName(typeName: string, methodName: string): string { export function toCFunctionName(typeName: string, methodName: string): string {
return `spine_${toSnakeCase(typeName)}_${toSnakeCase(methodName)}`; return `spine_${toSnakeCase(typeName)}_${toSnakeCase(methodName)}`;
} }
export function toCTypeName(cppType: string): string { /**
// Remove any spine:: namespace prefix first * Checks if a type is a primitive by tokenizing and checking if ALL tokens start with lowercase.
cppType = cppType.replace(/^spine::/, ''); * Examples:
* - "int" true
* - "const char*" true (all tokens: "const", "char*" start lowercase)
* - "unsigned int" true (all tokens start lowercase)
* - "Array<float>" false (starts uppercase)
* - "const Array<float>&" false ("Array" starts uppercase)
*/
function isPrimitive(cppType: string): boolean {
const tokens = cppType.split(/\s+/);
return tokens.every(token => {
// Remove any trailing punctuation like *, &
const cleanToken = token.replace(/[*&]+$/, '');
return cleanToken.length > 0 && /^[a-z]/.test(cleanToken);
});
}
// Category 1: Primitives (including void) /**
const primitiveMap: { [key: string]: string } = { * Converts a C++ type to its corresponding C type.
'void': 'void', *
'bool': 'bool', * @param cppType The C++ type to convert
'char': 'char', * @param validTypes Set of valid type names (classes and enums) from filtered types
'int': 'int32_t', * @returns The C type
'int32_t': 'int32_t', * @throws Error if the type is not recognized
'unsigned int': 'uint32_t', *
'uint32_t': 'uint32_t', * Examples:
'short': 'int16_t', * - Primitives: "int" "int", "const float*" "const float*"
'int16_t': 'int16_t', * - String types: "String" "const char*", "const String&" "const char*"
'unsigned short': 'uint16_t', * - Arrays: "Array<float>" "spine_array_float"
'uint16_t': 'uint16_t', * - Class pointers: "Bone*" "spine_bone"
'long long': 'int64_t', * - Class references: "const Color&" "spine_color"
'int64_t': 'int64_t', * - Non-const primitive refs: "float&" "float*" (output parameter)
'unsigned long long': 'uint64_t', */
'uint64_t': 'uint64_t', export function toCTypeName(cppType: string, validTypes: Set<string>): string {
'float': 'float', // Remove extra spaces and normalize
'double': 'double', const normalizedType = cppType.replace(/\s+/g, ' ').trim();
'size_t': 'size_t',
'uint8_t': 'uint8_t'
};
if (primitiveMap[cppType]) { // Primitives - pass through unchanged
return primitiveMap[cppType]; if (isPrimitive(normalizedType)) {
return normalizedType;
} }
// Category 2: Special types // Special type: String
if (cppType === 'String' || cppType === 'const String' || cppType === 'const char *') { if (normalizedType === 'String' || normalizedType === 'const String' ||
return 'const utf8 *'; normalizedType === 'String&' || normalizedType === 'const String&') {
} return 'const char*';
if (cppType === 'void *') {
return 'spine_void';
}
if (cppType === 'DisposeRendererObject') {
return 'spine_dispose_renderer_object';
}
if (cppType === 'TextureLoader' || cppType === 'TextureLoader *') {
return 'spine_texture_loader';
}
if (cppType === 'PropertyId') {
return 'int64_t'; // PropertyId is typedef'd to long long
} }
// Category 3: Arrays - must check before pointers/references // PropertyId is a typedef
const arrayMatch = cppType.match(/^(?:const\s+)?Array<(.+?)>\s*(?:&|\*)?$/); if (normalizedType === 'PropertyId') {
return 'int64_t';
}
// Arrays - must check before pointers/references
const arrayMatch = normalizedType.match(/^(?:const\s+)?Array<(.+?)>\s*(?:&|\*)?$/);
if (arrayMatch) { if (arrayMatch) {
const elementType = arrayMatch[1].trim(); const elementType = arrayMatch[1].trim();
// Map element types to C array type suffixes // For primitive element types, use the type name with spaces replaced by underscores
let typeSuffix: string; if (isPrimitive(elementType)) {
return `spine_array_${elementType.replace(/\s+/g, '_')}`;
// Handle primitives
if (elementType === 'float') typeSuffix = 'float';
else if (elementType === 'int') typeSuffix = 'int32';
else if (elementType === 'unsigned short') typeSuffix = 'uint16';
else if (elementType === 'bool') typeSuffix = 'bool';
else if (elementType === 'char') typeSuffix = 'char';
else if (elementType === 'size_t') typeSuffix = 'size';
else if (elementType === 'PropertyId') typeSuffix = 'property_id';
// Handle pointer types - remove * and convert
else if (elementType.endsWith('*')) {
const cleanType = elementType.slice(0, -1).trim();
typeSuffix = toSnakeCase(cleanType);
}
// Handle everything else (enums, classes)
else {
typeSuffix = toSnakeCase(elementType);
} }
return `spine_array_${typeSuffix}`; // For pointer types, remove the * and convert
if (elementType.endsWith('*')) {
const baseType = elementType.slice(0, -1).trim();
return `spine_array_${toSnakeCase(baseType)}`;
} }
// Category 4: Pointers // For class/enum types
const pointerMatch = cppType.match(/^(.+?)\s*\*$/); return `spine_array_${toSnakeCase(elementType)}`;
}
// Pointers
const pointerMatch = normalizedType.match(/^(.+?)\s*\*$/);
if (pointerMatch) { if (pointerMatch) {
const baseType = pointerMatch[1].trim(); const baseType = pointerMatch[1].trim();
// Primitive pointers stay as-is // Primitive pointers stay as-is
if (primitiveMap[baseType]) { if (isPrimitive(baseType)) {
const mappedType = primitiveMap[baseType]; return normalizedType;
// For numeric types, use the mapped type
return mappedType === 'void' ? 'void *' : `${mappedType} *`;
} }
// char* becomes utf8* // Class pointers become opaque types
if (baseType === 'char' || baseType === 'const char') {
return 'utf8 *';
}
// Class pointers
return `spine_${toSnakeCase(baseType)}`; return `spine_${toSnakeCase(baseType)}`;
} }
// Category 5: References // References
const refMatch = cppType.match(/^(?:const\s+)?(.+?)\s*&$/); const refMatch = normalizedType.match(/^((?:const\s+)?(.+?))\s*&$/);
if (refMatch) { if (refMatch) {
const baseType = refMatch[1].trim(); const fullBaseType = refMatch[1].trim();
const isConst = cppType.includes('const '); const baseType = refMatch[2].trim();
const isConst = fullBaseType.startsWith('const ');
// Special cases
if (baseType === 'String') return 'const utf8 *';
if (baseType === 'RTTI') return 'spine_rtti';
// Non-const references to primitives become pointers (output parameters) // Non-const references to primitives become pointers (output parameters)
if (!isConst && primitiveMap[baseType]) { if (!isConst && isPrimitive(baseType)) {
const mappedType = primitiveMap[baseType]; return `${baseType}*`;
return mappedType === 'void' ? 'void *' : `${mappedType} *`;
} }
// Const references and class references - recurse without the reference // Const references and class references - recurse without the reference
return toCTypeName(baseType); return toCTypeName(baseType, validTypes);
} }
// Category 6: Known enums // Function pointers - for now, just error
const knownEnums = [ if (normalizedType.includes('(') && normalizedType.includes(')')) {
'MixBlend', 'MixDirection', 'BlendMode', 'AttachmentType', 'EventType', throw new Error(`Function pointer types not yet supported: ${normalizedType}`);
'Format', 'TextureFilter', 'TextureWrap', 'Inherit', 'Physics',
'PositionMode', 'Property', 'RotateMode', 'SequenceMode', 'SpacingMode'
];
if (knownEnums.includes(cppType)) {
return `spine_${toSnakeCase(cppType)}`;
} }
// Category 7: Classes (default case) // Everything else should be a class or enum type
// Assume any remaining type is a spine class // Check if it's a valid type
return `spine_${toSnakeCase(cppType)}`; if (!validTypes.has(normalizedType)) {
throw new Error(`Unknown type: ${normalizedType}. Not a primitive and not in the list of valid types.`);
}
return `spine_${toSnakeCase(normalizedType)}`;
} }

View File

@ -1,529 +0,0 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#include "custom.h"
#include <spine/spine.h>
#include <spine/Version.h>
#include <spine/Debug.h>
#include <spine/SkeletonRenderer.h>
#include <spine/AnimationState.h>
#include <cstring>
using namespace spine;
// Internal structures
struct _spine_atlas {
void *atlas;
utf8 **imagePaths;
int32_t numImagePaths;
utf8 *error;
};
struct _spine_skeleton_data_result {
spine_skeleton_data skeletonData;
utf8 *error;
};
struct _spine_bounds {
float x, y, width, height;
};
struct _spine_vector {
float x, y;
};
struct _spine_skeleton_drawable : public SpineObject {
spine_skeleton skeleton;
spine_animation_state animationState;
spine_animation_state_data animationStateData;
spine_animation_state_events animationStateEvents;
SkeletonRenderer *renderer;
};
struct _spine_skin_entry {
int32_t slotIndex;
utf8 *name;
spine_attachment attachment;
};
struct _spine_skin_entries {
int32_t numEntries;
_spine_skin_entry *entries;
};
// Animation state event tracking
struct AnimationStateEvent {
EventType type;
TrackEntry *entry;
Event *event;
AnimationStateEvent(EventType type, TrackEntry *entry, Event *event) : type(type), entry(entry), event(event){};
};
class EventListener : public AnimationStateListenerObject, public SpineObject {
public:
Vector<AnimationStateEvent> events;
void callback(AnimationState *state, EventType type, TrackEntry *entry, Event *event) override {
events.add(AnimationStateEvent(type, entry, event));
SP_UNUSED(state);
}
};
// Static variables
static Color NULL_COLOR(0, 0, 0, 0);
static SpineExtension *defaultExtension = nullptr;
static DebugExtension *debugExtension = nullptr;
static void initExtensions() {
if (defaultExtension == nullptr) {
defaultExtension = new DefaultSpineExtension();
debugExtension = new DebugExtension(defaultExtension);
}
}
namespace spine {
SpineExtension *getDefaultExtension() {
initExtensions();
return defaultExtension;
}
}
// Version functions
int32_t spine_major_version() {
return SPINE_MAJOR_VERSION;
}
int32_t spine_minor_version() {
return SPINE_MINOR_VERSION;
}
void spine_enable_debug_extension(spine_bool enable) {
initExtensions();
SpineExtension::setInstance(enable ? debugExtension : defaultExtension);
}
void spine_report_leaks() {
initExtensions();
debugExtension->reportLeaks();
fflush(stdout);
}
// Color functions
float spine_color_get_r(spine_color color) {
if (!color) return 0;
return ((Color *) color)->r;
}
float spine_color_get_g(spine_color color) {
if (!color) return 0;
return ((Color *) color)->g;
}
float spine_color_get_b(spine_color color) {
if (!color) return 0;
return ((Color *) color)->b;
}
float spine_color_get_a(spine_color color) {
if (!color) return 0;
return ((Color *) color)->a;
}
// Bounds functions
float spine_bounds_get_x(spine_bounds bounds) {
if (!bounds) return 0;
return ((_spine_bounds *) bounds)->x;
}
float spine_bounds_get_y(spine_bounds bounds) {
if (!bounds) return 0;
return ((_spine_bounds *) bounds)->y;
}
float spine_bounds_get_width(spine_bounds bounds) {
if (!bounds) return 0;
return ((_spine_bounds *) bounds)->width;
}
float spine_bounds_get_height(spine_bounds bounds) {
if (!bounds) return 0;
return ((_spine_bounds *) bounds)->height;
}
// Vector functions
float spine_vector_get_x(spine_vector vector) {
if (!vector) return 0;
return ((_spine_vector *) vector)->x;
}
float spine_vector_get_y(spine_vector vector) {
if (!vector) return 0;
return ((_spine_vector *) vector)->y;
}
// Atlas functions
class LiteTextureLoad : public TextureLoader {
void load(AtlasPage &page, const String &path) {
page.texture = (void *) (intptr_t) page.index;
}
void unload(void *texture) {
}
};
static LiteTextureLoad liteLoader;
spine_atlas spine_atlas_load(const utf8 *atlasData) {
if (!atlasData) return nullptr;
int32_t length = (int32_t) strlen(atlasData);
auto atlas = new (__FILE__, __LINE__) Atlas(atlasData, length, "", &liteLoader, true);
_spine_atlas *result = SpineExtension::calloc<_spine_atlas>(1, __FILE__, __LINE__);
result->atlas = atlas;
result->numImagePaths = (int32_t) atlas->getPages().size();
result->imagePaths = SpineExtension::calloc<utf8 *>(result->numImagePaths, __FILE__, __LINE__);
for (int i = 0; i < result->numImagePaths; i++) {
result->imagePaths[i] = (utf8 *) strdup(atlas->getPages()[i]->texturePath.buffer());
}
return (spine_atlas) result;
}
class CallbackTextureLoad : public TextureLoader {
spine_texture_loader_load_func loadCb;
spine_texture_loader_unload_func unloadCb;
public:
CallbackTextureLoad() : loadCb(nullptr), unloadCb(nullptr) {}
void setCallbacks(spine_texture_loader_load_func load, spine_texture_loader_unload_func unload) {
loadCb = load;
unloadCb = unload;
}
void load(AtlasPage &page, const String &path) {
page.texture = this->loadCb(path.buffer());
}
void unload(void *texture) {
this->unloadCb(texture);
}
};
static CallbackTextureLoad callbackLoader;
spine_atlas spine_atlas_load_callback(const utf8 *atlasData, const utf8 *atlasDir,
spine_texture_loader_load_func load,
spine_texture_loader_unload_func unload) {
if (!atlasData) return nullptr;
int32_t length = (int32_t) strlen(atlasData);
callbackLoader.setCallbacks(load, unload);
auto atlas = new (__FILE__, __LINE__) Atlas(atlasData, length, (const char *) atlasDir, &callbackLoader, true);
_spine_atlas *result = SpineExtension::calloc<_spine_atlas>(1, __FILE__, __LINE__);
result->atlas = atlas;
result->numImagePaths = (int32_t) atlas->getPages().size();
result->imagePaths = SpineExtension::calloc<utf8 *>(result->numImagePaths, __FILE__, __LINE__);
for (int i = 0; i < result->numImagePaths; i++) {
result->imagePaths[i] = (utf8 *) strdup(atlas->getPages()[i]->texturePath.buffer());
}
return (spine_atlas) result;
}
int32_t spine_atlas_get_num_image_paths(spine_atlas atlas) {
if (!atlas) return 0;
return ((_spine_atlas *) atlas)->numImagePaths;
}
utf8 *spine_atlas_get_image_path(spine_atlas atlas, int32_t index) {
if (!atlas) return nullptr;
_spine_atlas *_atlas = (_spine_atlas *) atlas;
if (index < 0 || index >= _atlas->numImagePaths) return nullptr;
return _atlas->imagePaths[index];
}
spine_bool spine_atlas_is_pma(spine_atlas atlas) {
if (!atlas) return 0;
Atlas *_atlas = (Atlas *) ((_spine_atlas *) atlas)->atlas;
for (size_t i = 0; i < _atlas->getPages().size(); i++) {
AtlasPage *page = _atlas->getPages()[i];
if (page->pma) return -1;
}
return 0;
}
utf8 *spine_atlas_get_error(spine_atlas atlas) {
if (!atlas) return nullptr;
return ((_spine_atlas *) atlas)->error;
}
void spine_atlas_dispose(spine_atlas atlas) {
if (!atlas) return;
_spine_atlas *_atlas = (_spine_atlas *) atlas;
if (_atlas->atlas) {
delete (Atlas *) _atlas->atlas;
}
if (_atlas->imagePaths) {
for (int i = 0; i < _atlas->numImagePaths; i++) {
if (_atlas->imagePaths[i]) {
SpineExtension::free(_atlas->imagePaths[i], __FILE__, __LINE__);
}
}
SpineExtension::free(_atlas->imagePaths, __FILE__, __LINE__);
}
if (_atlas->error) {
SpineExtension::free(_atlas->error, __FILE__, __LINE__);
}
SpineExtension::free(_atlas, __FILE__, __LINE__);
}
// Skeleton data loading
spine_skeleton_data_result spine_skeleton_data_load_json(spine_atlas atlas, const utf8 *skeletonData) {
if (!atlas || !skeletonData) return nullptr;
_spine_skeleton_data_result *result = SpineExtension::calloc<_spine_skeleton_data_result>(1, __FILE__, __LINE__);
SkeletonJson json((Atlas *) ((_spine_atlas *) atlas)->atlas);
json.setScale(1);
SkeletonData *data = json.readSkeletonData(skeletonData);
if (!data) {
result->error = (utf8 *) strdup("Failed to load skeleton data");
return (spine_skeleton_data_result) result;
}
result->skeletonData = (spine_skeleton_data) data;
return (spine_skeleton_data_result) result;
}
spine_skeleton_data_result spine_skeleton_data_load_binary(spine_atlas atlas, const uint8_t *skeletonData, int32_t length) {
if (!atlas || !skeletonData) return nullptr;
_spine_skeleton_data_result *result = SpineExtension::calloc<_spine_skeleton_data_result>(1, __FILE__, __LINE__);
SkeletonBinary binary((Atlas *) ((_spine_atlas *) atlas)->atlas);
binary.setScale(1);
SkeletonData *data = binary.readSkeletonData((const unsigned char *) skeletonData, length);
if (!data) {
result->error = (utf8 *) strdup("Failed to load skeleton data");
return (spine_skeleton_data_result) result;
}
result->skeletonData = (spine_skeleton_data) data;
return (spine_skeleton_data_result) result;
}
utf8 *spine_skeleton_data_result_get_error(spine_skeleton_data_result result) {
if (!result) return nullptr;
return ((_spine_skeleton_data_result *) result)->error;
}
spine_skeleton_data spine_skeleton_data_result_get_data(spine_skeleton_data_result result) {
if (!result) return nullptr;
return ((_spine_skeleton_data_result *) result)->skeletonData;
}
void spine_skeleton_data_result_dispose(spine_skeleton_data_result result) {
if (!result) return;
_spine_skeleton_data_result *_result = (_spine_skeleton_data_result *) result;
if (_result->error) {
SpineExtension::free(_result->error, __FILE__, __LINE__);
}
SpineExtension::free(_result, __FILE__, __LINE__);
}
// Skeleton drawable
spine_skeleton_drawable spine_skeleton_drawable_create(spine_skeleton_data skeletonData) {
if (!skeletonData) return nullptr;
_spine_skeleton_drawable *drawable = new (__FILE__, __LINE__) _spine_skeleton_drawable();
Skeleton *skeleton = new (__FILE__, __LINE__) Skeleton(*((SkeletonData *) skeletonData));
AnimationStateData *stateData = new (__FILE__, __LINE__) AnimationStateData((SkeletonData *) skeletonData);
AnimationState *state = new (__FILE__, __LINE__) AnimationState(stateData);
EventListener *listener = new (__FILE__, __LINE__) EventListener();
state->setListener(listener);
drawable->skeleton = (spine_skeleton) skeleton;
drawable->animationStateData = (spine_animation_state_data) stateData;
drawable->animationState = (spine_animation_state) state;
drawable->animationStateEvents = (spine_animation_state_events) listener;
drawable->renderer = new (__FILE__, __LINE__) SkeletonRenderer();
return (spine_skeleton_drawable) drawable;
}
spine_render_command spine_skeleton_drawable_render(spine_skeleton_drawable drawable) {
if (!drawable) return nullptr;
_spine_skeleton_drawable *_drawable = (_spine_skeleton_drawable *) drawable;
Skeleton *skeleton = (Skeleton *) _drawable->skeleton;
SkeletonRenderer *renderer = _drawable->renderer;
RenderCommand *commands = renderer->render(*skeleton);
return (spine_render_command) commands;
}
void spine_skeleton_drawable_dispose(spine_skeleton_drawable drawable) {
if (!drawable) return;
_spine_skeleton_drawable *_drawable = (_spine_skeleton_drawable *) drawable;
if (_drawable->renderer) {
delete _drawable->renderer;
}
if (_drawable->animationState) {
delete (AnimationState *) _drawable->animationState;
}
if (_drawable->animationStateData) {
delete (AnimationStateData *) _drawable->animationStateData;
}
if (_drawable->skeleton) {
delete (Skeleton *) _drawable->skeleton;
}
if (_drawable->animationStateEvents) {
delete (EventListener *) _drawable->animationStateEvents;
}
delete _drawable;
}
spine_skeleton spine_skeleton_drawable_get_skeleton(spine_skeleton_drawable drawable) {
if (!drawable) return nullptr;
return ((_spine_skeleton_drawable *) drawable)->skeleton;
}
spine_animation_state spine_skeleton_drawable_get_animation_state(spine_skeleton_drawable drawable) {
if (!drawable) return nullptr;
return ((_spine_skeleton_drawable *) drawable)->animationState;
}
spine_animation_state_data spine_skeleton_drawable_get_animation_state_data(spine_skeleton_drawable drawable) {
if (!drawable) return nullptr;
return ((_spine_skeleton_drawable *) drawable)->animationStateData;
}
spine_animation_state_events spine_skeleton_drawable_get_animation_state_events(spine_skeleton_drawable drawable) {
if (!drawable) return nullptr;
return ((_spine_skeleton_drawable *) drawable)->animationStateEvents;
}
// Render command functions
float *spine_render_command_get_positions(spine_render_command command) {
if (!command) return nullptr;
return ((RenderCommand *) command)->positions;
}
float *spine_render_command_get_uvs(spine_render_command command) {
if (!command) return nullptr;
return ((RenderCommand *) command)->uvs;
}
int32_t *spine_render_command_get_colors(spine_render_command command) {
if (!command) return nullptr;
return (int32_t *) ((RenderCommand *) command)->colors;
}
int32_t *spine_render_command_get_dark_colors(spine_render_command command) {
if (!command) return nullptr;
return (int32_t *) ((RenderCommand *) command)->darkColors;
}
int32_t spine_render_command_get_num_vertices(spine_render_command command) {
if (!command) return 0;
return ((RenderCommand *) command)->numVertices;
}
uint16_t *spine_render_command_get_indices(spine_render_command command) {
if (!command) return nullptr;
return ((RenderCommand *) command)->indices;
}
int32_t spine_render_command_get_num_indices(spine_render_command command) {
if (!command) return 0;
return ((RenderCommand *) command)->numIndices;
}
int32_t spine_render_command_get_atlas_page(spine_render_command command) {
if (!command) return 0;
return (int32_t) (intptr_t) ((RenderCommand *) command)->texture;
}
spine_blend_mode spine_render_command_get_blend_mode(spine_render_command command) {
if (!command) return SPINE_BLEND_MODE_NORMAL;
BlendMode mode = ((RenderCommand *) command)->blendMode;
switch (mode) {
case BlendMode_Normal: return SPINE_BLEND_MODE_NORMAL;
case BlendMode_Additive: return SPINE_BLEND_MODE_ADDITIVE;
case BlendMode_Multiply: return SPINE_BLEND_MODE_MULTIPLY;
case BlendMode_Screen: return SPINE_BLEND_MODE_SCREEN;
default: return SPINE_BLEND_MODE_NORMAL;
}
}
spine_render_command spine_render_command_get_next(spine_render_command command) {
if (!command) return nullptr;
return (spine_render_command) ((RenderCommand *) command)->next;
}
// Skin entries
spine_skin_entries spine_skin_entries_create() {
_spine_skin_entries *entries = SpineExtension::calloc<_spine_skin_entries>(1, __FILE__, __LINE__);
return (spine_skin_entries) entries;
}
void spine_skin_entries_dispose(spine_skin_entries entries) {
if (!entries) return;
_spine_skin_entries *_entries = (_spine_skin_entries *) entries;
if (_entries->entries) {
for (int i = 0; i < _entries->numEntries; i++) {
if (_entries->entries[i].name) {
SpineExtension::free(_entries->entries[i].name, __FILE__, __LINE__);
}
}
SpineExtension::free(_entries->entries, __FILE__, __LINE__);
}
SpineExtension::free(_entries, __FILE__, __LINE__);
}
int32_t spine_skin_entries_get_num_entries(spine_skin_entries entries) {
if (!entries) return 0;
return ((_spine_skin_entries *) entries)->numEntries;
}
spine_skin_entry spine_skin_entries_get_entry(spine_skin_entries entries, int32_t index) {
if (!entries) return nullptr;
_spine_skin_entries *_entries = (_spine_skin_entries *) entries;
if (index < 0 || index >= _entries->numEntries) return nullptr;
return (spine_skin_entry) &_entries->entries[index];
}
int32_t spine_skin_entry_get_slot_index(spine_skin_entry entry) {
if (!entry) return 0;
return ((_spine_skin_entry *) entry)->slotIndex;
}
const utf8 *spine_skin_entry_get_name(spine_skin_entry entry) {
if (!entry) return nullptr;
return ((_spine_skin_entry *) entry)->name;
}
spine_attachment spine_skin_entry_get_attachment(spine_skin_entry entry) {
if (!entry) return nullptr;
return ((_spine_skin_entry *) entry)->attachment;
}

View File

@ -1,194 +0,0 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#ifndef SPINE_C_NEW_CUSTOM_H
#define SPINE_C_NEW_CUSTOM_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
#if _WIN32
#define SPINE_C_EXPORT extern "C" __declspec(dllexport)
#else
#ifdef __EMSCRIPTEN__
#define SPINE_C_EXPORT extern "C" __attribute__((used))
#else
#define SPINE_C_EXPORT extern "C"
#endif
#endif
#else
#if _WIN32
#define SPINE_C_EXPORT __declspec(dllexport)
#else
#ifdef __EMSCRIPTEN__
#define SPINE_C_EXPORT __attribute__((used))
#else
#define SPINE_C_EXPORT
#endif
#endif
#endif
#define SPINE_OPAQUE_TYPE(name) \
typedef struct name##_wrapper { \
} name##_wrapper; \
typedef name##_wrapper *name;
typedef char utf8;
typedef int32_t spine_bool;
typedef size_t spine_size_t;
// Custom types for spine-c-new
SPINE_OPAQUE_TYPE(spine_atlas)
SPINE_OPAQUE_TYPE(spine_skeleton_data_result)
SPINE_OPAQUE_TYPE(spine_bounds)
SPINE_OPAQUE_TYPE(spine_vector)
SPINE_OPAQUE_TYPE(spine_color)
SPINE_OPAQUE_TYPE(spine_skeleton_drawable)
SPINE_OPAQUE_TYPE(spine_render_command)
SPINE_OPAQUE_TYPE(spine_skin_entry)
SPINE_OPAQUE_TYPE(spine_skin_entries)
SPINE_OPAQUE_TYPE(spine_rtti)
// Texture loader callbacks
typedef void* (*spine_texture_loader_load_func)(const char *path);
typedef void (*spine_texture_loader_unload_func)(void *texture);
// Version functions
SPINE_C_EXPORT int32_t spine_major_version();
SPINE_C_EXPORT int32_t spine_minor_version();
SPINE_C_EXPORT void spine_enable_debug_extension(spine_bool enable);
SPINE_C_EXPORT void spine_report_leaks();
// Color functions
SPINE_C_EXPORT float spine_color_get_r(spine_color color);
SPINE_C_EXPORT float spine_color_get_g(spine_color color);
SPINE_C_EXPORT float spine_color_get_b(spine_color color);
SPINE_C_EXPORT float spine_color_get_a(spine_color color);
// Bounds functions
SPINE_C_EXPORT float spine_bounds_get_x(spine_bounds bounds);
SPINE_C_EXPORT float spine_bounds_get_y(spine_bounds bounds);
SPINE_C_EXPORT float spine_bounds_get_width(spine_bounds bounds);
SPINE_C_EXPORT float spine_bounds_get_height(spine_bounds bounds);
// Vector functions
SPINE_C_EXPORT float spine_vector_get_x(spine_vector vector);
SPINE_C_EXPORT float spine_vector_get_y(spine_vector vector);
// Atlas functions
SPINE_C_EXPORT spine_atlas spine_atlas_load(const utf8 *atlasData);
SPINE_C_EXPORT spine_atlas spine_atlas_load_callback(const utf8 *atlasData, const utf8 *atlasDir,
spine_texture_loader_load_func load,
spine_texture_loader_unload_func unload);
SPINE_C_EXPORT int32_t spine_atlas_get_num_image_paths(spine_atlas atlas);
SPINE_C_EXPORT utf8 *spine_atlas_get_image_path(spine_atlas atlas, int32_t index);
SPINE_C_EXPORT spine_bool spine_atlas_is_pma(spine_atlas atlas);
SPINE_C_EXPORT utf8 *spine_atlas_get_error(spine_atlas atlas);
SPINE_C_EXPORT void spine_atlas_dispose(spine_atlas atlas);
// Forward declarations for types used in generated headers
struct spine_skeleton_data_wrapper;
typedef struct spine_skeleton_data_wrapper *spine_skeleton_data;
struct spine_timeline_wrapper;
typedef struct spine_timeline_wrapper *spine_timeline;
struct spine_skeleton_wrapper;
typedef struct spine_skeleton_wrapper *spine_skeleton;
struct spine_event_wrapper;
typedef struct spine_event_wrapper *spine_event;
struct spine_skin_wrapper;
typedef struct spine_skin_wrapper *spine_skin;
struct spine_sequence_wrapper;
typedef struct spine_sequence_wrapper *spine_sequence;
struct spine_attachment_wrapper;
typedef struct spine_attachment_wrapper *spine_attachment;
SPINE_C_EXPORT spine_skeleton_data_result spine_skeleton_data_load_json(spine_atlas atlas, const utf8 *skeletonData);
SPINE_C_EXPORT spine_skeleton_data_result spine_skeleton_data_load_binary(spine_atlas atlas, const uint8_t *skeletonData, int32_t length);
SPINE_C_EXPORT utf8 *spine_skeleton_data_result_get_error(spine_skeleton_data_result result);
SPINE_C_EXPORT spine_skeleton_data spine_skeleton_data_result_get_data(spine_skeleton_data_result result);
SPINE_C_EXPORT void spine_skeleton_data_result_dispose(spine_skeleton_data_result result);
// Skeleton drawable - these need forward declarations
struct spine_skeleton_wrapper;
typedef struct spine_skeleton_wrapper *spine_skeleton;
struct spine_animation_state_wrapper;
typedef struct spine_animation_state_wrapper *spine_animation_state;
struct spine_animation_state_data_wrapper;
typedef struct spine_animation_state_data_wrapper *spine_animation_state_data;
struct spine_animation_state_events_wrapper;
typedef struct spine_animation_state_events_wrapper *spine_animation_state_events;
SPINE_C_EXPORT spine_skeleton_drawable spine_skeleton_drawable_create(spine_skeleton_data skeletonData);
SPINE_C_EXPORT spine_render_command spine_skeleton_drawable_render(spine_skeleton_drawable drawable);
SPINE_C_EXPORT void spine_skeleton_drawable_dispose(spine_skeleton_drawable drawable);
SPINE_C_EXPORT spine_skeleton spine_skeleton_drawable_get_skeleton(spine_skeleton_drawable drawable);
SPINE_C_EXPORT spine_animation_state spine_skeleton_drawable_get_animation_state(spine_skeleton_drawable drawable);
SPINE_C_EXPORT spine_animation_state_data spine_skeleton_drawable_get_animation_state_data(spine_skeleton_drawable drawable);
SPINE_C_EXPORT spine_animation_state_events spine_skeleton_drawable_get_animation_state_events(spine_skeleton_drawable drawable);
// Render command functions
SPINE_C_EXPORT float *spine_render_command_get_positions(spine_render_command command);
SPINE_C_EXPORT float *spine_render_command_get_uvs(spine_render_command command);
SPINE_C_EXPORT int32_t *spine_render_command_get_colors(spine_render_command command);
SPINE_C_EXPORT int32_t *spine_render_command_get_dark_colors(spine_render_command command);
SPINE_C_EXPORT int32_t spine_render_command_get_num_vertices(spine_render_command command);
SPINE_C_EXPORT uint16_t *spine_render_command_get_indices(spine_render_command command);
SPINE_C_EXPORT int32_t spine_render_command_get_num_indices(spine_render_command command);
SPINE_C_EXPORT int32_t spine_render_command_get_atlas_page(spine_render_command command);
// Forward declaration for spine_blend_mode enum
typedef enum spine_blend_mode {
SPINE_BLEND_MODE_NORMAL = 0,
SPINE_BLEND_MODE_ADDITIVE,
SPINE_BLEND_MODE_MULTIPLY,
SPINE_BLEND_MODE_SCREEN
} spine_blend_mode;
// Forward declarations for other enum types used in generated headers
typedef enum spine_mix_blend spine_mix_blend;
typedef enum spine_mix_direction spine_mix_direction;
SPINE_C_EXPORT spine_blend_mode spine_render_command_get_blend_mode(spine_render_command command);
SPINE_C_EXPORT spine_render_command spine_render_command_get_next(spine_render_command command);
// Skin entries - these need forward declarations
struct spine_attachment_wrapper;
typedef struct spine_attachment_wrapper *spine_attachment;
SPINE_C_EXPORT spine_skin_entries spine_skin_entries_create();
SPINE_C_EXPORT void spine_skin_entries_dispose(spine_skin_entries entries);
SPINE_C_EXPORT int32_t spine_skin_entries_get_num_entries(spine_skin_entries entries);
SPINE_C_EXPORT spine_skin_entry spine_skin_entries_get_entry(spine_skin_entries entries, int32_t index);
SPINE_C_EXPORT int32_t spine_skin_entry_get_slot_index(spine_skin_entry entry);
SPINE_C_EXPORT const utf8 *spine_skin_entry_get_name(spine_skin_entry entry);
SPINE_C_EXPORT spine_attachment spine_skin_entry_get_attachment(spine_skin_entry entry);
#endif // SPINE_C_NEW_CUSTOM_H

View File

@ -1,670 +0,0 @@
#!/usr/bin/env node
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const scriptDir = path.dirname(path.resolve(__filename));
const spineIncludeDir = path.join(scriptDir, 'spine-cpp', 'include');
function showHelp() {
console.log(`
extract-spine-cpp-types.js - Extract type information from spine-cpp header files
USAGE:
extract-spine-cpp-types.js [<header-file>]
extract-spine-cpp-types.js --help
DESCRIPTION:
Extracts classes, structs, enums, and their public members from C++ headers.
Without arguments, processes all headers in spine-cpp/include.
In all-files mode, performs two passes:
1. Extract all type definitions
2. Add inherited methods from supertypes (including template instantiations)
OUTPUT FORMAT:
Single file mode: Array of type definitions
All files mode: Object with relative file paths as keys
TYPE DEFINITION STRUCTURE:
{
"name": "ClassName", // Type name
"kind": "class" | "struct" | "enum", // Type kind
"loc": { // Source location
"line": 45,
"col": 15
},
"superTypes": ["BaseClass", "Interface"], // Base classes (optional)
"members": [...], // Public members (classes/structs)
"values": [...], // Enum constants (enums only)
"isTemplate": true, // Present if type is a template
"templateParams": ["T", "U"], // Template parameter names (templates only)
"isAbstract": true // Present if class has pure virtual methods
}
MEMBER STRUCTURE:
Fields:
{
"kind": "field",
"name": "fieldName",
"type": "int"
}
Methods:
{
"kind": "method",
"name": "methodName",
"returnType": "void",
"parameters": [
{"name": "param1", "type": "int"},
{"name": "param2", "type": "const String &"}
],
"isStatic": false,
"isVirtual": true,
"isPure": false,
"fromSupertype": "BaseClass" // Present if inherited (all-files mode only)
}
Constructors:
{
"kind": "constructor",
"name": "ClassName",
"parameters": [...]
}
ENUM VALUES:
{
"name": "ENUM_VALUE",
"value": "1 << 0" // Present only if explicitly initialized
}
INHERITANCE (all-files mode only):
- Methods inherited from non-template supertypes are included
- Methods inherited from template supertypes have parameters substituted
- Full inheritance hierarchy is preserved (grandparent methods appear via parent)
- SpineObject methods are excluded (considered noise)
- Methods are marked with "fromSupertype" field indicating immediate source
- Template supertypes appear as "TemplateName<Type1, Type2>"
- Example: If C inherits B inherits A, C gets all methods from A and B
NOTES:
- Only public members are extracted
- Forward declarations are excluded
- Template instantiations (e.g., Vector<int>) are excluded
- Friend declarations are excluded
- Implicit/compiler-generated methods are excluded
- Destructors and operator methods are excluded (not needed for C wrapper generation)
EXAMPLES:
# Extract types from single file
extract-spine-cpp-types.js spine-cpp/include/spine/Animation.h > animation.json
# Extract all types with inheritance resolution
extract-spine-cpp-types.js > all-spine-types.json
# Query specific type
extract-spine-cpp-types.js > types.json
jq '."spine/Bone.h"[] | select(.name == "Bone")' types.json
`);
}
function extractLocalTypes(headerFile, typeMap = null) {
const absHeaderPath = path.resolve(headerFile);
const sourceContent = fs.readFileSync(absHeaderPath, 'utf8');
const sourceLines = sourceContent.split('\n');
// Get AST from clang
const cmd = `clang++ -Xclang -ast-dump=json -fsyntax-only -std=c++11 -I "${spineIncludeDir}" "${absHeaderPath}" 2>/dev/null`;
const maxBuffer = headerFile.includes('Debug.h') ? 500 : 200; // MB
let astJson;
try {
const output = execSync(cmd, { encoding: 'utf8', maxBuffer: maxBuffer * 1024 * 1024 });
astJson = JSON.parse(output);
} catch (error) {
throw new Error(error.code === 'ENOBUFS'
? `AST output too large (>${maxBuffer}MB)`
: error.message);
}
const types = [];
function extractEnumValueFromSource(enumConstNode) {
if (!enumConstNode.loc) return undefined;
const line = sourceLines[enumConstNode.loc.line - 1];
if (!line) return undefined;
// Find enum name and check for '='
const nameMatch = line.match(new RegExp(`\\b${enumConstNode.name}\\b`));
if (!nameMatch) return undefined;
const afterName = line.substring(nameMatch.index + enumConstNode.name.length);
const equalIndex = afterName.indexOf('=');
if (equalIndex === -1) return null; // No explicit value
// Extract value expression
let valueText = afterName.substring(equalIndex + 1);
// Handle multi-line values
let currentLine = enumConstNode.loc.line;
while (currentLine < sourceLines.length && !valueText.match(/[,}]/)) {
valueText += '\n' + sourceLines[currentLine++];
}
// Extract up to comma or brace
const endMatch = valueText.match(/^(.*?)([,}])/s);
if (endMatch) valueText = endMatch[1];
// Clean up
return valueText
.replace(/\/\/.*$/gm, '') // Remove single-line comments
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
.replace(/\s+/g, ' ') // Normalize whitespace
.trim();
}
function extractReturnType(methodNode) {
const fullType = methodNode.type?.qualType || '';
const match = fullType.match(/^(.+?)\s*\(/);
return match ? match[1].trim() : 'void';
}
function extractParameters(methodNode) {
return (methodNode.inner || [])
.filter(n => n.kind === 'ParmVarDecl')
.map(n => ({
name: n.name || '',
type: n.type?.qualType || ''
}));
}
function isInTargetFile(node) {
if (!node.loc) return false;
const targetPath = path.resolve(headerFile);
const loc = node.loc;
// Check direct file location
if (loc.file) return path.resolve(loc.file) === targetPath;
// Check macro locations
for (const locType of ['spellingLoc', 'expansionLoc']) {
if (loc[locType]) {
if (loc[locType].includedFrom) return false;
if (loc[locType].file) return path.resolve(loc[locType].file) === targetPath;
}
}
// If included from another file, reject
if (loc.includedFrom) return false;
// No location info - assume it's in the main file
return true;
}
function extractTypeInfo(node) {
const info = {
name: node.name || '',
kind: node.kind === 'EnumDecl' ? 'enum' : (node.tagUsed || ''),
loc: {
line: node.loc?.line || 0,
col: node.loc?.col || 0
}
};
// Extract base classes
if (node.bases?.length > 0) {
info.superTypes = node.bases.map(b => b.type?.qualType || '').filter(Boolean);
}
// For enums, extract the values
if (node.kind === 'EnumDecl') {
info.values = (node.inner || [])
.filter(n => n.kind === 'EnumConstantDecl')
.map(n => {
const enumValue = { name: n.name || '' };
const sourceValue = extractEnumValueFromSource(n);
if (sourceValue === null) {
// Implicit value - no value property
} else if (sourceValue) {
enumValue.value = sourceValue;
} else if (n.inner?.length > 0) {
enumValue.value = "<<extraction failed>>";
}
return enumValue;
});
return info;
}
// For classes/structs, extract public members
info.members = [];
let currentAccess = node.tagUsed === 'struct' ? 'public' : 'private';
let hasPureVirtual = false;
for (const inner of node.inner || []) {
if (inner.kind === 'AccessSpecDecl') {
currentAccess = inner.access || 'private';
continue;
}
if (inner.kind === 'FriendDecl' || currentAccess !== 'public') continue;
const member = extractMember(inner, node);
if (member) {
info.members.push(member);
// Check if this is a pure virtual method
if (member.kind === 'method' && member.isPure) {
hasPureVirtual = true;
}
}
}
// Always set isAbstract to a boolean value
info.isAbstract = hasPureVirtual;
return info;
}
function extractMember(inner, parent) {
if (inner.isImplicit) return null;
switch (inner.kind) {
case 'FieldDecl':
return {
kind: 'field',
name: inner.name || '',
type: inner.type?.qualType || ''
};
case 'CXXMethodDecl':
if (!inner.name) return null;
// Skip operators - not needed for C wrapper generation
if (inner.name.startsWith('operator')) return null;
return {
kind: 'method',
name: inner.name,
returnType: extractReturnType(inner),
parameters: extractParameters(inner),
isStatic: inner.storageClass === 'static',
isVirtual: inner.virtual || false,
isPure: inner.pure || false
};
case 'CXXConstructorDecl':
return {
kind: 'constructor',
name: inner.name || parent.name || '',
parameters: extractParameters(inner)
};
case 'CXXDestructorDecl':
// Skip destructors - not needed for C wrapper generation
return null;
default:
return null;
}
}
function processNode(node, inSpineNamespace = false) {
if (!node || typeof node !== 'object') return;
// Handle spine namespace
if (node.kind === 'NamespaceDecl' && node.name === 'spine') {
(node.inner || []).forEach(n => processNode(n, true));
return;
}
// Recurse to find spine namespace
if (!inSpineNamespace) {
(node.inner || []).forEach(n => processNode(n, false));
return;
}
// Process type declarations
const typeKinds = ['CXXRecordDecl', 'ClassTemplateDecl', 'EnumDecl', 'TypedefDecl', 'TypeAliasDecl'];
if (!typeKinds.includes(node.kind)) return;
// Skip if not in target file or invalid
if (!isInTargetFile(node) ||
node.isImplicit ||
!node.name ||
node.name.startsWith('_') ||
node.name.includes('<')) return;
// Skip forward declarations
if (node.previousDecl && (!node.inner || node.inner.length === 0)) return;
// Extract type info
if (node.kind === 'ClassTemplateDecl') {
const classNode = (node.inner || []).find(n => n.kind === 'CXXRecordDecl');
if (classNode) {
const typeInfo = extractTypeInfo(classNode);
typeInfo.isTemplate = true;
// Extract template parameters
const templateParams = [];
for (const inner of node.inner || []) {
if (inner.kind === 'TemplateTypeParmDecl' && inner.name) {
templateParams.push(inner.name);
}
}
if (templateParams.length > 0) {
typeInfo.templateParams = templateParams;
}
types.push(typeInfo);
}
} else if (node.kind === 'CXXRecordDecl' && node.inner?.length > 0) {
const typeInfo = extractTypeInfo(node);
// Ensure isTemplate is always set for non-template classes
if (typeInfo.isTemplate === undefined) {
typeInfo.isTemplate = false;
}
types.push(typeInfo);
} else if (node.kind === 'EnumDecl') {
types.push(extractTypeInfo(node));
} else if (node.kind === 'TypedefDecl' || node.kind === 'TypeAliasDecl') {
types.push(extractTypeInfo(node));
}
}
processNode(astJson);
// Filter out forward declarations and sort by line number
const filteredTypes = types
.filter(t => !(t.members && t.members.length === 0))
.sort((a, b) => a.loc.line - b.loc.line);
// Add inherited methods if we have a type map
if (typeMap) {
for (const type of filteredTypes) {
if (type.superTypes && type.members) {
addInheritedMethods(type, typeMap);
}
}
}
return filteredTypes;
}
function addInheritedMethods(type, typeMap) {
const inheritedMethods = [];
const ownMethodSignatures = new Set();
// Build a set of method signatures from this type
for (const member of type.members) {
if (member.kind === 'method') {
const sig = getMethodSignature(member);
ownMethodSignatures.add(sig);
}
}
// Process each supertype
for (const superTypeName of type.superTypes) {
// Clean up the supertype name (remove namespaces, etc)
const cleanName = superTypeName.replace(/^.*::/, '');
// Skip SpineObject inheritance - it's just noise
if (cleanName === 'SpineObject') continue;
// Check if this is a template supertype
const templateMatch = cleanName.match(/^([^<]+)<(.+)>$/);
if (templateMatch) {
const templateClassName = templateMatch[1];
const templateArgs = templateMatch[2];
const templateType = typeMap.get(templateClassName);
if (templateType && templateType.isTemplate && templateType.members) {
// Process template inheritance
addTemplateInheritedMethods(
type, templateType, templateClassName, templateArgs,
inheritedMethods, ownMethodSignatures
);
} else if (templateType && !templateType.isTemplate) {
// Template might not be marked as isTemplate, try anyway
addTemplateInheritedMethods(
type, templateType, templateClassName, templateArgs,
inheritedMethods, ownMethodSignatures
);
}
} else {
// Non-template supertype
const superType = typeMap.get(cleanName);
if (!superType || !superType.members) continue;
// Add non-overridden methods from supertype
for (const member of superType.members) {
if (member.kind === 'method') {
const sig = getMethodSignature(member);
if (!ownMethodSignatures.has(sig)) {
const inheritedMember = { ...member, fromSupertype: cleanName };
inheritedMethods.push(inheritedMember);
ownMethodSignatures.add(sig); // Prevent duplicates from multiple inheritance
}
}
}
}
}
// Add inherited methods to the type
type.members.push(...inheritedMethods);
}
function addTemplateInheritedMethods(
type, templateType, templateClassName, templateArgs,
inheritedMethods, ownMethodSignatures
) {
// Parse template arguments (handle multiple args)
const argsList = [];
let depth = 0;
let currentArg = '';
for (const char of templateArgs) {
if (char === '<') depth++;
else if (char === '>') depth--;
if (char === ',' && depth === 0) {
argsList.push(currentArg.trim());
currentArg = '';
} else {
currentArg += char;
}
}
if (currentArg.trim()) {
argsList.push(currentArg.trim());
}
// Build a mapping of template params to actual types
const paramMap = new Map();
// Use the actual template parameters if we have them
if (templateType.templateParams && templateType.templateParams.length === argsList.length) {
templateType.templateParams.forEach((param, i) => {
paramMap.set(param, argsList[i]);
});
} else {
// Fallback: if we don't have template param info, skip substitution
console.error(`Warning: Template ${templateClassName} missing parameter info, skipping substitution`);
return;
}
// Process each member of the template
for (const member of templateType.members) {
if (member.kind === 'method') {
// Skip template constructors - they have weird names like "Pose<P>"
if (member.name.includes('<')) continue;
// Clone the member and substitute template parameters
const inheritedMember = JSON.parse(JSON.stringify(member));
inheritedMember.fromSupertype = `${templateClassName}<${templateArgs}>`;
// Replace template parameters in return type
if (inheritedMember.returnType) {
inheritedMember.returnType = substituteTemplateParams(
inheritedMember.returnType, paramMap
);
}
// Replace template parameters in parameters
if (inheritedMember.parameters) {
for (const param of inheritedMember.parameters) {
param.type = substituteTemplateParams(param.type, paramMap);
}
}
// Check if this method is overridden
const sig = getMethodSignature(inheritedMember);
if (!ownMethodSignatures.has(sig)) {
inheritedMethods.push(inheritedMember);
ownMethodSignatures.add(sig);
}
}
}
}
function substituteTemplateParams(typeStr, paramMap) {
let result = typeStr;
// Replace template parameters in order of length (longest first)
// to avoid replacing substrings (e.g., V before V1)
const sortedParams = Array.from(paramMap.keys()).sort((a, b) => b.length - a.length);
for (const param of sortedParams) {
const regex = new RegExp(`\\b${param}\\b`, 'g');
result = result.replace(regex, paramMap.get(param));
}
return result;
}
function getMethodSignature(method) {
// Create a signature that identifies method overrides
// For virtual methods, just use the name
// For non-virtual methods, include parameter types
let sig = method.name;
if (method.parameters && method.parameters.length > 0) {
sig += '(' + method.parameters.map(p => p.type).join(',') + ')';
} else {
sig += '()';
}
// Add const qualifier if present
if (method.returnType && method.returnType.includes('const')) {
sig += ' const';
}
return sig;
}
function findAllHeaderFiles() {
const headers = [];
function walkDir(dir) {
fs.readdirSync(dir).forEach(file => {
const fullPath = path.join(dir, file);
if (fs.statSync(fullPath).isDirectory()) {
walkDir(fullPath);
} else if (file.endsWith('.h') && file !== 'spine.h') {
headers.push(fullPath);
}
});
}
walkDir(spineIncludeDir);
return headers.sort();
}
// Main execution
const arg = process.argv[2];
if (arg === '--help' || arg === '-h') {
showHelp();
process.exit(0);
}
if (arg) {
// Single file mode - no inheritance support
if (!fs.existsSync(arg)) {
console.error(`Error: File not found: ${arg}`);
process.exit(1);
}
try {
const types = extractLocalTypes(arg);
if (types.length === 0) {
console.error('No types found in the specified file');
process.exit(1);
}
console.log(JSON.stringify(types, null, 2));
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
} else {
// Process all files
const allHeaders = findAllHeaderFiles();
const allTypes = {};
let processed = 0, errors = 0;
console.error(`Processing ${allHeaders.length} header files...`);
// First pass: extract all types without inheritance
const typeMap = new Map();
for (const headerFile of allHeaders) {
const relPath = path.relative(spineIncludeDir, headerFile);
process.stderr.write(`\r\x1b[K Pass 1 - Processing ${++processed}/${allHeaders.length}: ${relPath}...`);
try {
const types = extractLocalTypes(headerFile);
if (types.length > 0) {
allTypes[relPath] = types;
// Build type map
for (const type of types) {
typeMap.set(type.name, type);
}
}
} catch (error) {
errors++;
console.error(`\n ERROR processing ${relPath}: ${error.message}`);
}
}
// Second pass: add inherited methods
console.error(`\n Pass 2 - Adding inherited methods...`);
processed = 0;
for (const headerFile of allHeaders) {
const relPath = path.relative(spineIncludeDir, headerFile);
if (!allTypes[relPath]) continue;
process.stderr.write(`\r\x1b[K Pass 2 - Processing ${++processed}/${Object.keys(allTypes).length}: ${relPath}...`);
for (const type of allTypes[relPath]) {
if (type.superTypes && type.members) {
addInheritedMethods(type, typeMap);
// Check if any inherited methods are pure virtual
// If so, and the class doesn't override them, it's abstract
if (!type.isAbstract) {
const hasPureVirtual = type.members.some(m =>
m.kind === 'method' && m.isPure === true
);
if (hasPureVirtual) {
type.isAbstract = true;
}
}
}
}
}
console.error(`\n Completed: ${Object.keys(allTypes).length} files processed, ${errors} errors`);
console.log(JSON.stringify(allTypes, null, 2));
}