diff --git a/spine-c-new/codegen/exclusions.txt b/spine-c-new/codegen/exclusions.txt index 24f4151a3..532db031f 100644 --- a/spine-c-new/codegen/exclusions.txt +++ b/spine-c-new/codegen/exclusions.txt @@ -48,7 +48,7 @@ method: EventData::setAudioPath method: EventData::setVolume method: EventData::setBalance -# Vector methods need special handling +# Array methods need special handling method: AttachmentTimeline.getAttachmentNames # BoneLocal/BonePose setScale is overloaded in a confusing way diff --git a/spine-c-new/codegen/src/array-scanner.ts b/spine-c-new/codegen/src/array-scanner.ts index d0c531353..6b4aa6ab3 100644 --- a/spine-c-new/codegen/src/array-scanner.ts +++ b/spine-c-new/codegen/src/array-scanner.ts @@ -18,18 +18,18 @@ export interface ArraySpecialization { export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[], enumTypes: Set): ArraySpecialization[] { const arrayTypes = new Set(); const warnings: string[] = []; - + // Extract Array from a type string function extractArrayTypes(typeStr: string | undefined) { if (!typeStr) return; - + const regex = /Array<([^>]+)>/g; let match; while ((match = regex.exec(typeStr)) !== null) { arrayTypes.add(match[0]); } } - + // Process all types for (const header of Object.keys(typesJson)) { for (const type of typesJson[header]) { @@ -37,62 +37,69 @@ export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[ if (isTypeExcluded(type.name, exclusions) || type.isTemplate) { continue; } - + if (!type.members) continue; - + for (const member of type.members) { - extractArrayTypes(member.returnType); - extractArrayTypes(member.type); - - if (member.parameters) { - for (const param of member.parameters) { - extractArrayTypes(param.type); - } + switch (member.kind) { + case 'method': + extractArrayTypes(member.returnType); + if (member.parameters) { + for (const param of member.parameters) { + extractArrayTypes(param.type); + } + } + break; + case 'field': + extractArrayTypes(member.type); + break; + default: + break; } } } } - + // Convert to specializations const specializations: ArraySpecialization[] = []; - + for (const arrayType of arrayTypes) { const elementMatch = arrayType.match(/Array<(.+)>$/); if (!elementMatch) continue; - + const elementType = elementMatch[1].trim(); - + // Skip template placeholders if (elementType === 'T' || elementType === 'K') { continue; } - + // Handle nested arrays - emit warning if (elementType.startsWith('Array<')) { warnings.push(`Skipping nested array: ${arrayType} - manual handling required`); continue; } - + // Handle String arrays - emit warning if (elementType === 'String') { warnings.push(`Skipping String array: ${arrayType} - should be fixed in spine-cpp`); continue; } - + // Determine type characteristics const isPointer = elementType.endsWith('*'); let cleanElementType = isPointer ? elementType.slice(0, -1).trim() : elementType; - + // Remove "class " or "struct " prefix if present cleanElementType = cleanElementType.replace(/^(?:class|struct)\s+/, ''); const isEnum = enumTypes.has(cleanElementType) || cleanElementType === 'PropertyId'; - const isPrimitive = !isPointer && !isEnum && + const isPrimitive = !isPointer && !isEnum && ['int', 'float', 'double', 'bool', 'char', 'unsigned short', 'size_t'].includes(cleanElementType); - + // Generate C type names let cTypeName: string; let cElementType: string; - + if (isPrimitive) { // Map primitive types const typeMap: { [key: string]: string } = { @@ -127,7 +134,7 @@ export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[ warnings.push(`Unknown array element type: ${elementType}`); continue; } - + specializations.push({ cppType: arrayType, elementType: elementType, @@ -138,7 +145,7 @@ export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[ isPrimitive: isPrimitive }); } - + // Print warnings if (warnings.length > 0) { console.log('\nArray Generation Warnings:'); @@ -147,9 +154,9 @@ export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[ } console.log(''); } - + // Sort by C type name for consistent output specializations.sort((a, b) => a.cTypeName.localeCompare(b.cTypeName)); - + return specializations; } \ No newline at end of file diff --git a/spine-c-new/codegen/src/find-array-types.ts b/spine-c-new/codegen/src/find-array-types.ts deleted file mode 100644 index 75ced0dda..000000000 --- a/spine-c-new/codegen/src/find-array-types.ts +++ /dev/null @@ -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 types -const arrayTypes = new Set(); - -// Function to extract Array 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 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}`); -} \ No newline at end of file diff --git a/spine-c-new/codegen/src/generators/array-generator.ts b/spine-c-new/codegen/src/generators/array-generator.ts index ec8338799..30fd5a9d3 100644 --- a/spine-c-new/codegen/src/generators/array-generator.ts +++ b/spine-c-new/codegen/src/generators/array-generator.ts @@ -1,9 +1,9 @@ -import { Type, Member, toSnakeCase, toCTypeName } from '../types'; +import { Type, Member, toSnakeCase, Method } from '../types'; import { ArraySpecialization } from '../array-scanner'; export class ArrayGenerator { private arrayType: Type | undefined; - + constructor(private typesJson: any) { // Find the Array type definition for (const header of Object.keys(typesJson)) { @@ -14,19 +14,19 @@ export class ArrayGenerator { } } } - + /** * Generates arrays.h and arrays.cpp content */ generate(specializations: ArraySpecialization[]): { header: string[], source: string[] } { const header: string[] = []; const source: string[] = []; - + if (!this.arrayType) { console.error('ERROR: Array type not found in spine-cpp types'); return { header, source }; } - + // Header file header.push('#ifndef SPINE_C_ARRAYS_H'); header.push('#define SPINE_C_ARRAYS_H'); @@ -38,7 +38,7 @@ export class ArrayGenerator { header.push('extern "C" {'); header.push('#endif'); header.push(''); - + // Source file source.push('#include "arrays.h"'); source.push('#include '); @@ -46,50 +46,48 @@ export class ArrayGenerator { source.push(''); source.push('using namespace spine;'); source.push(''); - + // Generate for each specialization for (const spec of specializations) { console.log(`Generating array specialization: ${spec.cTypeName}`); this.generateSpecialization(spec, header, source); } - + // Close header header.push('#ifdef __cplusplus'); header.push('}'); header.push('#endif'); header.push(''); header.push('#endif // SPINE_C_ARRAYS_H'); - + return { header, source }; } - + private generateSpecialization(spec: ArraySpecialization, header: string[], source: string[]) { // Opaque type declaration header.push(`// ${spec.cppType}`); header.push(`SPINE_OPAQUE_TYPE(${spec.cTypeName})`); header.push(''); - + // Get Array methods to wrap - const methods = this.arrayType!.members?.filter(m => - m.kind === 'method' && - !m.isStatic && - !m.name.includes('operator') - ) || []; - + const methods = this.arrayType!.members?.filter(m => + m.kind === 'method' + ).filter(m => !m.isStatic && !m.name.includes('operator')) || []; + // Generate create method (constructor) header.push(`SPINE_C_EXPORT ${spec.cTypeName} ${spec.cTypeName}_create();`); source.push(`${spec.cTypeName} ${spec.cTypeName}_create() {`); source.push(` return (${spec.cTypeName}) new (__FILE__, __LINE__) ${spec.cppType}();`); source.push('}'); source.push(''); - + // Generate create with capacity header.push(`SPINE_C_EXPORT ${spec.cTypeName} ${spec.cTypeName}_create_with_capacity(int32_t capacity);`); source.push(`${spec.cTypeName} ${spec.cTypeName}_create_with_capacity(int32_t capacity) {`); source.push(` return (${spec.cTypeName}) new (__FILE__, __LINE__) ${spec.cppType}(capacity);`); source.push('}'); source.push(''); - + // Generate dispose header.push(`SPINE_C_EXPORT void ${spec.cTypeName}_dispose(${spec.cTypeName} array);`); source.push(`void ${spec.cTypeName}_dispose(${spec.cTypeName} array) {`); @@ -97,7 +95,7 @@ export class ArrayGenerator { source.push(` delete (${spec.cppType}*) array;`); source.push('}'); source.push(''); - + // Generate hardcoded get/set methods header.push(`SPINE_C_EXPORT ${spec.cElementType} ${spec.cTypeName}_get(${spec.cTypeName} array, int32_t index);`); source.push(`${spec.cElementType} ${spec.cTypeName}_get(${spec.cTypeName} array, int32_t index) {`); @@ -106,7 +104,7 @@ export class ArrayGenerator { source.push(` return ${this.convertFromCpp(spec, '(*_array)[index]')};`); source.push('}'); source.push(''); - + header.push(`SPINE_C_EXPORT void ${spec.cTypeName}_set(${spec.cTypeName} array, int32_t index, ${spec.cElementType} value);`); source.push(`void ${spec.cTypeName}_set(${spec.cTypeName} array, int32_t index, ${spec.cElementType} value) {`); source.push(` if (!array) return;`); @@ -114,22 +112,22 @@ export class ArrayGenerator { source.push(` (*_array)[index] = ${this.convertToCpp(spec, 'value')};`); source.push('}'); source.push(''); - + // Generate wrapper for each Array method for (const method of methods) { this.generateMethodWrapper(spec, method, header, source); } - + 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 if (method.name === 'Array' || method.name === '~Array') return; - + // Build C function name const cFuncName = `${spec.cTypeName}_${toSnakeCase(method.name)}`; - + // Convert return type let returnType = 'void'; let hasReturn = false; @@ -154,14 +152,14 @@ export class ArrayGenerator { return; } } - + // Build parameter list const cParams: string[] = [`${spec.cTypeName} array`]; const cppArgs: string[] = []; - + if (method.parameters) { for (const param of method.parameters) { - if (param.type === 'T' || param.type === spec.elementType || + if (param.type === 'T' || param.type === spec.elementType || param.type === 'const T &' || param.type === `const ${spec.elementType} &`) { cParams.push(`${spec.cElementType} ${param.name}`); cppArgs.push(this.convertToCpp(spec, param.name)); @@ -178,15 +176,15 @@ export class ArrayGenerator { } } } - + // Generate declaration header.push(`SPINE_C_EXPORT ${returnType} ${cFuncName}(${cParams.join(', ')});`); - + // Generate implementation source.push(`${returnType} ${cFuncName}(${cParams.join(', ')}) {`); source.push(` if (!array) return${hasReturn ? ' ' + this.getDefaultReturn(returnType, spec) : ''};`); source.push(` ${spec.cppType} *_array = (${spec.cppType}*) array;`); - + const call = `_array->${method.name}(${cppArgs.join(', ')})`; if (hasReturn) { if (returnType === spec.cElementType) { @@ -197,11 +195,11 @@ export class ArrayGenerator { } else { source.push(` ${call};`); } - + source.push('}'); source.push(''); } - + private getDefaultValue(spec: ArraySpecialization): string { if (spec.isPointer) return 'nullptr'; if (spec.isPrimitive) { @@ -211,7 +209,7 @@ export class ArrayGenerator { if (spec.isEnum) return '0'; return '0'; } - + private getDefaultReturn(returnType: string, spec: ArraySpecialization): string { if (returnType === 'bool') return 'false'; if (returnType === 'size_t' || returnType === 'int32_t') return '0'; @@ -219,7 +217,7 @@ export class ArrayGenerator { if (returnType === spec.cTypeName) return 'nullptr'; return '0'; } - + private convertFromCpp(spec: ArraySpecialization, expr: string): string { if (spec.isPointer) { return `(${spec.cElementType}) ${expr}`; @@ -229,7 +227,7 @@ export class ArrayGenerator { } return expr; } - + private convertToCpp(spec: ArraySpecialization, expr: string): string { if (spec.isPointer) { return `(${spec.elementType}) ${expr}`; diff --git a/spine-c-new/codegen/src/generators/constructor-generator.ts b/spine-c-new/codegen/src/generators/constructor-generator.ts index 68e5a58b1..2df9b7411 100644 --- a/spine-c-new/codegen/src/generators/constructor-generator.ts +++ b/spine-c-new/codegen/src/generators/constructor-generator.ts @@ -1,4 +1,4 @@ -import { Type, Member, toSnakeCase, toCFunctionName, toCTypeName } from '../types'; +import { Type, Member, toSnakeCase, toCFunctionName, toCTypeName, Constructor } from '../types'; export interface GeneratorResult { declarations: string[]; @@ -6,15 +6,17 @@ export interface GeneratorResult { } export class ConstructorGenerator { + constructor(private validTypes: Set) {} + generate(type: Type): GeneratorResult { const declarations: string[] = []; const implementations: string[] = []; - + if (!type.members) return { declarations, implementations }; - + const constructors = type.members.filter(m => m.kind === 'constructor'); const cTypeName = `spine_${toSnakeCase(type.name)}`; - + // Skip constructor generation for abstract types if (!type.isAbstract) { // Generate create functions for each constructor @@ -22,79 +24,79 @@ export class ConstructorGenerator { for (const constructor of constructors) { const funcName = this.getCreateFunctionName(type.name, constructor, constructorIndex); const params = this.generateParameters(constructor); - + // Declaration declarations.push(`SPINE_C_EXPORT ${cTypeName} ${funcName}(${params.declaration});`); - + // Implementation implementations.push(`${cTypeName} ${funcName}(${params.declaration}) {`); implementations.push(` ${type.name} *obj = new (__FILE__, __LINE__) ${type.name}(${params.call});`); implementations.push(` return (${cTypeName}) obj;`); implementations.push(`}`); implementations.push(''); - + constructorIndex++; } } - + // Always generate dispose function declarations.push(`SPINE_C_EXPORT void ${cTypeName}_dispose(${cTypeName} obj);`); - + implementations.push(`void ${cTypeName}_dispose(${cTypeName} obj) {`); implementations.push(` if (!obj) return;`); implementations.push(` delete (${type.name} *) obj;`); implementations.push(`}`); implementations.push(''); - + 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`; - + if (!constructor.parameters || constructor.parameters.length === 0) { return baseName; } - + if (index === 0) { return baseName; } - + // Generate name based on parameter types const paramNames = constructor.parameters .map(p => this.getParamTypeSuffix(p.type)) .join('_'); - + return `${baseName}_with_${paramNames}`; } - + private getParamTypeSuffix(type: string): string { if (type.includes('float')) return 'float'; if (type.includes('int')) return 'int'; if (type.includes('bool')) return 'bool'; if (type.includes('String') || type.includes('char')) return 'string'; - + // Extract class name from pointers/references const match = type.match(/(?:const\s+)?(\w+)(?:\s*[*&])?/); if (match) { return toSnakeCase(match[1]); } - + 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) { return { declaration: 'void', call: '' }; } - + const declParts: string[] = []; const callParts: string[] = []; - + for (const param of constructor.parameters) { - const cType = toCTypeName(param.type); + const cType = toCTypeName(param.type, this.validTypes); declParts.push(`${cType} ${param.name}`); - + // Convert C type back to C++ for the call let callExpr = param.name; if (param.type === 'const String &' || param.type === 'String') { @@ -106,10 +108,10 @@ export class ConstructorGenerator { const baseType = param.type.replace(/^(?:const\s+)?(.+?)\s*&$/, '$1').trim(); callExpr = `*(${baseType}*) ${param.name}`; } - + callParts.push(callExpr); } - + return { declaration: declParts.join(', '), call: callParts.join(', ') diff --git a/spine-c-new/codegen/src/generators/method-generator.ts b/spine-c-new/codegen/src/generators/method-generator.ts index acc4b08cd..5b9712d72 100644 --- a/spine-c-new/codegen/src/generators/method-generator.ts +++ b/spine-c-new/codegen/src/generators/method-generator.ts @@ -1,24 +1,23 @@ -import { Type, Member, toSnakeCase, toCFunctionName, toCTypeName, Exclusion } from '../types'; +import { Type, Member, toSnakeCase, toCFunctionName, toCTypeName, Exclusion, Method } from '../types'; import { isMethodExcluded } from '../exclusions'; import { GeneratorResult } from './constructor-generator'; export class MethodGenerator { - constructor(private exclusions: Exclusion[]) {} - + constructor(private exclusions: Exclusion[], private validTypes: Set) {} + generate(type: Type): GeneratorResult { const declarations: string[] = []; const implementations: string[] = []; - + if (!type.members) return { declarations, implementations }; - - const methods = type.members.filter(m => - m.kind === 'method' && - !m.isStatic && - !isMethodExcluded(type.name, m.name, this.exclusions, m) - ); - + + const methods = type.members.filter(m => + m.kind === 'method' + ).filter(m => !isMethodExcluded(type.name, m.name, this.exclusions, m)) + .filter(m => !m.isStatic); + // Check for const/non-const method pairs - const methodGroups = new Map(); + const methodGroups = new Map(); for (const method of methods) { const key = method.name + '(' + (method.parameters?.map(p => p.type).join(',') || '') + ')'; if (!methodGroups.has(key)) { @@ -26,10 +25,10 @@ export class MethodGenerator { } methodGroups.get(key)!.push(method); } - + // Collect all errors before failing const errors: string[] = []; - + // Report errors for duplicate methods with different signatures for (const [signature, group] of methodGroups) { if (group.length > 1) { @@ -38,7 +37,7 @@ export class MethodGenerator { if (returnTypes.size > 1) { let error = `\nERROR: Type '${type.name}' has multiple versions of method '${group[0].name}' with different return types:\n`; error += `This is likely a const/non-const overload pattern in C++ which cannot be represented in C.\n\n`; - + for (const method of group) { const source = method.fromSupertype ? ` (inherited from ${method.fromSupertype})` : ''; error += ` - ${method.returnType || 'void'} ${method.name}()${source}\n`; @@ -51,12 +50,12 @@ export class MethodGenerator { error += ` 1. Only exposing the const version in the C API\n`; error += ` 2. Renaming one of the methods in C++\n`; error += ` 3. Adding the method to exclusions.txt\n`; - + errors.push(error); } } } - + // If we have errors, throw them all at once if (errors.length > 0) { console.error("=".repeat(80)); @@ -70,53 +69,53 @@ export class MethodGenerator { console.error("=".repeat(80)); throw new Error(`Cannot generate C API due to ${errors.length} const/non-const overloaded method conflicts.`); } - + // For now, continue with the unique methods const uniqueMethods = methods; - + const cTypeName = `spine_${toSnakeCase(type.name)}`; - + // Track method names to detect overloads const methodCounts = new Map(); for (const method of uniqueMethods) { const count = methodCounts.get(method.name) || 0; methodCounts.set(method.name, count + 1); } - + // Track how many times we've seen each method name const methodSeenCounts = new Map(); - + for (const method of uniqueMethods) { // Handle getters if (method.name.startsWith('get') && (!method.parameters || method.parameters.length === 0)) { const propName = method.name.substring(3); - - // Check if return type is Vector - if (method.returnType && method.returnType.includes('Vector<')) { - // For Vector types, only generate collection accessors + + // Check if return type is Array + if (method.returnType && method.returnType.includes('Array<')) { + // For Array types, only generate collection accessors this.generateCollectionAccessors(type, method, propName, declarations, implementations); } else if (method.name === 'getRTTI') { // Special handling for getRTTI - make it static const funcName = toCFunctionName(type.name, 'get_rtti'); const returnType = 'spine_rtti'; - + declarations.push(`SPINE_C_EXPORT ${returnType} ${funcName}();`); - + implementations.push(`${returnType} ${funcName}() {`); implementations.push(` return (spine_rtti) &${type.name}::rtti;`); implementations.push(`}`); implementations.push(''); } else { - // For non-Vector types, generate regular getter + // For non-Array types, generate regular getter 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);`); - + implementations.push(`${returnType} ${funcName}(${cTypeName} obj) {`); implementations.push(` if (!obj) return ${this.getDefaultReturn(returnType)};`); implementations.push(` ${type.name} *_obj = (${type.name} *) obj;`); - + const callExpr = this.generateMethodCall('_obj', method); implementations.push(` return ${callExpr};`); implementations.push(`}`); @@ -126,14 +125,14 @@ export class MethodGenerator { // Handle setters else if (method.name.startsWith('set') && method.parameters && method.parameters.length === 1) { 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);`); - + implementations.push(`void ${funcName}(${cTypeName} obj, ${paramType} value) {`); implementations.push(` if (!obj) return;`); implementations.push(` ${type.name} *_obj = (${type.name} *) obj;`); - + const callExpr = this.generateSetterCall('_obj', method, 'value'); implementations.push(` ${callExpr};`); implementations.push(`}`); @@ -145,31 +144,31 @@ export class MethodGenerator { const isOverloaded = (methodCounts.get(method.name) || 0) > 1; const seenCount = methodSeenCounts.get(method.name) || 0; methodSeenCounts.set(method.name, seenCount + 1); - + // Generate function name with suffix for overloads let funcName = toCFunctionName(type.name, method.name); - + // Check for naming conflicts with type names if (method.name === 'pose' && type.name === 'Bone') { // Rename bone_pose() method to avoid conflict with spine_bone_pose type funcName = toCFunctionName(type.name, 'update_pose'); } - + if (isOverloaded && seenCount > 0) { // Add parameter count suffix for overloaded methods const paramCount = method.parameters ? method.parameters.length : 0; funcName = `${funcName}_${paramCount}`; } - - const returnType = toCTypeName(method.returnType || 'void'); + + const returnType = toCTypeName(method.returnType || 'void', this.validTypes); const params = this.generateMethodParameters(cTypeName, method); - + declarations.push(`SPINE_C_EXPORT ${returnType} ${funcName}(${params.declaration});`); - + implementations.push(`${returnType} ${funcName}(${params.declaration}) {`); implementations.push(` if (!obj) return ${this.getDefaultReturn(returnType)};`); implementations.push(` ${type.name} *_obj = (${type.name} *) obj;`); - + const callExpr = this.generateMethodCall('_obj', method, params.call); if (returnType === 'void') { implementations.push(` ${callExpr};`); @@ -180,18 +179,18 @@ export class MethodGenerator { implementations.push(''); } } - + return { declarations, implementations }; } - - private generateCollectionAccessors(type: Type, method: Member, propName: string, + + private generateCollectionAccessors(type: Type, method: Method, propName: string, declarations: string[], implementations: string[]) { const cTypeName = `spine_${toSnakeCase(type.name)}`; const propSnake = toSnakeCase(propName); - const vectorMatch = method.returnType!.match(/Vector<(.+?)>/); - if (!vectorMatch) return; - - const elementType = vectorMatch[1].trim().replace(/\s*\*$/, ''); + const arrayMatch = method.returnType!.match(/Array<(.+?)>/); + if (!arrayMatch) return; + + const elementType = arrayMatch[1].trim().replace(/\s*\*$/, ''); let cElementType: string; if (elementType === 'int') { cElementType = 'int32_t'; @@ -206,43 +205,43 @@ export class MethodGenerator { } else { cElementType = `spine_${toSnakeCase(elementType)}`; } - + // Get count function const getCountFunc = `spine_${toSnakeCase(type.name)}_get_num_${propSnake}`; declarations.push(`SPINE_C_EXPORT int32_t ${getCountFunc}(${cTypeName} obj);`); - + implementations.push(`int32_t ${getCountFunc}(${cTypeName} obj) {`); implementations.push(` if (!obj) return 0;`); implementations.push(` ${type.name} *_obj = (${type.name} *) obj;`); implementations.push(` return (int32_t) _obj->get${propName}().size();`); implementations.push(`}`); implementations.push(''); - + // Get array function const getArrayFunc = `spine_${toSnakeCase(type.name)}_get_${propSnake}`; declarations.push(`SPINE_C_EXPORT ${cElementType} *${getArrayFunc}(${cTypeName} obj);`); - + implementations.push(`${cElementType} *${getArrayFunc}(${cTypeName} obj) {`); implementations.push(` if (!obj) return nullptr;`); 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')) { - // 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(` if (vec.size() == 0) return nullptr;`); implementations.push(` return (${cElementType} *) &vec[0];`); } else { implementations.push(` return (${cElementType} *) _obj->get${propName}().buffer();`); } - + 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 || ''})`; - + // Handle return type conversions if (method.returnType) { if (method.returnType === 'const String &' || method.returnType === 'String') { @@ -251,37 +250,37 @@ export class MethodGenerator { // RTTI needs special handling - return as opaque pointer call = `(spine_rtti) &${call}`; } else if (method.returnType.includes('*')) { - const cType = toCTypeName(method.returnType); + const cType = toCTypeName(method.returnType, this.validTypes); call = `(${cType}) ${call}`; } else if (method.returnType === 'Color &' || method.returnType === 'const Color &') { call = `(spine_color) &${call}`; } else if (method.returnType.includes('&')) { // For reference returns, take the address - const cType = toCTypeName(method.returnType); + const cType = toCTypeName(method.returnType, this.validTypes); call = `(${cType}) &${call}`; } else if (!this.isPrimitiveType(method.returnType) && !this.isEnumType(method.returnType)) { // 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}`; } else if (this.isEnumType(method.returnType)) { // Cast enum return values - const cType = toCTypeName(method.returnType); + const cType = toCTypeName(method.returnType, this.validTypes); call = `(${cType}) ${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]; let value = valueName; - + // Convert from C type to C++ if (param.type === 'const String &' || param.type === 'String') { value = `String(${valueName})`; - } else if (param.type.includes('Vector<')) { - // Vector types are passed as void* and need to be cast back + } else if (param.type.includes('Array<')) { + // Array types are passed as void* and need to be cast back value = `(${param.type}) ${valueName}`; } else if (param.type.includes('*')) { value = `(${param.type}) ${valueName}`; @@ -293,25 +292,25 @@ export class MethodGenerator { // Cast enum types value = `(${param.type}) ${valueName}`; } - + 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 callParts: string[] = []; - + if (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}`); - + // Convert C type back to C++ for the call let callExpr = param.name; if (param.type === 'const String &' || param.type === 'String') { callExpr = `String(${param.name})`; - } else if (param.type.includes('Vector<')) { - // Vector types are passed as void* and need to be cast back + } else if (param.type.includes('Array<')) { + // Array types are passed as void* and need to be cast back callExpr = `(${param.type}) ${param.name}`; } else if (param.type.includes('*')) { callExpr = `(${param.type}) ${param.name}`; @@ -319,7 +318,7 @@ export class MethodGenerator { // Handle reference types const isConst = param.type.startsWith('const'); const baseType = param.type.replace(/^(?:const\s+)?(.+?)\s*&$/, '$1').trim(); - + // Non-const references to primitive types are output parameters - just pass the pointer if (!isConst && ['float', 'int', 'double', 'bool'].includes(baseType)) { callExpr = `*${param.name}`; @@ -331,17 +330,17 @@ export class MethodGenerator { // Cast enum types callExpr = `(${param.type}) ${param.name}`; } - + callParts.push(callExpr); } } - + return { declaration: declParts.join(', '), call: callParts.join(', ') }; } - + private getDefaultReturn(returnType: string): string { if (returnType === 'void') return ''; if (returnType === 'bool') return 'false'; @@ -353,7 +352,7 @@ export class MethodGenerator { } return 'nullptr'; } - + private isEnumType(type: string): boolean { // List of known enum types in spine-cpp const enumTypes = [ @@ -364,9 +363,9 @@ export class MethodGenerator { ]; return enumTypes.includes(type); } - + private isPrimitiveType(type: string): boolean { - return ['int', 'float', 'double', 'bool', 'size_t', 'int32_t', 'uint32_t', + return ['int', 'float', 'double', 'bool', 'size_t', 'int32_t', 'uint32_t', 'int16_t', 'uint16_t', 'uint8_t', 'void'].includes(type); } } \ No newline at end of file diff --git a/spine-c-new/codegen/src/generators/opaque-type-generator.ts b/spine-c-new/codegen/src/generators/opaque-type-generator.ts deleted file mode 100644 index 499eb4a39..000000000 --- a/spine-c-new/codegen/src/generators/opaque-type-generator.ts +++ /dev/null @@ -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})`; - } -} \ No newline at end of file diff --git a/spine-c-new/codegen/src/index.ts b/spine-c-new/codegen/src/index.ts index eeb281dcc..58cad2066 100644 --- a/spine-c-new/codegen/src/index.ts +++ b/spine-c-new/codegen/src/index.ts @@ -30,20 +30,21 @@ function checkConstNonConstConflicts(classes: Type[], exclusions: Exclusion[]): const conflicts: Array<{ type: string, method: string }> = []; for (const type of classes) { + if (type.members === undefined) { + continue; + } + // Get all non-static methods first - const allMethods = type.members?.filter(m => - m.kind === 'method' && - !m.isStatic - ); + const allMethods = type.members?.filter(m => m.kind === 'method').filter(m => !m.isStatic); if (allMethods) { - const methodGroups = new Map(); + const methodGroups = new Map>(); for (const method of allMethods) { + if (method.name === 'getSetupPose') { + 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)) { - if (method.name === 'getSetupPose') { - console.log(`Skipping excluded method: ${type.name}::${method.name}${method.isConst ? ' const' : ''}`); - } continue; } const key = method.name + '(' + (method.parameters?.map(p => p.type).join(',') || '') + ')'; @@ -133,9 +134,12 @@ async function main() { // Check for const/non-const conflicts checkConstNonConstConflicts(classes, exclusions); + // Create a set of valid type names for type checking + const validTypes = new Set(includedTypes.map(t => t.name)); + // Initialize generators - const constructorGen = new ConstructorGenerator(); - const methodGen = new MethodGenerator(exclusions); + const constructorGen = new ConstructorGenerator(validTypes); + const methodGen = new MethodGenerator(exclusions, validTypes); const enumGen = new EnumGenerator(); const fileWriter = new FileWriter(path.join(__dirname, '../../src/generated')); diff --git a/spine-c-new/codegen/src/type-extractor.ts b/spine-c-new/codegen/src/type-extractor.ts index fabb68201..7a18800bb 100644 --- a/spine-c-new/codegen/src/type-extractor.ts +++ b/spine-c-new/codegen/src/type-extractor.ts @@ -1,11 +1,547 @@ import * as fs from 'fs'; import * as path from 'path'; 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 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 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 = "<>"; + } + + 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 | 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 { + 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 +): 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(); + + // 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

