[c] Port type extractor to TypeScript and improve codegen

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

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

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

View File

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

View File

@ -18,18 +18,18 @@ export interface ArraySpecialization {
export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[], enumTypes: Set<string>): ArraySpecialization[] {
const arrayTypes = new Set<string>();
const warnings: string[] = [];
// Extract Array<T> 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;
}

View File

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

View File

@ -1,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 <spine/Array.h>');
@ -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}`;

View File

@ -1,4 +1,4 @@
import { Type, Member, toSnakeCase, toCFunctionName, toCTypeName } from '../types';
import { Type, Member, toSnakeCase, toCFunctionName, toCTypeName, Constructor } from '../types';
export interface GeneratorResult {
declarations: string[];
@ -6,15 +6,17 @@ export interface GeneratorResult {
}
export class ConstructorGenerator {
constructor(private validTypes: Set<string>) {}
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(', ')

View File

@ -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<string>) {}
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<string, Member[]>();
const methodGroups = new Map<string, Method[]>();
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<string, number>();
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<string, number>();
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);
}
}

View File

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

View File

@ -30,20 +30,21 @@ function checkConstNonConstConflicts(classes: Type[], exclusions: Exclusion[]):
const conflicts: Array<{ type: string, method: string }> = [];
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<string, Member[]>();
const methodGroups = new Map<string, Array<Member & { kind: 'method' }>>();
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<string>(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'));

View File

@ -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 = "<<extraction failed>>";
}
return enumValue;
});
return info;
}
// For classes/structs, extract public members
info.members = [];
let currentAccess = node.tagUsed === 'struct' ? 'public' : 'private';
let hasPureVirtual = false;
for (const inner of node.inner || []) {
if (inner.kind === 'AccessSpecDecl') {
currentAccess = inner.access || 'private';
continue;
}
if (inner.kind === 'FriendDecl' || currentAccess !== 'public') continue;
const member = extractMember(inner, node);
if (member) {
info.members.push(member);
// Check if this is a pure virtual method
if (member.kind === 'method' && member.isPure) {
hasPureVirtual = true;
}
}
}
// Always set isAbstract to a boolean value
info.isAbstract = hasPureVirtual;
return info;
}
/**
* Processes an AST node to extract types
*/
function processNode(
node: any,
types: Type[],
targetPath: string,
sourceLines: string[],
inSpineNamespace: boolean = false
): void {
if (!node || typeof node !== 'object') return;
// Handle spine namespace
if (node.kind === 'NamespaceDecl' && node.name === 'spine') {
(node.inner || []).forEach((n: any) => processNode(n, types, targetPath, sourceLines, true));
return;
}
// Recurse to find spine namespace
if (!inSpineNamespace) {
(node.inner || []).forEach((n: any) => processNode(n, types, targetPath, sourceLines, false));
return;
}
// Process type declarations
const typeKinds = ['CXXRecordDecl', 'ClassTemplateDecl', 'EnumDecl', 'TypedefDecl', 'TypeAliasDecl'];
if (!typeKinds.includes(node.kind)) return;
// Skip if not in target file or invalid
if (!isInTargetFile(node, targetPath) ||
node.isImplicit ||
!node.name ||
node.name.startsWith('_') ||
node.name.includes('<')) return;
// Skip forward declarations
if (node.previousDecl && (!node.inner || node.inner.length === 0)) return;
// Extract type info
if (node.kind === 'ClassTemplateDecl') {
const classNode = (node.inner || []).find((n: any) => n.kind === 'CXXRecordDecl');
if (classNode) {
const typeInfo = extractTypeInfo(classNode, sourceLines);
typeInfo.isTemplate = true;
// Extract template parameters
const templateParams: string[] = [];
for (const inner of node.inner || []) {
if (inner.kind === 'TemplateTypeParmDecl' && inner.name) {
templateParams.push(inner.name);
}
}
if (templateParams.length > 0) {
typeInfo.templateParams = templateParams;
}
types.push(typeInfo);
}
} else if (node.kind === 'CXXRecordDecl' && node.inner?.length > 0) {
const typeInfo = extractTypeInfo(node, sourceLines);
// Ensure isTemplate is always set for non-template classes
if (typeInfo.isTemplate === undefined) {
typeInfo.isTemplate = false;
}
types.push(typeInfo);
} else if (node.kind === 'EnumDecl') {
types.push(extractTypeInfo(node, sourceLines));
} else if (node.kind === 'TypedefDecl' || node.kind === 'TypeAliasDecl') {
types.push(extractTypeInfo(node, sourceLines));
}
}
/**
* Extracts types from a single header file
*/
function extractLocalTypes(headerFile: string, typeMap: Map<string, Type> | null = null): Type[] {
const absHeaderPath = path.resolve(headerFile);
const sourceContent = fs.readFileSync(absHeaderPath, 'utf8');
const sourceLines = sourceContent.split('\n');
// Get AST from clang
const cmd = `clang++ -Xclang -ast-dump=json -fsyntax-only -std=c++11 -I "${SPINE_INCLUDE_DIR}" "${absHeaderPath}" 2>/dev/null`;
const maxBuffer = headerFile.includes('Debug.h') ? 500 : 200; // MB
let astJson: any;
try {
const output = execSync(cmd, { encoding: 'utf8', maxBuffer: maxBuffer * 1024 * 1024 });
astJson = JSON.parse(output);
} catch (error: any) {
throw new Error(error.code === 'ENOBUFS'
? `AST output too large (>${maxBuffer}MB)`
: error.message);
}
const types: Type[] = [];
processNode(astJson, types, absHeaderPath, sourceLines);
// Filter out forward declarations and SpineObject
const filteredTypes = types
.filter(t => {
// Skip types with no members (forward declarations)
if (t.members && t.members.length === 0) return false;
// Skip SpineObject - it's not needed for C wrapper generation
if (t.name === 'SpineObject') return false;
return true;
})
.sort((a, b) => (a.loc?.line || 0) - (b.loc?.line || 0));
// Add inherited methods if we have a type map
if (typeMap) {
for (const type of filteredTypes) {
if (type.superTypes && type.members) {
addInheritedMethods(type, typeMap);
}
}
}
return filteredTypes;
}
/**
* Creates a method signature for comparison
*/
function getMethodSignature(method: Method): string {
let sig = method.name;
if (method.parameters && method.parameters.length > 0) {
sig += '(' + method.parameters.map(p => p.type).join(',') + ')';
} else {
sig += '()';
}
// Add const qualifier if present
if (method.isConst) {
sig += ' const';
}
return sig;
}
/**
* Substitutes template parameters in a type string
*/
function substituteTemplateParams(typeStr: string, paramMap: Map<string, string>): string {
let result = typeStr;
// Replace template parameters in order of length (longest first)
// to avoid replacing substrings (e.g., V before V1)
const sortedParams = Array.from(paramMap.keys()).sort((a, b) => b.length - a.length);
for (const param of sortedParams) {
const regex = new RegExp(`\\b${param}\\b`, 'g');
result = result.replace(regex, paramMap.get(param)!);
}
return result;
}
/**
* Adds methods inherited from template supertypes
*/
function addTemplateInheritedMethods(
_type: Type,
templateType: Type,
templateClassName: string,
templateArgs: string,
inheritedMethods: Member[],
ownMethodSignatures: Set<string>
): void {
// Parse template arguments (handle multiple args)
const argsList: string[] = [];
let depth = 0;
let currentArg = '';
for (const char of templateArgs) {
if (char === '<') depth++;
else if (char === '>') depth--;
if (char === ',' && depth === 0) {
argsList.push(currentArg.trim());
currentArg = '';
} else {
currentArg += char;
}
}
if (currentArg.trim()) {
argsList.push(currentArg.trim());
}
// Build a mapping of template params to actual types
const paramMap = new Map<string, string>();
// Use the actual template parameters if we have them
if (templateType.templateParams && templateType.templateParams.length === argsList.length) {
templateType.templateParams.forEach((param, i) => {
paramMap.set(param, argsList[i]);
});
} else {
// Fallback: if we don't have template param info, skip substitution
console.error(`Warning: Template ${templateClassName} missing parameter info, skipping substitution`);
return;
}
// Process each member of the template
for (const member of templateType.members || []) {
if (member.kind === 'method') {
const method = member as Method;
// Skip template constructors - they have weird names like "Pose<P>"
if (method.name.includes('<')) continue;
// Clone the member and substitute template parameters
const inheritedMember: Method = JSON.parse(JSON.stringify(method));
inheritedMember.fromSupertype = `${templateClassName}<${templateArgs}>`;
// Replace template parameters in return type
if (inheritedMember.returnType) {
inheritedMember.returnType = substituteTemplateParams(
inheritedMember.returnType, paramMap
);
}
// Replace template parameters in parameters
if (inheritedMember.parameters) {
for (const param of inheritedMember.parameters) {
param.type = substituteTemplateParams(param.type, paramMap);
}
}
// Check if this method is overridden
const sig = getMethodSignature(inheritedMember);
if (!ownMethodSignatures.has(sig)) {
inheritedMethods.push(inheritedMember);
ownMethodSignatures.add(sig);
}
}
}
}
/**
* Adds inherited methods to a type
*/
function addInheritedMethods(type: Type, typeMap: Map<string, Type>): void {
const inheritedMethods: Member[] = [];
const ownMethodSignatures = new Set<string>();
// Build a set of method signatures from this type
for (const member of type.members || []) {
if (member.kind === 'method') {
const sig = getMethodSignature(member as Method);
ownMethodSignatures.add(sig);
}
}
// Process each supertype
for (const superTypeName of type.superTypes || []) {
// Clean up the supertype name (remove namespaces, etc)
const cleanName = superTypeName.replace(/^.*::/, '');
// Skip SpineObject inheritance - it's just noise
if (cleanName === 'SpineObject') continue;
// Check if this is a template supertype
const templateMatch = cleanName.match(/^([^<]+)<(.+)>$/);
if (templateMatch) {
const templateClassName = templateMatch[1];
const templateArgs = templateMatch[2];
const templateType = typeMap.get(templateClassName);
if (templateType && templateType.members) {
// Process template inheritance
addTemplateInheritedMethods(
type, templateType, templateClassName, templateArgs,
inheritedMethods, ownMethodSignatures
);
}
} else {
// Non-template supertype
const superType = typeMap.get(cleanName);
if (!superType || !superType.members) continue;
// Add non-overridden methods from supertype
for (const member of superType.members) {
if (member.kind === 'method') {
const method = member as Method;
const sig = getMethodSignature(method);
if (!ownMethodSignatures.has(sig)) {
const inheritedMember = { ...method, fromSupertype: cleanName };
inheritedMethods.push(inheritedMember);
ownMethodSignatures.add(sig); // Prevent duplicates from multiple inheritance
}
}
}
}
}
// Add inherited methods to the type
if (type.members) {
type.members.push(...inheritedMethods);
}
}
/**
* Finds all header files in the spine include directory
*/
function findAllHeaderFiles(): string[] {
const headers: string[] = [];
function walkDir(dir: string): void {
fs.readdirSync(dir).forEach(file => {
const fullPath = path.join(dir, file);
if (fs.statSync(fullPath).isDirectory()) {
walkDir(fullPath);
} else if (file.endsWith('.h') && file !== 'spine.h') {
headers.push(fullPath);
}
});
}
walkDir(SPINE_INCLUDE_DIR);
return headers.sort();
}
/**
* Checks if type extraction is needed based on file timestamps
@ -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<ArrayBufferLike>) => f.toString().endsWith('.h'))
.map((f: string | Buffer<ArrayBufferLike>) => 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<string, Type>();
for (const headerFile of allHeaders) {
const relPath = path.relative(SPINE_INCLUDE_DIR, headerFile);
process.stderr.write(`\r\x1b[K Pass 1 - Processing ${++processed}/${allHeaders.length}: ${relPath}...`);
try {
const types = extractLocalTypes(headerFile);
if (types.length > 0) {
allTypes[relPath] = types;
// Build type map
for (const type of types) {
typeMap.set(type.name, type);
}
}
} catch (error: any) {
errors++;
console.error(`\n ERROR processing ${relPath}: ${error.message}`);
}
}
// Second pass: add inherited methods
console.error(`\n Pass 2 - Adding inherited methods...`);
processed = 0;
for (const headerFile of allHeaders) {
const relPath = path.relative(SPINE_INCLUDE_DIR, headerFile);
if (!allTypes[relPath]) continue;
process.stderr.write(`\r\x1b[K Pass 2 - Processing ${++processed}/${Object.keys(allTypes).length}: ${relPath}...`);
for (const type of allTypes[relPath]) {
if (type.superTypes && type.members) {
addInheritedMethods(type, typeMap);
// Check if any inherited methods are pure virtual
// If so, and the class doesn't override them, it's abstract
if (!type.isAbstract) {
const hasPureVirtual = type.members.some(m =>
m.kind === 'method' && m.isPure === true
);
if (hasPureVirtual) {
type.isAbstract = true;
}
}
}
}
}
console.error(`\n Completed: ${Object.keys(allTypes).length} files processed, ${errors} errors`);
// Write to output file
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'));
}

View File

@ -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<float>" false (starts uppercase)
* - "const Array<float>&" false ("Array" starts uppercase)
*/
function isPrimitive(cppType: string): boolean {
const tokens = cppType.split(/\s+/);
return tokens.every(token => {
// Remove any trailing punctuation like *, &
const cleanToken = token.replace(/[*&]+$/, '');
return cleanToken.length > 0 && /^[a-z]/.test(cleanToken);
});
}
/**
* 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<float>" "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>): 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)}`;
}

View File

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

View File

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

View File

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