mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-02-06 07:14:55 +08:00
[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:
parent
fdbb180409
commit
759434e461
370
spine-c-new/codegen/README.md
Normal file
370
spine-c-new/codegen/README.md
Normal 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
|
||||
@ -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
|
||||
@ -1,8 +0,0 @@
|
||||
# Base types that need RTTI emulation
|
||||
Attachment
|
||||
Constraint
|
||||
ConstraintData
|
||||
Pose
|
||||
Posed
|
||||
PosedData
|
||||
Timeline
|
||||
155
spine-c-new/codegen/src/array-scanner.ts
Normal file
155
spine-c-new/codegen/src/array-scanner.ts
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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'));
|
||||
}
|
||||
}
|
||||
93
spine-c-new/codegen/src/find-array-types.ts
Normal file
93
spine-c-new/codegen/src/find-array-types.ts
Normal 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}`);
|
||||
}
|
||||
242
spine-c-new/codegen/src/generators/array-generator.ts
Normal file
242
spine-c-new/codegen/src/generators/array-generator.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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 ? ',' : ''}`);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 };
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
88
spine-c-new/codegen/src/type-extractor.ts
Normal file
88
spine-c-new/codegen/src/type-extractor.ts
Normal 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'));
|
||||
}
|
||||
@ -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)}`;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user