" + 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): void { + const inheritedMethods: Member[] = []; + 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 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 @@ -16,20 +552,21 @@ function isExtractionNeeded(): boolean { console.log('spine-cpp-types.json not found, extraction needed'); return true; } - + // Get output file timestamp const outputStats = fs.statSync(OUTPUT_FILE); const outputTime = outputStats.mtime.getTime(); - - // Check all header files - const headerFiles = fs.readdirSync(HEADERS_DIR) - .filter(f => f.endsWith('.h')) - .map(f => path.join(HEADERS_DIR, f)); - + + // Check all header files in the spine subdirectory + const spineDir = path.join(SPINE_INCLUDE_DIR, 'spine'); + const headerFiles = fs.readdirSync(spineDir, { recursive: true }) + .filter((f: string | Buffer) => f.toString().endsWith('.h')) + .map((f: string | Buffer) => path.join(spineDir, f.toString())); + // Find newest header modification time let newestHeaderTime = 0; let newestHeader = ''; - + for (const headerFile of headerFiles) { const stats = fs.statSync(headerFile); if (stats.mtime.getTime() > newestHeaderTime) { @@ -37,38 +574,88 @@ function isExtractionNeeded(): boolean { newestHeader = path.basename(headerFile); } } - + // If any header is newer than output, we need to extract if (newestHeaderTime > outputTime) { console.log(`Header ${newestHeader} is newer than spine-cpp-types.json, extraction needed`); return true; } - + console.log('spine-cpp-types.json is up to date'); return false; } /** - * 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 { if (!isExtractionNeeded()) { return; } - + console.log('Running type extraction...'); - + try { - // Run the extractor script - const output = execSync(`node "${EXTRACTOR_SCRIPT}"`, { - cwd: SPINE_CPP_PATH, - encoding: 'utf8', - maxBuffer: 10 * 1024 * 1024 // 10MB buffer for large output - }); - + const allHeaders = findAllHeaderFiles(); + const allTypes: SpineTypes = {}; + 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(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 - fs.writeFileSync(OUTPUT_FILE, output); - + fs.writeFileSync(OUTPUT_FILE, JSON.stringify(allTypes, null, 2)); console.log(`Type extraction complete, wrote ${OUTPUT_FILE}`); } catch (error: any) { console.error('Failed to extract types:', error.message); @@ -79,10 +666,10 @@ export function extractTypes(): void { /** * Loads the extracted type information */ -export function loadTypes(): any { +export function loadTypes(): SpineTypes { if (!fs.existsSync(OUTPUT_FILE)) { throw new Error(`Type information not found at ${OUTPUT_FILE}. Run extraction first.`); } - + return JSON.parse(fs.readFileSync(OUTPUT_FILE, 'utf8')); } \ No newline at end of file diff --git a/spine-c-new/codegen/src/types.ts b/spine-c-new/codegen/src/types.ts index 2edb2d99d..ca837e2f3 100644 --- a/spine-c-new/codegen/src/types.ts +++ b/spine-c-new/codegen/src/types.ts @@ -1,22 +1,49 @@ export interface Parameter { name: string; type: string; - defaultValue?: string; } -export interface Member { - kind: 'field' | 'method' | 'constructor' | 'destructor'; +export type Field = { + kind: 'field'; name: string; - type?: string; // For fields - returnType?: string; // For methods + type: string; + isStatic?: boolean; + fromSupertype?: string; +} + +export type Method = { + kind: 'method'; + name: string; + returnType: string; parameters?: Parameter[]; isStatic?: boolean; isVirtual?: boolean; isPure?: 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 { name: string; value?: string; @@ -41,7 +68,7 @@ export interface SpineTypes { [header: string]: Type[]; } -export type Exclusion = +export type Exclusion = | { kind: 'type'; typeName: 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 }; +/** + * 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 { // Handle acronyms and consecutive capitals 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(); } +/** + * 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 { return `spine_${toSnakeCase(typeName)}_${toSnakeCase(methodName)}`; } -export function toCTypeName(cppType: string): string { - // Remove any spine:: namespace prefix first - cppType = cppType.replace(/^spine::/, ''); - - // Category 1: Primitives (including void) - const primitiveMap: { [key: string]: string } = { - 'void': 'void', - 'bool': 'bool', - 'char': 'char', - 'int': 'int32_t', - 'int32_t': 'int32_t', - 'unsigned int': 'uint32_t', - 'uint32_t': 'uint32_t', - 'short': 'int16_t', - 'int16_t': 'int16_t', - 'unsigned short': 'uint16_t', - 'uint16_t': 'uint16_t', - 'long long': 'int64_t', - 'int64_t': 'int64_t', - 'unsigned long long': 'uint64_t', - 'uint64_t': 'uint64_t', - 'float': 'float', - 'double': 'double', - 'size_t': 'size_t', - 'uint8_t': 'uint8_t' - }; - - if (primitiveMap[cppType]) { - return primitiveMap[cppType]; +/** + * Checks if a type is a primitive by tokenizing and checking if ALL tokens start with lowercase. + * Examples: + * - "int" → true + * - "const char*" → true (all tokens: "const", "char*" start lowercase) + * - "unsigned int" → true (all tokens start lowercase) + * - "Array" → false (starts uppercase) + * - "const Array&" → 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); + }); +} + +/** + * Converts a C++ type to its corresponding C type. + * + * @param cppType The C++ type to convert + * @param validTypes Set of valid type names (classes and enums) from filtered types + * @returns The C type + * @throws Error if the type is not recognized + * + * Examples: + * - Primitives: "int" → "int", "const float*" → "const float*" + * - String types: "String" → "const char*", "const String&" → "const char*" + * - Arrays: "Array" → "spine_array_float" + * - Class pointers: "Bone*" → "spine_bone" + * - Class references: "const Color&" → "spine_color" + * - Non-const primitive refs: "float&" → "float*" (output parameter) + */ +export function toCTypeName(cppType: string, validTypes: Set): string { + // Remove extra spaces and normalize + const normalizedType = cppType.replace(/\s+/g, ' ').trim(); + + // Primitives - pass through unchanged + if (isPrimitive(normalizedType)) { + return normalizedType; } - - // Category 2: Special types - if (cppType === 'String' || cppType === 'const String' || cppType === 'const char *') { - return 'const utf8 *'; + + // Special type: String + if (normalizedType === 'String' || normalizedType === 'const String' || + normalizedType === 'String&' || normalizedType === 'const String&') { + return 'const char*'; } - if (cppType === 'void *') { - return 'spine_void'; + + // PropertyId is a typedef + if (normalizedType === 'PropertyId') { + return 'int64_t'; } - 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 - const arrayMatch = cppType.match(/^(?:const\s+)?Array<(.+?)>\s*(?:&|\*)?$/); + + // Arrays - must check before pointers/references + const arrayMatch = normalizedType.match(/^(?:const\s+)?Array<(.+?)>\s*(?:&|\*)?$/); if (arrayMatch) { const elementType = arrayMatch[1].trim(); - - // Map element types to C array type suffixes - let typeSuffix: string; - - // 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); + + // For primitive element types, use the type name with spaces replaced by underscores + if (isPrimitive(elementType)) { + return `spine_array_${elementType.replace(/\s+/g, '_')}`; } - // Handle everything else (enums, classes) - else { - typeSuffix = toSnakeCase(elementType); + + // For pointer types, remove the * and convert + if (elementType.endsWith('*')) { + const baseType = elementType.slice(0, -1).trim(); + return `spine_array_${toSnakeCase(baseType)}`; } - - return `spine_array_${typeSuffix}`; + + // For class/enum types + return `spine_array_${toSnakeCase(elementType)}`; } - - // Category 4: Pointers - const pointerMatch = cppType.match(/^(.+?)\s*\*$/); + + // Pointers + const pointerMatch = normalizedType.match(/^(.+?)\s*\*$/); if (pointerMatch) { const baseType = pointerMatch[1].trim(); - + // Primitive pointers stay as-is - if (primitiveMap[baseType]) { - const mappedType = primitiveMap[baseType]; - // For numeric types, use the mapped type - return mappedType === 'void' ? 'void *' : `${mappedType} *`; + if (isPrimitive(baseType)) { + return normalizedType; } - - // char* becomes utf8* - if (baseType === 'char' || baseType === 'const char') { - return 'utf8 *'; - } - - // Class pointers + + // Class pointers become opaque types return `spine_${toSnakeCase(baseType)}`; } - - // Category 5: References - const refMatch = cppType.match(/^(?:const\s+)?(.+?)\s*&$/); + + // References + const refMatch = normalizedType.match(/^((?:const\s+)?(.+?))\s*&$/); if (refMatch) { - const baseType = refMatch[1].trim(); - const isConst = cppType.includes('const '); - - // Special cases - if (baseType === 'String') return 'const utf8 *'; - if (baseType === 'RTTI') return 'spine_rtti'; - + const fullBaseType = refMatch[1].trim(); + const baseType = refMatch[2].trim(); + const isConst = fullBaseType.startsWith('const '); + // Non-const references to primitives become pointers (output parameters) - if (!isConst && primitiveMap[baseType]) { - const mappedType = primitiveMap[baseType]; - return mappedType === 'void' ? 'void *' : `${mappedType} *`; + if (!isConst && isPrimitive(baseType)) { + return `${baseType}*`; } - + // Const references and class references - recurse without the reference - return toCTypeName(baseType); + return toCTypeName(baseType, validTypes); } - - // Category 6: Known enums - const knownEnums = [ - 'MixBlend', 'MixDirection', 'BlendMode', 'AttachmentType', 'EventType', - 'Format', 'TextureFilter', 'TextureWrap', 'Inherit', 'Physics', - 'PositionMode', 'Property', 'RotateMode', 'SequenceMode', 'SpacingMode' - ]; - - if (knownEnums.includes(cppType)) { - return `spine_${toSnakeCase(cppType)}`; + + // Function pointers - for now, just error + if (normalizedType.includes('(') && normalizedType.includes(')')) { + throw new Error(`Function pointer types not yet supported: ${normalizedType}`); } - - // Category 7: Classes (default case) - // Assume any remaining type is a spine class - return `spine_${toSnakeCase(cppType)}`; + + // Everything else should be a class or enum type + // Check if it's a valid type + 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)}`; } \ No newline at end of file diff --git a/spine-c-new/src/custom.cpp b/spine-c-new/src/custom.cpp deleted file mode 100644 index 9cf4b1ce8..000000000 --- a/spine-c-new/src/custom.cpp +++ /dev/null @@ -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 -#include -#include -#include -#include -#include - -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 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(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(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; -} \ No newline at end of file diff --git a/spine-c-new/src/custom.h b/spine-c-new/src/custom.h deleted file mode 100644 index bebb9ff0d..000000000 --- a/spine-c-new/src/custom.h +++ /dev/null @@ -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 -#include - -#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 \ No newline at end of file diff --git a/spine-cpp/extract-spine-cpp-types.js b/spine-cpp/extract-spine-cpp-types.js deleted file mode 100755 index accb0e96a..000000000 --- a/spine-cpp/extract-spine-cpp-types.js +++ /dev/null @@ -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 [] - 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" - - 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) 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 = "<>"; - } - - 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

" - 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)); -} \ No newline at end of file