[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
This commit is contained in:
Mario Zechner 2025-07-08 23:08:48 +02:00
parent fdbb180409
commit 759434e461
17 changed files with 1561 additions and 272 deletions

View File

@ -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<T> 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<T>` specializations
- Generated as `spine_array_<element_type>`
- 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_<snake_case>`
### 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<T> found in spine-cpp
- Complete API for each specialization
- **<type>.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_<type>_create()` for default constructor
- Generates `spine_<type>_create_with_<params>()` for parameterized constructors
- Always generates `spine_<type>_dispose()` for cleanup
#### Methods
- Getters: `spine_<type>_get_<property>()`
- Setters: `spine_<type>_set_<property>()`
- Other methods: `spine_<type>_<method_name>()`
- 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<T> 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<T> 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<T>, 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<Array<T>>)
- **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<T> 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

View File

@ -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
method: EventData::setBalance
# Vector<String> 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

View File

@ -1,8 +0,0 @@
# Base types that need RTTI emulation
Attachment
Constraint
ConstraintData
Pose
Posed
PosedData
Timeline

View File

@ -0,0 +1,155 @@
import { Type, SpineTypes } from './types';
import { isTypeExcluded } from './exclusions';
export interface ArraySpecialization {
cppType: string; // e.g. "Array<float>"
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<T> specializations
* Only includes arrays from non-excluded types
*/
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]) {
// 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;
}

View File

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

View File

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

View File

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

@ -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 <spine/Array.h>');
source.push('#include <spine/spine.h>');
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;
}
}

View File

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

View File

@ -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 ? ',' : ''}`);

View File

@ -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<string, Member[]>();
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<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);
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);
}
}

View File

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

View File

@ -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<string, Member[]>();
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<string, Type>();
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 <spine/spine.h>');
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);

View File

@ -1,61 +0,0 @@
import * as fs from 'fs';
import { Type } from './types';
export function loadRttiBases(filePath: string): Set<string> {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
const bases = new Set<string>();
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;
}

View File

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

View File

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

View File

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