From 759434e4612b1364e60a3339253d72cdf02bb586 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 8 Jul 2025 23:08:48 +0200 Subject: [PATCH] [c] Fix abstract type detection and improve codegen filtering - Always set isAbstract to boolean in extract-spine-cpp-types.js - Check for inherited pure virtual methods after inheritance pass - Include abstract classes but skip create() function generation - Only exclude template classes from code generation - Extract const/non-const conflict checking into separate function - Remove unused OpaqueTypeGenerator and helper functions --- spine-c-new/codegen/README.md | 370 ++++++++++++++++++ spine-c-new/codegen/exclusions.txt | 28 +- spine-c-new/codegen/rtti-bases.txt | 8 - spine-c-new/codegen/src/array-scanner.ts | 155 ++++++++ spine-c-new/codegen/src/exclusions.ts | 38 +- spine-c-new/codegen/src/file-writer.ts | 99 ++++- spine-c-new/codegen/src/find-array-types.ts | 93 +++++ .../codegen/src/generators/array-generator.ts | 242 ++++++++++++ .../src/generators/constructor-generator.ts | 15 +- .../codegen/src/generators/enum-generator.ts | 11 +- .../src/generators/method-generator.ts | 215 +++++++++- .../codegen/src/generators/rtti-generator.ts | 85 ---- spine-c-new/codegen/src/index.ts | 154 ++++++-- spine-c-new/codegen/src/rtti.ts | 61 --- spine-c-new/codegen/src/type-extractor.ts | 88 +++++ spine-c-new/codegen/src/types.ts | 147 +++++-- spine-cpp/extract-spine-cpp-types.js | 24 +- 17 files changed, 1561 insertions(+), 272 deletions(-) create mode 100644 spine-c-new/codegen/README.md delete mode 100644 spine-c-new/codegen/rtti-bases.txt create mode 100644 spine-c-new/codegen/src/array-scanner.ts create mode 100644 spine-c-new/codegen/src/find-array-types.ts create mode 100644 spine-c-new/codegen/src/generators/array-generator.ts delete mode 100644 spine-c-new/codegen/src/generators/rtti-generator.ts delete mode 100644 spine-c-new/codegen/src/rtti.ts create mode 100644 spine-c-new/codegen/src/type-extractor.ts diff --git a/spine-c-new/codegen/README.md b/spine-c-new/codegen/README.md new file mode 100644 index 000000000..5c4d41047 --- /dev/null +++ b/spine-c-new/codegen/README.md @@ -0,0 +1,370 @@ +# Spine C API Code Generator + +This TypeScript-based code generator automatically creates a C wrapper API for the Spine C++ runtime. It parses the spine-cpp headers and generates a complete C API with opaque types, following systematic type conversion rules. + +## Table of Contents + +1. [Overview](#overview) +2. [Architecture](#architecture) +3. [Type System](#type-system) +4. [File Structure](#file-structure) +5. [Generation Process](#generation-process) +6. [Key Design Decisions](#key-design-decisions) +7. [Exclusion System](#exclusion-system) +8. [Array Specializations](#array-specializations) +9. [Type Conversion Rules](#type-conversion-rules) +10. [Running the Generator](#running-the-generator) + +## Overview + +The generator creates a C API that wraps the spine-cpp C++ runtime, allowing C programs to use Spine functionality. Key features: + +- **Opaque Types**: All C++ classes are exposed as opaque pointers in C +- **Automatic Memory Management**: Generates create/dispose functions +- **Method Wrapping**: Converts C++ methods to C functions with proper type conversion +- **Array Specializations**: Generates concrete array types for all Array usage +- **Systematic Type Handling**: Uses categorized type conversion instead of ad-hoc rules + +## Architecture + +### Core Components + +``` +codegen/ +├── src/ +│ ├── index.ts # Main entry point +│ ├── types.ts # Type definitions and conversion +│ ├── exclusions.ts # Exclusion system +│ ├── type-extractor.ts # Automatic type extraction +│ ├── array-scanner.ts # Array specialization scanner +│ ├── file-writer.ts # File generation +│ └── generators/ +│ ├── opaque-type-generator.ts # Opaque type declarations +│ ├── constructor-generator.ts # Create/dispose functions +│ ├── method-generator.ts # Method wrappers +│ ├── enum-generator.ts # Enum conversions +│ └── array-generator.ts # Array specializations +├── exclusions.txt # Types/methods to exclude +└── spine-cpp-types.json # Extracted type information +``` + +### Data Flow + +1. **Type Extraction**: `extract-spine-cpp-types.js` parses C++ headers → `spine-cpp-types.json` +2. **Loading**: Generator loads JSON and exclusions +3. **Filtering**: Excludes types based on rules (templates, abstracts, manual exclusions) +4. **Generation**: Each generator processes types and creates C code +5. **Writing**: Files are written to `src/generated/` + +## Type System + +### Type Categories + +The generator classifies all C++ types into systematic categories: + +1. **Primitives**: `int`, `float`, `double`, `bool`, `char`, `void`, `size_t` + - Direct mapping (e.g., `bool` → `bool`) + +2. **Special Types**: String, function pointers, PropertyId + - `String` → `const utf8 *` + - `void *` → `spine_void` + - `PropertyId` → `int64_t` (typedef'd to long long) + +3. **Arrays**: `Array` specializations + - Generated as `spine_array_` + - Full API for each specialization + +4. **Pointers**: Type followed by `*` + - Primitive pointers stay as-is (`float *`) + - Class pointers become opaque (`Bone *` → `spine_bone`) + +5. **References**: Type followed by `&` + - Const references: treated as value parameters + - Non-const primitive references: output parameters (`float &` → `float *`) + - Class references: converted to opaque types + +6. **Enums**: Known spine enums + - Prefixed with `spine_` and converted to snake_case + +7. **Classes**: All other types + - Assumed to be spine classes, converted to `spine_` + +### Opaque Type Pattern + +All C++ classes are exposed as opaque pointers: + +```c +// In types.h +SPINE_OPAQUE_TYPE(spine_bone) // Expands to typedef struct spine_bone_wrapper* spine_bone + +// In implementation +spine_bone spine_bone_create() { + return (spine_bone) new (__FILE__, __LINE__) Bone(); +} +``` + +## File Structure + +### Generated Files + +- **types.h**: Forward declarations for all types + - All opaque type declarations + - Includes for all enum headers + - Includes arrays.h at the bottom + +- **arrays.h/arrays.cpp**: Array specializations + - Generated for all Array found in spine-cpp + - Complete API for each specialization + +- **.h/.cpp**: One pair per type + - Header contains function declarations + - Source contains implementations + +- **spine-c.h**: Main header that includes everything + +### Include Order + +The main spine-c.h includes files in this order: +1. base.h (basic definitions) +2. types.h (all forward declarations) +3. extensions.h (custom functionality) +4. All generated type headers + +This ensures all types are declared before use. + +## Generation Process + +### 1. Type Extraction + +The generator automatically runs `extract-spine-cpp-types.js` if: +- `spine-cpp-types.json` doesn't exist +- Any spine-cpp header is newer than the JSON file + +This script: +- Parses all spine-cpp headers using tree-sitter +- Extracts complete type information including inherited members +- Resolves template inheritance +- Marks abstract classes and templates + +### 2. Type Filtering + +Types are excluded if they are: +- **Templates**: Detected by `isTemplate` field +- **Abstract**: Have unimplemented pure virtual methods +- **Internal utilities**: Array, String, HashMap, etc. +- **Manually excluded**: Listed in exclusions.txt + +### 3. Code Generation + +For each included type: + +#### Constructors +- Generates `spine__create()` for default constructor +- Generates `spine__create_with_()` for parameterized constructors +- Always generates `spine__dispose()` for cleanup + +#### Methods +- Getters: `spine__get_()` +- Setters: `spine__set_()` +- Other methods: `spine__()` +- Special handling for: + - Vector return types (generate collection accessors) + - RTTI methods (made static) + - Const/non-const overloads (reported as errors) + +#### Arrays +- Scans all types for Array usage +- Generates specializations for each unique T +- Filters out template placeholders (T, K) +- Warns about problematic types (String, nested arrays) + +## Key Design Decisions + +### 1. Why Opaque Types? + +C doesn't support classes or inheritance. Opaque pointers: +- Hide implementation details +- Prevent direct struct access +- Allow polymorphism through base type pointers +- Match C convention for handles + +### 2. Why Generate Array Specializations? + +C can't have template types. Options were: +1. Use `void *` everywhere (loses type safety) +2. Generate specializations (chosen approach) + +Benefits: +- Type safety in C +- Better API documentation +- Prevents casting errors + +### 3. Why Systematic Type Classification? + +Original code had many special cases. Systematic approach: +- Reduces bugs from missed cases +- Makes behavior predictable +- Easier to maintain +- Clear rules for each category + +### 4. Why Exclude Const Methods? + +C doesn't have const-correctness. When C++ has: +```cpp +T& getValue(); // for non-const objects +const T& getValue() const; // for const objects +``` + +C can only have one function name. We exclude const versions and expose non-const. + +### 5. Why Static RTTI Methods? + +RTTI objects are singletons in spine-cpp. Making getRTTI() static: +- Reflects actual usage (Type::rtti) +- Avoids unnecessary object parameter +- Cleaner API + +## Exclusion System + +### exclusions.txt Format + +``` +# Exclude entire types +type: SkeletonClipping +type: Triangulator + +# Exclude specific methods +method: AnimationState::setListener +method: AnimationState::addListener + +# Exclude const versions specifically +method: BoneData::getSetupPose const +``` + +### Exclusion Rules + +1. **Type exclusions**: Entire type and all methods excluded +2. **Method exclusions**: Specific methods on otherwise included types +3. **Const-specific**: Can exclude just const or non-const version + +## Array Specializations + +### Scanning Process + +1. Examines all members of non-excluded types +2. Extracts Array patterns from: + - Return types + - Parameter types + - Field types +3. Cleans element types (removes class/struct prefix) +4. Categorizes as primitive/enum/pointer + +### Generated API + +For each Array, generates: +```c +// Creation +spine_array_float spine_array_float_create(); +spine_array_float spine_array_float_create_with_capacity(int32_t capacity); +void spine_array_float_dispose(spine_array_float array); + +// Element access +float spine_array_float_get(spine_array_float array, int32_t index); +void spine_array_float_set(spine_array_float array, int32_t index, float value); + +// Array methods (auto-generated from Array type) +size_t spine_array_float_size(spine_array_float array); +void spine_array_float_clear(spine_array_float array); +void spine_array_float_add(spine_array_float array, float value); +// ... etc +``` + +### Special Cases + +- **String arrays**: Warned but skipped (should use const char**) +- **Nested arrays**: Warned and skipped (Array>) +- **PropertyId**: Treated as int64_t, not enum + +## Type Conversion Rules + +### toCTypeName Function + +Implements systematic type conversion: + +1. **Remove namespace**: Strip any `spine::` prefix +2. **Check primitives**: Direct mapping via table +3. **Check special types**: String, void*, function pointers +4. **Check arrays**: Convert Array to spine_array_* +5. **Check pointers**: Handle based on pointed-to type +6. **Check references**: Handle based on const-ness +7. **Check enums**: Known enum list +8. **Default to class**: Assume spine type + +### Method Parameter Conversion + +- **Input parameters**: C++ type to C type +- **Output parameters**: Non-const references become pointers +- **String parameters**: Create String objects from const char* +- **Enum parameters**: Cast to C++ enum type + +### Return Value Conversion + +- **Strings**: Return buffer() as const char* +- **References**: Take address and cast +- **Enums**: Cast to C enum type +- **Arrays**: Return as specialized array type + +## Running the Generator + +### Prerequisites + +```bash +npm install +``` + +### Build and Run + +```bash +npm run build # Compile TypeScript +node dist/index.js # Run generator +``` + +### What Happens + +1. Checks if type extraction needed (file timestamps) +2. Runs extraction if needed +3. Loads types and exclusions +4. Filters types based on rules +5. Generates code for each type +6. Writes all files to src/generated/ +7. Updates main spine-c.h + +### Output + +- Generates ~150 .h/.cpp file pairs +- Creates arrays.h with ~30 specializations +- All files include proper license headers +- Organized by type for easy navigation + +## Maintenance + +### Adding New Types + +1. No action needed - automatically detected from spine-cpp + +### Excluding Types/Methods + +1. Add to exclusions.txt +2. Regenerate + +### Changing Type Mappings + +1. Update toCTypeName in types.ts +2. Follow systematic categories + +### Debugging + +- Check spine-cpp-types.json for extracted data +- Look for warnings in console output +- Verify exclusions are applied correctly +- Check generated files for correctness \ No newline at end of file diff --git a/spine-c-new/codegen/exclusions.txt b/spine-c-new/codegen/exclusions.txt index 84eea9fc2..24f4151a3 100644 --- a/spine-c-new/codegen/exclusions.txt +++ b/spine-c-new/codegen/exclusions.txt @@ -12,10 +12,10 @@ type: EventListener type: AnimationStateListener type: AnimationStateListenerObject type: Pool -type: ContainerUtil +type: ArrayUtils type: BlockAllocator type: MathUtil -type: Vector +type: Array type: PoolObject type: HasRendererObject type: String @@ -46,4 +46,26 @@ method: EventData::setFloatValue method: EventData::setStringValue method: EventData::setAudioPath method: EventData::setVolume -method: EventData::setBalance \ No newline at end of file +method: EventData::setBalance + +# Vector methods need special handling +method: AttachmentTimeline.getAttachmentNames + +# BoneLocal/BonePose setScale is overloaded in a confusing way +method: BoneLocal::setScale +method: BonePose::setScale + +# Color set is overloaded with conflicting signatures +method: Color::set + + +# Exclude const versions of getSetupPose() - we'll only expose the non-const version +method: BoneData::getSetupPose const +method: ConstraintDataGeneric::getSetupPose const +method: IkConstraintData::getSetupPose const +method: PathConstraintData::getSetupPose const +method: PhysicsConstraintData::getSetupPose const +method: PosedDataGeneric::getSetupPose const +method: SliderData::getSetupPose const +method: SlotData::getSetupPose const +method: TransformConstraintData::getSetupPose const \ No newline at end of file diff --git a/spine-c-new/codegen/rtti-bases.txt b/spine-c-new/codegen/rtti-bases.txt deleted file mode 100644 index 3ff4851fb..000000000 --- a/spine-c-new/codegen/rtti-bases.txt +++ /dev/null @@ -1,8 +0,0 @@ -# Base types that need RTTI emulation -Attachment -Constraint -ConstraintData -Pose -Posed -PosedData -Timeline \ No newline at end of file diff --git a/spine-c-new/codegen/src/array-scanner.ts b/spine-c-new/codegen/src/array-scanner.ts new file mode 100644 index 000000000..d0c531353 --- /dev/null +++ b/spine-c-new/codegen/src/array-scanner.ts @@ -0,0 +1,155 @@ +import { Type, SpineTypes } from './types'; +import { isTypeExcluded } from './exclusions'; + +export interface ArraySpecialization { + cppType: string; // e.g. "Array" + elementType: string; // e.g. "float" + cTypeName: string; // e.g. "spine_array_float" + cElementType: string; // e.g. "float" or "spine_animation" + isPointer: boolean; + isEnum: boolean; + isPrimitive: boolean; +} + +/** + * Scans all spine-cpp types to find Array specializations + * Only includes arrays from non-excluded types + */ +export function scanArraySpecializations(typesJson: SpineTypes, exclusions: any[], enumTypes: Set): ArraySpecialization[] { + const arrayTypes = new Set(); + const warnings: string[] = []; + + // Extract Array from a type string + function extractArrayTypes(typeStr: string | undefined) { + if (!typeStr) return; + + const regex = /Array<([^>]+)>/g; + let match; + while ((match = regex.exec(typeStr)) !== null) { + arrayTypes.add(match[0]); + } + } + + // Process all types + for (const header of Object.keys(typesJson)) { + for (const type of typesJson[header]) { + // Skip excluded types and template types + 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); + } + } + } + } + } + + // 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 && + ['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 } = { + 'int': 'int32_t', + 'unsigned short': 'uint16_t', + 'float': 'float', + 'double': 'double', + 'bool': 'bool', + 'char': 'char', + 'size_t': 'size_t' + }; + cElementType = typeMap[cleanElementType] || cleanElementType; + cTypeName = `spine_array_${cElementType.replace(/_t$/, '')}`; + } else if (isEnum) { + // Handle enums + if (cleanElementType === 'PropertyId') { + cElementType = 'int64_t'; // PropertyId is typedef long long + cTypeName = 'spine_array_property_id'; + } else { + // Convert enum name to snake_case + const snakeCase = cleanElementType.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, ''); + cElementType = `spine_${snakeCase}`; + cTypeName = `spine_array_${snakeCase}`; + } + } else if (isPointer) { + // Handle pointer types + const snakeCase = cleanElementType.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, ''); + cElementType = `spine_${snakeCase}`; + cTypeName = `spine_array_${snakeCase}`; + } else { + // Unknown type - skip + warnings.push(`Unknown array element type: ${elementType}`); + continue; + } + + specializations.push({ + cppType: arrayType, + elementType: elementType, + cTypeName: cTypeName, + cElementType: cElementType, + isPointer: isPointer, + isEnum: isEnum, + isPrimitive: isPrimitive + }); + } + + // Print warnings + if (warnings.length > 0) { + console.log('\nArray Generation Warnings:'); + for (const warning of warnings) { + console.log(` - ${warning}`); + } + console.log(''); + } + + // Sort by C type name for consistent output + specializations.sort((a, b) => a.cTypeName.localeCompare(b.cTypeName)); + + return specializations; +} \ No newline at end of file diff --git a/spine-c-new/codegen/src/exclusions.ts b/spine-c-new/codegen/src/exclusions.ts index e7f953802..1cfe525c9 100644 --- a/spine-c-new/codegen/src/exclusions.ts +++ b/spine-c-new/codegen/src/exclusions.ts @@ -22,14 +22,23 @@ export function loadExclusions(filePath: string): Exclusion[] { continue; } - // Parse method exclusion - const methodMatch = trimmed.match(/^method:\s*(.+?)::(.+)$/); + // Parse method exclusion with optional const specification + // Format: method: Type::method or method: Type::method const + const methodMatch = trimmed.match(/^method:\s*(.+?)::(.+?)(\s+const)?$/); if (methodMatch) { + const methodName = methodMatch[2].trim(); + const isConst = !!methodMatch[3]; + exclusions.push({ kind: 'method', typeName: methodMatch[1].trim(), - methodName: methodMatch[2].trim() + methodName: methodName, + isConst: isConst }); + + if (isConst) { + console.log(`Parsed const exclusion: ${methodMatch[1].trim()}::${methodName} const`); + } } } @@ -40,10 +49,21 @@ export function isTypeExcluded(typeName: string, exclusions: Exclusion[]): boole return exclusions.some(ex => ex.kind === 'type' && ex.typeName === typeName); } -export function isMethodExcluded(typeName: string, methodName: string, exclusions: Exclusion[]): boolean { - return exclusions.some(ex => - ex.kind === 'method' && - ex.typeName === typeName && - ex.methodName === methodName - ); +export function isMethodExcluded(typeName: string, methodName: string, exclusions: Exclusion[], returnType?: string): boolean { + // Determine if method is const by looking at return type + const isConstMethod = returnType ? returnType.includes('const ') && returnType.includes('&') : false; + + const result = exclusions.some(ex => { + if (ex.kind === 'method' && + ex.typeName === typeName && + ex.methodName === methodName) { + // If exclusion doesn't specify const, it matches all + if (ex.isConst === undefined) return true; + // Otherwise, it must match the const flag + return ex.isConst === isConstMethod; + } + return false; + }); + + return result; } \ No newline at end of file diff --git a/spine-c-new/codegen/src/file-writer.ts b/spine-c-new/codegen/src/file-writer.ts index 2e5a73310..a3940f53b 100644 --- a/spine-c-new/codegen/src/file-writer.ts +++ b/spine-c-new/codegen/src/file-writer.ts @@ -55,6 +55,12 @@ export class FileWriter { const headerPath = path.join(this.outputDir, `${fileName}.h`); const headerGuard = `SPINE_C_${typeName.toUpperCase()}_H`; + // Check if the header content uses any custom types from extensions.h + const headerString = headerContent.join('\n'); + const needsExtensions = headerString.includes('spine_void') || + headerString.includes('spine_dispose_renderer_object') || + headerString.includes('spine_texture_loader'); + const headerLines = [ LICENSE_HEADER, '', @@ -64,6 +70,16 @@ export class FileWriter { '#ifdef __cplusplus', 'extern "C" {', '#endif', + '', + '#include "types.h"', + ]; + + // Include extensions.h if needed + if (needsExtensions) { + headerLines.push('#include "../extensions.h"'); + } + + headerLines.push( '', ...headerContent, '', @@ -72,7 +88,7 @@ export class FileWriter { '#endif', '', `#endif // ${headerGuard}` - ]; + ); fs.writeFileSync(headerPath, headerLines.join('\n')); @@ -98,7 +114,7 @@ export class FileWriter { `#ifndef ${headerGuard}`, `#define ${headerGuard}`, '', - '#include "../../custom.h"', + '#include "../base.h"', '', '#ifdef __cplusplus', 'extern "C" {', @@ -131,21 +147,19 @@ export class FileWriter { '#ifndef SPINE_C_H', '#define SPINE_C_H', '', - '// Custom types and functions', - '#include "../src/custom.h"', + '// Base definitions', + '#include "../src/base.h"', '', - '// Generated enum types' + '// All type declarations and enum includes', + '#include "../src/generated/types.h"', + '', + '// Extension functions', + '#include "../src/extensions.h"', + '', + '// Generated class types' ]; - // Add enum includes - for (const enumType of enums) { - lines.push(`#include "../src/generated/${toSnakeCase(enumType.name)}.h"`); - } - - lines.push(''); - lines.push('// Generated class types'); - - // Add class includes + // Add class includes (they contain the actual function declarations) for (const classType of classes) { lines.push(`#include "../src/generated/${toSnakeCase(classType.name)}.h"`); } @@ -155,4 +169,61 @@ export class FileWriter { fs.writeFileSync(mainHeaderPath, lines.join('\n')); } + + async writeArrays(header: string[], source: string[]): Promise { + const headerPath = path.join(this.outputDir, 'arrays.h'); + const sourcePath = path.join(this.outputDir, 'arrays.cpp'); + + // Add license header to both files + const headerLines = [LICENSE_HEADER, '', ...header]; + const sourceLines = [LICENSE_HEADER, '', ...source]; + + fs.writeFileSync(headerPath, headerLines.join('\n')); + fs.writeFileSync(sourcePath, sourceLines.join('\n')); + } + + async writeTypesHeader(classes: Type[], enums: Type[]): Promise { + const headerPath = path.join(this.outputDir, 'types.h'); + const lines: string[] = [ + LICENSE_HEADER, + '', + '#ifndef SPINE_C_TYPES_H', + '#define SPINE_C_TYPES_H', + '', + '#ifdef __cplusplus', + 'extern "C" {', + '#endif', + '', + '#include "../base.h"', + '', + '// Forward declarations for all non-enum types' + ]; + + // Forward declare all class types + for (const classType of classes) { + const cTypeName = `spine_${toSnakeCase(classType.name)}`; + lines.push(`SPINE_OPAQUE_TYPE(${cTypeName})`); + } + + lines.push(''); + lines.push('// Include all enum types (cannot be forward declared)'); + + // Include all enum headers + for (const enumType of enums) { + lines.push(`#include "${toSnakeCase(enumType.name)}.h"`); + } + + lines.push(''); + lines.push('// Array specializations'); + lines.push('#include "arrays.h"'); + + lines.push(''); + lines.push('#ifdef __cplusplus'); + lines.push('}'); + lines.push('#endif'); + lines.push(''); + lines.push('#endif // SPINE_C_TYPES_H'); + + fs.writeFileSync(headerPath, lines.join('\n')); + } } \ No newline at end of file diff --git a/spine-c-new/codegen/src/find-array-types.ts b/spine-c-new/codegen/src/find-array-types.ts new file mode 100644 index 000000000..75ced0dda --- /dev/null +++ b/spine-c-new/codegen/src/find-array-types.ts @@ -0,0 +1,93 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +// Load the spine-cpp types +const typesJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../spine-cpp-types.json'), 'utf8')); + +// Set to store unique Array types +const arrayTypes = new Set(); + +// Function to extract Array from a type string +function extractArrayTypes(typeStr: string | undefined) { + if (!typeStr) return; + + // Find all Array<...> patterns + const regex = /Array<([^>]+)>/g; + let match; + while ((match = regex.exec(typeStr)) !== null) { + arrayTypes.add(match[0]); // Full Array string + } +} + +// Process all types +for (const header of Object.keys(typesJson)) { + for (const type of typesJson[header]) { + if (!type.members) continue; + + for (const member of type.members) { + // Check return types + extractArrayTypes(member.returnType); + + // Check field types + extractArrayTypes(member.type); + + // Check parameter types + if (member.parameters) { + for (const param of member.parameters) { + extractArrayTypes(param.type); + } + } + } + } +} + +// Sort and display results +const sortedArrayTypes = Array.from(arrayTypes).sort(); + +console.log(`Found ${sortedArrayTypes.length} Array specializations:\n`); + +// Group by element type +const primitiveArrays: string[] = []; +const pointerArrays: string[] = []; + +for (const arrayType of sortedArrayTypes) { + const elementType = arrayType.match(/Array<(.+)>/)![1]; + + if (elementType.includes('*')) { + pointerArrays.push(arrayType); + } else { + primitiveArrays.push(arrayType); + } +} + +console.log('Primitive Arrays:'); +for (const type of primitiveArrays) { + console.log(` ${type}`); +} + +console.log('\nPointer Arrays:'); +for (const type of pointerArrays) { + console.log(` ${type}`); +} + +// Generate C type names +console.log('\nC Type Names:'); +console.log('\nPrimitive Arrays:'); +for (const type of primitiveArrays) { + const elementType = type.match(/Array<(.+)>/)![1]; + const cType = elementType === 'float' ? 'float' : + elementType === 'int' ? 'int32_t' : + elementType === 'unsigned short' ? 'uint16_t' : + elementType; + console.log(` ${type} -> spine_array_${cType.replace(/ /g, '_')}`); +} + +console.log('\nPointer Arrays:'); +for (const type of pointerArrays) { + const elementType = type.match(/Array<(.+?)\s*\*/)![1].trim(); + // Remove 'class ' prefix if present + const cleanType = elementType.replace(/^class\s+/, ''); + // Convert to snake case + const snakeCase = cleanType.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, ''); + console.log(` ${type} -> spine_array_${snakeCase}`); +} \ No newline at end of file diff --git a/spine-c-new/codegen/src/generators/array-generator.ts b/spine-c-new/codegen/src/generators/array-generator.ts new file mode 100644 index 000000000..ec8338799 --- /dev/null +++ b/spine-c-new/codegen/src/generators/array-generator.ts @@ -0,0 +1,242 @@ +import { Type, Member, toSnakeCase, toCTypeName } 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)) { + const arrayType = typesJson[header].find((t: Type) => t.name === 'Array'); + if (arrayType) { + this.arrayType = arrayType; + break; + } + } + } + + /** + * 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'); + header.push(''); + header.push('#include "../base.h"'); + header.push('#include "types.h"'); + header.push(''); + header.push('#ifdef __cplusplus'); + header.push('extern "C" {'); + header.push('#endif'); + header.push(''); + + // Source file + source.push('#include "arrays.h"'); + source.push('#include '); + source.push('#include '); + 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') + ) || []; + + // 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) {`); + source.push(` if (!array) return;`); + 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) {`); + source.push(` if (!array) return ${this.getDefaultValue(spec)};`); + source.push(` ${spec.cppType} *_array = (${spec.cppType}*) array;`); + 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;`); + source.push(` ${spec.cppType} *_array = (${spec.cppType}*) array;`); + 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[]) { + // 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; + if (method.returnType && method.returnType !== 'void') { + hasReturn = true; + if (method.returnType === 'T &' || method.returnType === `${spec.elementType} &`) { + // Return by reference becomes return by value in C + returnType = spec.cElementType; + } else if (method.returnType === 'T *' || method.returnType === `${spec.elementType} *`) { + returnType = spec.cElementType; + } else if (method.returnType === 'size_t') { + returnType = 'size_t'; + } else if (method.returnType === 'int') { + returnType = 'int32_t'; + } else if (method.returnType === 'bool') { + returnType = 'bool'; + } else if (method.returnType === `${spec.cppType} *`) { + returnType = spec.cTypeName; + } else { + // Unknown return type - skip this method + console.log(` Skipping Array method ${method.name} - unknown return type: ${method.returnType}`); + 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 || + param.type === 'const T &' || param.type === `const ${spec.elementType} &`) { + cParams.push(`${spec.cElementType} ${param.name}`); + cppArgs.push(this.convertToCpp(spec, param.name)); + } else if (param.type === 'size_t') { + cParams.push(`size_t ${param.name}`); + cppArgs.push(param.name); + } else if (param.type === 'int') { + cParams.push(`int32_t ${param.name}`); + cppArgs.push(param.name); + } else { + // Unknown parameter type - skip this method + console.log(` Skipping Array method ${method.name} - unknown param type: ${param.type}`); + return; + } + } + } + + // 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) { + source.push(` return ${this.convertFromCpp(spec, call)};`); + } else { + source.push(` return ${call};`); + } + } else { + source.push(` ${call};`); + } + + source.push('}'); + source.push(''); + } + + private getDefaultValue(spec: ArraySpecialization): string { + if (spec.isPointer) return 'nullptr'; + if (spec.isPrimitive) { + if (spec.cElementType === 'bool') return 'false'; + return '0'; + } + 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'; + if (returnType === spec.cElementType) return this.getDefaultValue(spec); + if (returnType === spec.cTypeName) return 'nullptr'; + return '0'; + } + + private convertFromCpp(spec: ArraySpecialization, expr: string): string { + if (spec.isPointer) { + return `(${spec.cElementType}) ${expr}`; + } + if (spec.isEnum && spec.elementType !== 'PropertyId') { + return `(${spec.cElementType}) ${expr}`; + } + return expr; + } + + private convertToCpp(spec: ArraySpecialization, expr: string): string { + if (spec.isPointer) { + return `(${spec.elementType}) ${expr}`; + } + if (spec.isEnum && spec.elementType !== 'PropertyId') { + return `(${spec.elementType}) ${expr}`; + } + return expr; + } +} \ No newline at end of file diff --git a/spine-c-new/codegen/src/generators/constructor-generator.ts b/spine-c-new/codegen/src/generators/constructor-generator.ts index 53cd08d59..68e5a58b1 100644 --- a/spine-c-new/codegen/src/generators/constructor-generator.ts +++ b/spine-c-new/codegen/src/generators/constructor-generator.ts @@ -15,9 +15,11 @@ export class ConstructorGenerator { const constructors = type.members.filter(m => m.kind === 'constructor'); const cTypeName = `spine_${toSnakeCase(type.name)}`; - // Generate create functions for each constructor - let constructorIndex = 0; - for (const constructor of constructors) { + // Skip constructor generation for abstract types + if (!type.isAbstract) { + // Generate create functions for each constructor + let constructorIndex = 0; + for (const constructor of constructors) { const funcName = this.getCreateFunctionName(type.name, constructor, constructorIndex); const params = this.generateParameters(constructor); @@ -31,7 +33,8 @@ export class ConstructorGenerator { implementations.push(`}`); implementations.push(''); - constructorIndex++; + constructorIndex++; + } } // Always generate dispose function @@ -98,6 +101,10 @@ export class ConstructorGenerator { callExpr = `String(${param.name})`; } else if (param.type.includes('*')) { callExpr = `(${param.type}) ${param.name}`; + } else if (param.type.includes('&')) { + // Handle reference types - need to dereference the pointer + const baseType = param.type.replace(/^(?:const\s+)?(.+?)\s*&$/, '$1').trim(); + callExpr = `*(${baseType}*) ${param.name}`; } callParts.push(callExpr); diff --git a/spine-c-new/codegen/src/generators/enum-generator.ts b/spine-c-new/codegen/src/generators/enum-generator.ts index 362607cdb..d83072d32 100644 --- a/spine-c-new/codegen/src/generators/enum-generator.ts +++ b/spine-c-new/codegen/src/generators/enum-generator.ts @@ -10,7 +10,16 @@ export class EnumGenerator { if (enumType.values) { for (let i = 0; i < enumType.values.length; i++) { const value = enumType.values[i]; - const cName = `SPINE_${toSnakeCase(enumType.name).toUpperCase()}_${toSnakeCase(value.name).toUpperCase()}`; + // Remove redundant enum name prefix from value name if present + let valueName = value.name; + if (valueName.toLowerCase().startsWith(enumType.name.toLowerCase())) { + valueName = valueName.substring(enumType.name.length); + // Remove leading underscore if present after removing prefix + if (valueName.startsWith('_')) { + valueName = valueName.substring(1); + } + } + const cName = `SPINE_${toSnakeCase(enumType.name).toUpperCase()}_${toSnakeCase(valueName).toUpperCase()}`; if (value.value !== undefined) { lines.push(` ${cName} = ${value.value}${i < enumType.values.length - 1 ? ',' : ''}`); diff --git a/spine-c-new/codegen/src/generators/method-generator.ts b/spine-c-new/codegen/src/generators/method-generator.ts index 4f4986d07..590f500b4 100644 --- a/spine-c-new/codegen/src/generators/method-generator.ts +++ b/spine-c-new/codegen/src/generators/method-generator.ts @@ -14,32 +14,113 @@ export class MethodGenerator { const methods = type.members.filter(m => m.kind === 'method' && !m.isStatic && - !isMethodExcluded(type.name, m.name, this.exclusions) + !isMethodExcluded(type.name, m.name, this.exclusions, m.returnType) ); + // Check for const/non-const method pairs + const methodGroups = new Map(); + for (const method of methods) { + const key = method.name + '(' + (method.parameters?.map(p => p.type).join(',') || '') + ')'; + if (!methodGroups.has(key)) { + methodGroups.set(key, []); + } + 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) { + // Check if we have different return types (const vs non-const overloading) + const returnTypes = new Set(group.map(m => m.returnType)); + 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`; + } + error += `\nC++ pattern detected:\n`; + error += ` T& ${group[0].name}() { return member; } // for non-const objects\n`; + error += ` const T& ${group[0].name}() const { return member; } // for const objects\n`; + error += `\nThis pattern provides const-correctness in C++ but cannot be represented in C.\n`; + error += `Consider:\n`; + 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)); + console.error("CONST/NON-CONST METHOD CONFLICTS FOUND"); + console.error("=".repeat(80)); + for (const error of errors) { + console.error(error); + } + console.error("=".repeat(80)); + console.error(`Total conflicts found: ${errors.length}`); + 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)}`; - for (const method of methods) { + // Track method names to detect overloads + const methodCounts = new Map(); + for (const method of uniqueMethods) { + const count = methodCounts.get(method.name) || 0; + methodCounts.set(method.name, count + 1); + } + + // Track how many times we've seen each method name + const methodSeenCounts = new Map(); + + for (const method of uniqueMethods) { // Handle getters if (method.name.startsWith('get') && (!method.parameters || method.parameters.length === 0)) { const propName = method.name.substring(3); - const funcName = toCFunctionName(type.name, method.name); - const returnType = toCTypeName(method.returnType || 'void'); - 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(`}`); - implementations.push(''); - - // Generate collection accessors if return type is Vector + // Check if return type is Vector if (method.returnType && method.returnType.includes('Vector<')) { + // For Vector 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 + const funcName = toCFunctionName(type.name, method.name); + const returnType = toCTypeName(method.returnType || 'void'); + + 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(`}`); + implementations.push(''); } } // Handle setters @@ -60,7 +141,26 @@ export class MethodGenerator { } // Handle other methods else { - const funcName = toCFunctionName(type.name, method.name); + // Check if this is an overloaded method + 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 params = this.generateMethodParameters(cTypeName, method); @@ -95,6 +195,14 @@ export class MethodGenerator { let cElementType: string; if (elementType === 'int') { cElementType = 'int32_t'; + } else if (elementType === 'float') { + cElementType = 'float'; + } else if (elementType === 'uint8_t') { + cElementType = 'uint8_t'; + } else if (elementType === 'String') { + cElementType = 'const utf8 *'; + } else if (elementType === 'PropertyId') { + cElementType = 'int32_t'; // PropertyId is just an int } else { cElementType = `spine_${toSnakeCase(elementType)}`; } @@ -117,7 +225,17 @@ export class MethodGenerator { implementations.push(`${cElementType} *${getArrayFunc}(${cTypeName} obj) {`); implementations.push(` if (!obj) return nullptr;`); implementations.push(` ${type.name} *_obj = (${type.name} *) obj;`); - implementations.push(` return (${cElementType} *) _obj->get${propName}().buffer();`); + + // Handle const vs non-const vectors + if (method.isConst || method.returnType!.includes('const')) { + // For const vectors, 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(''); } @@ -129,7 +247,7 @@ export class MethodGenerator { if (method.returnType) { if (method.returnType === 'const String &' || method.returnType === 'String') { call = `(const utf8 *) ${call}.buffer()`; - } else if (method.returnType === 'const spine::RTTI &' || method.returnType === 'const RTTI &') { + } else if (method.returnType === 'const RTTI &' || method.returnType === 'RTTI &') { // RTTI needs special handling - return as opaque pointer call = `(spine_rtti) &${call}`; } else if (method.returnType.includes('*')) { @@ -137,6 +255,18 @@ export class MethodGenerator { 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); + 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); + call = `(${cType}) &${call}`; + } else if (this.isEnumType(method.returnType)) { + // Cast enum return values + const cType = toCTypeName(method.returnType); + call = `(${cType}) ${call}`; } } @@ -155,6 +285,13 @@ export class MethodGenerator { value = `(${param.type}) ${valueName}`; } else if (param.type.includes('*')) { value = `(${param.type}) ${valueName}`; + } else if (param.type.includes('&') && !param.type.includes('const')) { + // Non-const reference parameters need to dereference the C pointer + const baseType = param.type.replace(/\s*&\s*$/, '').trim(); + value = `*((${baseType}*) ${valueName})`; + } else if (this.isEnumType(param.type)) { + // Cast enum types + value = `(${param.type}) ${valueName}`; } return `${objName}->${method.name}(${value})`; @@ -178,6 +315,21 @@ export class MethodGenerator { callExpr = `(${param.type}) ${param.name}`; } else if (param.type.includes('*')) { callExpr = `(${param.type}) ${param.name}`; + } else if (param.type.includes('&')) { + // 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}`; + } else { + // Const references and non-primitive types need dereferencing + callExpr = `*(${baseType}*) ${param.name}`; + } + } else if (this.isEnumType(param.type)) { + // Cast enum types + callExpr = `(${param.type}) ${param.name}`; } callParts.push(callExpr); @@ -192,8 +344,29 @@ export class MethodGenerator { private getDefaultReturn(returnType: string): string { if (returnType === 'void') return ''; - if (returnType === 'spine_bool') return '0'; - if (returnType.includes('int') || returnType === 'float' || returnType === 'double') return '0'; + if (returnType === 'bool') return 'false'; + if (returnType.includes('int') || returnType === 'float' || returnType === 'double' || returnType === 'size_t') return '0'; + // Check if it's an enum type (spine_* types that are not pointers) + if (returnType.startsWith('spine_') && !returnType.includes('*')) { + // Cast 0 to the enum type + return `(${returnType}) 0`; + } return 'nullptr'; } + + private isEnumType(type: string): boolean { + // List of known enum types in spine-cpp + const enumTypes = [ + 'EventType', 'Format', 'TextureFilter', 'TextureWrap', + 'AttachmentType', 'BlendMode', 'Inherit', 'MixBlend', + 'MixDirection', 'Physics', 'PositionMode', 'Property', + 'RotateMode', 'SequenceMode', 'SpacingMode' + ]; + return enumTypes.includes(type); + } + + private isPrimitiveType(type: string): boolean { + return ['int', 'float', 'double', 'bool', 'size_t', 'int32_t', 'uint32_t', + 'int16_t', 'uint16_t', 'uint8_t', 'void'].includes(type); + } } \ No newline at end of file diff --git a/spine-c-new/codegen/src/generators/rtti-generator.ts b/spine-c-new/codegen/src/generators/rtti-generator.ts deleted file mode 100644 index 3651da5f6..000000000 --- a/spine-c-new/codegen/src/generators/rtti-generator.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Type, toSnakeCase } from '../types'; -import { getTypeHierarchy, getLeafTypes } from '../rtti'; -import { GeneratorResult } from './constructor-generator'; - -export class RttiGenerator { - constructor( - private rttiBases: Set, - private allTypes: Type[] - ) {} - - needsRtti(type: Type): boolean { - const hierarchy = getTypeHierarchy(type, this.allTypes); - return hierarchy.some(t => this.rttiBases.has(t)); - } - - generateForType(baseType: Type): GeneratorResult { - const declarations: string[] = []; - const implementations: string[] = []; - - // Only generate RTTI for base types in rttiBases - if (!this.rttiBases.has(baseType.name)) { - return { declarations, implementations }; - } - - const leafTypes = getLeafTypes(baseType.name, this.allTypes); - const baseSnake = toSnakeCase(baseType.name); - const enumName = `spine_${baseSnake}_type`; - - // Add forward declarations for all leaf types - for (const leafType of leafTypes) { - const leafTypeName = `spine_${toSnakeCase(leafType.name)}`; - declarations.push(`struct ${leafTypeName}_wrapper;`); - declarations.push(`typedef struct ${leafTypeName}_wrapper *${leafTypeName};`); - } - declarations.push(''); - - // Generate enum - declarations.push(`typedef enum ${enumName} {`); - leafTypes.forEach((type, index) => { - const enumValue = `SPINE_TYPE_${baseSnake.toUpperCase()}_${toSnakeCase(type.name).toUpperCase()}`; - declarations.push(` ${enumValue} = ${index}${index < leafTypes.length - 1 ? ',' : ''}`); - }); - declarations.push(`} ${enumName};`); - declarations.push(''); - - // Generate is_type method - const isTypeFunc = `spine_${baseSnake}_is_type`; - declarations.push(`SPINE_C_EXPORT spine_bool ${isTypeFunc}(spine_${baseSnake} obj, ${enumName} type);`); - - implementations.push(`spine_bool ${isTypeFunc}(spine_${baseSnake} obj, ${enumName} type) {`); - implementations.push(` if (!obj) return 0;`); - implementations.push(` ${baseType.name} *_obj = (${baseType.name} *) obj;`); - implementations.push(` `); - implementations.push(` switch (type) {`); - - leafTypes.forEach((type, index) => { - const enumValue = `SPINE_TYPE_${baseSnake.toUpperCase()}_${toSnakeCase(type.name).toUpperCase()}`; - implementations.push(` case ${enumValue}:`); - implementations.push(` return _obj->getRTTI().instanceOf(${type.name}::rtti);`); - }); - - implementations.push(` }`); - implementations.push(` return 0;`); - implementations.push(`}`); - implementations.push(''); - - // Generate cast methods for each leaf type - for (const leafType of leafTypes) { - const castFunc = `spine_${baseSnake}_as_${toSnakeCase(leafType.name)}`; - const returnType = `spine_${toSnakeCase(leafType.name)}`; - - declarations.push(`SPINE_C_EXPORT ${returnType} ${castFunc}(spine_${baseSnake} obj);`); - - implementations.push(`${returnType} ${castFunc}(spine_${baseSnake} obj) {`); - implementations.push(` if (!obj) return nullptr;`); - implementations.push(` ${baseType.name} *_obj = (${baseType.name} *) obj;`); - implementations.push(` if (!_obj->getRTTI().instanceOf(${leafType.name}::rtti)) return nullptr;`); - implementations.push(` return (${returnType}) obj;`); - implementations.push(`}`); - implementations.push(''); - } - - return { declarations, implementations }; - } -} \ No newline at end of file diff --git a/spine-c-new/codegen/src/index.ts b/spine-c-new/codegen/src/index.ts index 029e71eee..085a92ce6 100644 --- a/spine-c-new/codegen/src/index.ts +++ b/spine-c-new/codegen/src/index.ts @@ -1,23 +1,100 @@ #!/usr/bin/env node import * as fs from 'fs'; import * as path from 'path'; -import { Type, Member, SpineTypes, toSnakeCase } from './types'; +import { Type, Member, SpineTypes, toSnakeCase, Exclusion } from './types'; import { loadExclusions, isTypeExcluded, isMethodExcluded } from './exclusions'; -import { loadRttiBases } from './rtti'; -import { OpaqueTypeGenerator } from './generators/opaque-type-generator'; import { ConstructorGenerator } from './generators/constructor-generator'; import { MethodGenerator } from './generators/method-generator'; import { EnumGenerator } from './generators/enum-generator'; -import { RttiGenerator } from './generators/rtti-generator'; +import { ArrayGenerator } from './generators/array-generator'; import { FileWriter } from './file-writer'; +import { extractTypes, loadTypes } from './type-extractor'; +import { scanArraySpecializations } from './array-scanner'; + +/** + * Checks for methods that have both const and non-const versions with different return types. + * This is a problem for C bindings because C doesn't support function overloading. + * + * In C++, you can have: + * T& getValue(); // for non-const objects + * const T& getValue() const; // for const objects + * + * But in C, we can only have one function with a given name, so we need to detect + * and report these conflicts. + * + * @param classes - Array of class/struct types to check + * @param exclusions - Exclusion rules to skip specific methods + * @returns Array of conflicts found, or exits if conflicts exist + */ +function checkConstNonConstConflicts(classes: Type[], exclusions: Exclusion[]): void { + const conflicts: Array<{type: string, method: string}> = []; + + for (const type of classes) { + // Get all non-static methods first + const allMethods = type.members?.filter(m => + m.kind === 'method' && + !m.isStatic + ); + + if (allMethods) { + const methodGroups = new Map(); + for (const method of allMethods) { + // Skip if this specific const/non-const version is excluded + if (isMethodExcluded(type.name, method.name, exclusions, method.returnType)) { + if (method.name === 'getSetupPose') { + const isConstMethod = method.returnType && method.returnType.includes('const ') && method.returnType.includes('&'); + console.log(`Skipping excluded method: ${type.name}::${method.name}${isConstMethod ? ' const' : ''}`); + } + continue; + } + const key = method.name + '(' + (method.parameters?.map(p => p.type).join(',') || '') + ')'; + if (!methodGroups.has(key)) { + methodGroups.set(key, []); + } + methodGroups.get(key)!.push(method); + } + + for (const [signature, group] of methodGroups) { + if (group.length > 1) { + const returnTypes = new Set(group.map(m => m.returnType)); + if (returnTypes.size > 1) { + conflicts.push({type: type.name, method: group[0].name}); + } + } + } + } + } + + // If we found conflicts, report them all and exit + if (conflicts.length > 0) { + console.error("\n" + "=".repeat(80)); + console.error("SUMMARY OF ALL CONST/NON-CONST METHOD CONFLICTS"); + console.error("=".repeat(80)); + console.error(`\nFound ${conflicts.length} method conflicts across the codebase:\n`); + + for (const conflict of conflicts) { + console.error(` - ${conflict.type}::${conflict.method}()`); + } + + console.error("\nThese methods have both const and non-const versions in C++ which cannot"); + console.error("be represented in the C API. You need to either:"); + console.error(" 1. Add these to exclusions.txt"); + console.error(" 2. Modify the C++ code to avoid const/non-const overloading"); + console.error("=".repeat(80) + "\n"); + + process.exit(1); + } +} async function main() { + // Extract types if needed + extractTypes(); + console.log('Loading type information...'); // Load all necessary data - const typesJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../all-spine-types.json'), 'utf8')) as SpineTypes; + const typesJson = loadTypes() as SpineTypes; const exclusions = loadExclusions(path.join(__dirname, '../exclusions.txt')); - const rttiBases = loadRttiBases(path.join(__dirname, '../rtti-bases.txt')); // Flatten all types from all headers into a single array const allTypes: Type[] = []; @@ -25,8 +102,28 @@ async function main() { allTypes.push(...typesJson[header]); } - // Filter out excluded types - const includedTypes = allTypes.filter(type => !isTypeExcluded(type.name, exclusions)); + // Create a map of all types for easy lookup + const typeMap = new Map(); + for (const type of allTypes) { + typeMap.set(type.name, type); + } + + + // Filter types: only exclude templates and manually excluded types + const includedTypes = allTypes.filter(type => { + if (isTypeExcluded(type.name, exclusions)) { + return false; + } + + // Only exclude template types + if (type.isTemplate === true) { + console.log(`Auto-excluding template type: ${type.name}`); + return false; + } + + // Include everything else (including abstract types) + return true; + }); // Separate classes and enums const classes = includedTypes.filter(t => t.kind === 'class' || t.kind === 'struct'); @@ -35,13 +132,14 @@ async function main() { console.log(`Found ${classes.length} classes/structs and ${enums.length} enums to generate`); // Initialize generators - const opaqueTypeGen = new OpaqueTypeGenerator(); const constructorGen = new ConstructorGenerator(); const methodGen = new MethodGenerator(exclusions); const enumGen = new EnumGenerator(); - const rttiGen = new RttiGenerator(rttiBases, allTypes); const fileWriter = new FileWriter(path.join(__dirname, '../../src/generated')); + // Check for const/non-const conflicts + checkConstNonConstConflicts(classes, exclusions); + // Generate code for each type for (const type of classes) { console.log(`Generating ${type.name}...`); @@ -49,19 +147,14 @@ async function main() { const headerContent: string[] = []; const sourceContent: string[] = []; - // Add includes - headerContent.push('#include "../custom.h"'); - headerContent.push(''); - + // Source includes sourceContent.push(`#include "${toSnakeCase(type.name)}.h"`); sourceContent.push('#include '); sourceContent.push(''); sourceContent.push('using namespace spine;'); sourceContent.push(''); - // Generate opaque type - headerContent.push(opaqueTypeGen.generate(type)); - headerContent.push(''); + // Opaque type is already in types.h, don't generate it here // Generate constructors const constructors = constructorGen.generate(type); @@ -77,15 +170,6 @@ async function main() { sourceContent.push(...methods.implementations); } - // Check if this type needs RTTI - if (rttiGen.needsRtti(type)) { - const rtti = rttiGen.generateForType(type); - if (rtti.declarations.length > 0) { - headerContent.push(...rtti.declarations); - sourceContent.push(...rtti.implementations); - } - } - // Write files await fileWriter.writeType(type.name, headerContent, sourceContent); } @@ -97,6 +181,24 @@ async function main() { await fileWriter.writeEnum(enumType.name, enumCode); } + // Generate Array specializations + console.log('\nScanning for Array specializations...'); + const enumNames = new Set(enums.map(e => e.name)); + const arraySpecs = scanArraySpecializations(typesJson, exclusions, enumNames); + console.log(`Found ${arraySpecs.length} array specializations to generate`); + + if (arraySpecs.length > 0) { + console.log('\nGenerating arrays.h/arrays.cpp...'); + const arrayGen = new ArrayGenerator(typesJson); + const { header, source } = arrayGen.generate(arraySpecs); + + // Write arrays.h and arrays.cpp + await fileWriter.writeArrays(header, source); + } + + // Generate types.h file (includes arrays.h) + await fileWriter.writeTypesHeader(classes, enums); + // Generate main header file await fileWriter.writeMainHeader(classes, enums); diff --git a/spine-c-new/codegen/src/rtti.ts b/spine-c-new/codegen/src/rtti.ts deleted file mode 100644 index 64fdbe602..000000000 --- a/spine-c-new/codegen/src/rtti.ts +++ /dev/null @@ -1,61 +0,0 @@ -import * as fs from 'fs'; -import { Type } from './types'; - -export function loadRttiBases(filePath: string): Set { - const content = fs.readFileSync(filePath, 'utf8'); - const lines = content.split('\n'); - const bases = new Set(); - - for (const line of lines) { - const trimmed = line.trim(); - if (trimmed && !trimmed.startsWith('#')) { - bases.add(trimmed); - } - } - - return bases; -} - -export function getTypeHierarchy(type: Type, allTypes: Type[]): string[] { - const hierarchy: string[] = [type.name]; - - if (type.superTypes) { - for (const superName of type.superTypes) { - const superType = allTypes.find(t => t.name === superName); - if (superType) { - hierarchy.push(...getTypeHierarchy(superType, allTypes)); - } - } - } - - return hierarchy; -} - -export function getAllDerivedTypes(baseName: string, allTypes: Type[]): Type[] { - const derived: Type[] = []; - - for (const type of allTypes) { - if (type.superTypes && type.superTypes.includes(baseName)) { - derived.push(type); - // Recursively find all derived types - derived.push(...getAllDerivedTypes(type.name, allTypes)); - } - } - - return derived; -} - -export function getLeafTypes(baseName: string, allTypes: Type[]): Type[] { - const allDerived = getAllDerivedTypes(baseName, allTypes); - const leafTypes: Type[] = []; - - for (const type of allDerived) { - // A type is a leaf if no other type derives from it - const hasChildren = allTypes.some(t => t.superTypes && t.superTypes.includes(type.name)); - if (!hasChildren && !type.isAbstract) { - leafTypes.push(type); - } - } - - return leafTypes; -} \ No newline at end of file diff --git a/spine-c-new/codegen/src/type-extractor.ts b/spine-c-new/codegen/src/type-extractor.ts new file mode 100644 index 000000000..fabb68201 --- /dev/null +++ b/spine-c-new/codegen/src/type-extractor.ts @@ -0,0 +1,88 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { execSync } from 'child_process'; + +const SPINE_CPP_PATH = path.join(__dirname, '../../../spine-cpp'); +const EXTRACTOR_SCRIPT = path.join(SPINE_CPP_PATH, 'extract-spine-cpp-types.js'); +const OUTPUT_FILE = path.join(__dirname, '../spine-cpp-types.json'); +const HEADERS_DIR = path.join(SPINE_CPP_PATH, 'spine-cpp/include/spine'); + +/** + * Checks if type extraction is needed based on file timestamps + */ +function isExtractionNeeded(): boolean { + // If output doesn't exist, we need to extract + if (!fs.existsSync(OUTPUT_FILE)) { + 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)); + + // 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) { + newestHeaderTime = stats.mtime.getTime(); + 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 + */ +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 + }); + + // Write to output file + fs.writeFileSync(OUTPUT_FILE, output); + + console.log(`Type extraction complete, wrote ${OUTPUT_FILE}`); + } catch (error: any) { + console.error('Failed to extract types:', error.message); + throw error; + } +} + +/** + * Loads the extracted type information + */ +export function loadTypes(): any { + if (!fs.existsSync(OUTPUT_FILE)) { + throw new Error(`Type information not found at ${OUTPUT_FILE}. Run extraction first.`); + } + + return JSON.parse(fs.readFileSync(OUTPUT_FILE, 'utf8')); +} \ No newline at end of file diff --git a/spine-c-new/codegen/src/types.ts b/spine-c-new/codegen/src/types.ts index 131a7faa7..97e29ad5f 100644 --- a/spine-c-new/codegen/src/types.ts +++ b/spine-c-new/codegen/src/types.ts @@ -45,6 +45,7 @@ export interface Exclusion { kind: 'type' | 'method'; typeName: string; methodName?: string; + isConst?: boolean; // For excluding specifically const or non-const versions } export function toSnakeCase(name: string): string { @@ -60,59 +61,135 @@ export function toCFunctionName(typeName: string, methodName: string): string { } export function toCTypeName(cppType: string): string { - // Handle basic types - if (cppType === 'float') return 'float'; - if (cppType === 'double') return 'double'; - if (cppType === 'int' || cppType === 'int32_t') return 'int32_t'; - if (cppType === 'unsigned int' || cppType === 'uint32_t') return 'uint32_t'; - if (cppType === 'short' || cppType === 'int16_t') return 'int16_t'; - if (cppType === 'unsigned short' || cppType === 'uint16_t') return 'uint16_t'; - if (cppType === 'bool') return 'spine_bool'; - if (cppType === 'void') return 'void'; - if (cppType === 'size_t') return 'spine_size_t'; - if (cppType === 'const char *' || cppType === 'String' || cppType === 'const String &') return 'const utf8 *'; + // Remove any spine:: namespace prefix first + cppType = cppType.replace(/^spine::/, ''); - // Handle RTTI type specially - if (cppType === 'const spine::RTTI &' || cppType === 'RTTI &' || cppType === 'const RTTI &') return 'spine_rtti'; + // 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' + }; - // Handle Vector types FIRST before checking for pointers - these should be converted to void* in C API - const vectorMatch = cppType.match(/Vector<(.+?)>/); - if (vectorMatch) { - const elementType = vectorMatch[1].trim(); - // Special case for Vector - use int32_t* - if (elementType === 'int') { - return 'int32_t *'; - } - // For now, use void* for other vector parameters since we can't expose templates - return 'void *'; + if (primitiveMap[cppType]) { + return primitiveMap[cppType]; } - // Handle pointers + // Category 2: Special types + if (cppType === 'String' || cppType === 'const String' || cppType === 'const char *') { + return 'const utf8 *'; + } + if (cppType === 'void *') { + return 'spine_void'; + } + if (cppType === 'DisposeRendererObject') { + return 'spine_dispose_renderer_object'; + } + if (cppType === 'TextureLoader' || cppType === 'TextureLoader *') { + return 'spine_texture_loader'; + } + if (cppType === 'PropertyId') { + return 'int64_t'; // PropertyId is typedef'd to long long + } + + // Category 3: Arrays - must check before pointers/references + const arrayMatch = cppType.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); + } + // Handle everything else (enums, classes) + else { + typeSuffix = toSnakeCase(elementType); + } + + return `spine_array_${typeSuffix}`; + } + + // Category 4: Pointers const pointerMatch = cppType.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} *`; + } + + // char* becomes utf8* + if (baseType === 'char' || baseType === 'const char') { + return 'utf8 *'; + } + + // Class pointers return `spine_${toSnakeCase(baseType)}`; } - // Handle references + // Category 5: References const refMatch = cppType.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' || baseType === 'spine::RTTI') return 'spine_rtti'; + if (baseType === 'RTTI') return 'spine_rtti'; + + // Non-const references to primitives become pointers (output parameters) + if (!isConst && primitiveMap[baseType]) { + const mappedType = primitiveMap[baseType]; + return mappedType === 'void' ? 'void *' : `${mappedType} *`; + } + + // Const references and class references - recurse without the reference return toCTypeName(baseType); } - // Handle enum types from spine namespace - const enumTypes = ['MixBlend', 'MixDirection', 'BlendMode', 'AttachmentType', 'EventType', - 'Format', 'TextureFilter', 'TextureWrap', 'Inherit', 'Physics', - 'PositionMode', 'Property', 'RotateMode', 'SequenceMode', 'SpacingMode']; - for (const enumType of enumTypes) { - if (cppType === enumType || cppType === `spine::${enumType}`) { - return `spine_${toSnakeCase(enumType)}`; - } + // 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)}`; } - // Default: assume it's a spine type + // Category 7: Classes (default case) + // Assume any remaining type is a spine class return `spine_${toSnakeCase(cppType)}`; } \ No newline at end of file diff --git a/spine-cpp/extract-spine-cpp-types.js b/spine-cpp/extract-spine-cpp-types.js index 0c0804ec7..accb0e96a 100755 --- a/spine-cpp/extract-spine-cpp-types.js +++ b/spine-cpp/extract-spine-cpp-types.js @@ -263,10 +263,8 @@ function extractLocalTypes(headerFile, typeMap = null) { } } - // Mark as abstract if it has pure virtual methods - if (hasPureVirtual) { - info.isAbstract = true; - } + // Always set isAbstract to a boolean value + info.isAbstract = hasPureVirtual; return info; } @@ -363,7 +361,12 @@ function extractLocalTypes(headerFile, typeMap = null) { types.push(typeInfo); } } else if (node.kind === 'CXXRecordDecl' && node.inner?.length > 0) { - types.push(extractTypeInfo(node)); + 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') { @@ -647,6 +650,17 @@ if (arg) { 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; + } + } } } }