[c] Update codegen README.md

This commit is contained in:
Mario Zechner 2025-07-14 21:23:08 +02:00
parent aac98324f2
commit 30d4cb9820

View File

@ -1,6 +1,6 @@
# 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.
This TypeScript-based code generator automatically creates a C wrapper API for the Spine C++ runtime. It parses the spine-cpp headers using Clang's AST and generates a complete C API with opaque types, following systematic type conversion rules.
## Table of Contents
@ -8,363 +8,456 @@ This TypeScript-based code generator automatically creates a C wrapper API for t
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)
5. [Usage](#usage)
6. [Type Conversion Rules](#type-conversion-rules)
7. [Exclusions System](#exclusions-system)
8. [Validation Checks](#validation-checks)
9. [Array Specializations](#array-specializations)
10. [Generated Code Examples](#generated-code-examples)
11. [Implementation Details](#implementation-details)
12. [Troubleshooting](#troubleshooting)
## Overview
The generator creates a C API that wraps the spine-cpp C++ runtime, allowing C programs to use Spine functionality. Key features:
The code generator performs static analysis on the spine-cpp headers to automatically generate a C API that wraps the C++ classes. It handles:
- **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
- Type conversions between C++ and C
- Method wrapping with proper parameter marshaling
- Memory management through constructors and destructors
- Enum conversions
- Array specializations for different element types
- Field accessors (getters/setters) for public fields
- Automatic validation and conflict detection
## Architecture
### Core Components
The generator follows a multi-stage pipeline:
```
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
```
1. **Type Extraction** (`type-extractor.ts`)
- Uses Clang's `-ast-dump=json` to parse C++ headers
- Extracts all public members (methods, fields, constructors, destructors)
- Handles template types and inheritance relationships
- Outputs to `spine-cpp-types.json`
### Data Flow
2. **Type Processing** (`index.ts`)
- Loads extracted types and exclusions
- Filters out template types, excluded types, and internal classes
- Determines which types inherit from SpineObject
- Validates that all types meet generation requirements
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/`
3. **Validation** (`checks.ts`)
- Detects const/non-const method conflicts
- Identifies multi-level pointers
- Finds field accessor conflicts
- Checks for method/type name collisions
- Validates return types
4. **Array Scanning** (`array-scanner.ts`)
- Scans all types for `Array<T>` usage
- Generates specialized array types for each element type
- Handles primitive, pointer, and enum arrays
5. **IR Generation** (`ir-generator.ts`)
- Converts C++ types to C intermediate representation
- Generates wrapper methods with proper marshaling
- Creates field accessors for public fields
- Adds constructors and destructors
6. **Code Writing** (`c-writer.ts`)
- Writes header files with C function declarations
- Writes implementation files with C++ wrapper code
- Generates array specialization files
- Creates main include files (`types.h`, `spine-c.h`)
## Type System
### Type Categories
The generator classifies all C++ types into systematic categories:
The generator categorizes types for systematic conversion:
1. **Primitives**: `int`, `float`, `double`, `bool`, `char`, `void`, `size_t`
- Direct mapping (e.g., `bool``bool`)
1. **Primitives**: Direct mapping (int, float, bool, size_t, etc.)
2. **Special Types**: Custom conversions (String → const char*, PropertyId → int64_t)
3. **Arrays**: Template specializations (Array<T> → spine_array_<type>)
4. **Pointers**: Class pointers become opaque types
5. **References**: Converted based on const-ness and type
6. **Enums**: Prefixed and snake_cased
7. **Classes**: Converted to opaque pointers with prefix
2. **Special Types**: String, function pointers, PropertyId
- `String``const utf8 *`
- `void *``spine_void`
- `PropertyId``int64_t` (typedef'd to long long)
### C++ to C Type Mapping
3. **Arrays**: `Array<T>` specializations
- Generated as `spine_array_<element_type>`
- Full API for each specialization
The generator uses opaque pointers for all C++ classes:
- `Skeleton*``spine_skeleton` (opaque pointer)
- `const Skeleton*``const spine_skeleton`
- `Skeleton&``spine_skeleton` (references become pointers)
4. **Pointers**: Type followed by `*`
- Primitive pointers stay as-is (`float *`)
- Class pointers become opaque (`Bone *``spine_bone`)
### Special Types
- `String``const char*`
- `PropertyId``int64_t`
- `Array<T>``spine_array_T` (specialized types)
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();
}
```
### Primitive Types
- Primitives pass through unchanged: `int`, `float`, `bool`, etc.
- Non-const primitive references become pointers: `float&``float*`
## 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
```
codegen/
├── src/
│ ├── index.ts # Main entry point and orchestration
│ ├── type-extractor.ts # Clang AST parsing
│ ├── types.ts # Type definitions and conversion logic
│ ├── c-types.ts # C IR type definitions
│ ├── array-scanner.ts # Array specialization detection
│ ├── checks.ts # Validation checks
│ ├── exclusions.ts # Exclusion handling
│ ├── ir-generator.ts # C++ to C IR conversion
│ ├── c-writer.ts # File generation
│ └── warnings.ts # Warning collection
├── exclusions.txt # Type/method exclusions
├── spine-cpp-types.json # Extracted type information
├── package.json # Node.js configuration
├── tsconfig.json # TypeScript configuration
└── generated/ # Output directory (temporary)
```
C can only have one function name. We exclude const versions and expose non-const.
Generated files are output to `../src/generated/`:
- Individual files per type (e.g., `skeleton.h`, `skeleton.cpp`)
- `types.h` - Forward declarations for all types
- `arrays.h/cpp` - Array specializations
- `spine-c.h` - Main include file
### 5. Why Static RTTI Methods?
## Usage
RTTI objects are singletons in spine-cpp. Making getRTTI() static:
- Reflects actual usage (Type::rtti)
- Avoids unnecessary object parameter
- Cleaner API
```bash
# Install dependencies
npm install
## 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
npx tsx src/index.ts
# The generated files will be in ../src/generated/
```
### 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
The generator automatically:
- Detects when spine-cpp headers have changed
- Regenerates only when necessary
- Reports warnings and errors during generation
## Type Conversion Rules
### toCTypeName Function
### Primitive Types
Primitive types are "pass-through".
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
```
C++ Type → C Type
─────────────────────────────────
int → int
float* → float*
const char* → const char*
bool → bool (stdbool.h)
size_t → size_t
```
### Build and Run
```bash
npm run build # Compile TypeScript
node dist/index.js # Run generator
### Class Types
```
C++ Type → C Type
─────────────────────────────────
Bone* → spine_bone
const Bone* → const spine_bone
Bone& → spine_bone
const Bone& → spine_bone
```
### What Happens
### Special Cases
```
C++ Type → C Type
─────────────────────────────────
String → const char*
String& → const char*
const String& → const char*
PropertyId → int64_t
Array<float> → spine_array_float
Array<Bone*> → spine_array_bone
```
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 Parameters
```
C++ Type → C Type
─────────────────────────────────
float& → float* (output param)
int& → int* (output param)
```
### Output
### Function Naming
```
C++ Method → C Function
─────────────────────────────────────────
Skeleton::updateCache() → spine_skeleton_update_cache()
AnimationState::apply() → spine_animation_state_apply()
Bone::getX() → spine_bone_get_x()
```
- Generates ~150 .h/.cpp file pairs
- Creates arrays.h with ~30 specializations
- All files include proper license headers
- Organized by type for easy navigation
## Exclusions System
## Maintenance
The `exclusions.txt` file controls what gets generated:
### Type Exclusions
Exclude entire types from generation:
```
type: SkeletonClipping
type: Triangulator
```
### Method Exclusions
Exclude specific methods:
```
method: AnimationState::setListener
method: AnimationState::addListener
```
### Const-Specific Exclusions
Exclude only const or non-const versions:
```
method: BoneData::getSetupPose const
```
### Constructor Exclusions
Allow type but prevent instantiation:
```
method: AtlasRegion::AtlasRegion
```
### Field Accessor Exclusions
Control field getter/setter generation:
```
field: AtlasRegion::names # Exclude both getter and setter
field-get: SecretData::password # Exclude only getter
field-set: Bone::x # Exclude only setter
```
### Type-Wide Field Exclusions
Exclude all field accessors for a type:
```
field: RenderCommand # No field accessors at all
field-get: DebugData # No getters (write-only fields)
field-set: RenderCommand # No setters (read-only fields)
```
## Validation Checks
The generator performs extensive validation to ensure correctness:
### 1. Const/Non-Const Conflicts
Detects methods with both const and non-const versions:
```cpp
T& getValue(); // Non-const version
const T& getValue() const; // Const version
```
### 2. Multi-Level Pointers
Rejects types with multiple pointer levels:
```cpp
char** strings; // Not supported
void*** ptr; // Not supported
```
### 3. Field Accessor Conflicts
Detects when generated accessors would conflict with existing methods:
```cpp
class Bone {
float x; // Would generate get_x/set_x
float getX(); // Conflicts with generated getter
};
```
### 4. Method/Type Name Conflicts
Ensures generated function names don't collide with type names:
```cpp
class BonePose { }; // → spine_bone_pose
class Bone {
void pose(); // → spine_bone_pose (conflict!)
};
```
### 5. Value Return Types
Detects methods returning non-primitive types by value:
```cpp
Color getColor(); // Cannot return objects by value in C
```
## Array Specializations
The generator automatically creates specialized array types for any `Array<T>` found in the API:
### Primitive Arrays
```cpp
Array<float> → spine_array_float
Array<int> → spine_array_int
Array<unsigned short> → spine_array_unsigned_short
```
### Pointer Arrays
```cpp
Array<Bone*> → spine_array_bone
Array<Slot*> → spine_array_slot
Array<float*> → spine_array_float_ptr
```
### Enum Arrays
```cpp
Array<BlendMode> → spine_array_blend_mode
Array<PropertyId> → spine_array_property_id
```
### Unsupported Arrays
- `Array<String>` - Use `const char**` instead
- `Array<Array<T>>` - Nested arrays not supported
- Arrays with const elements
## Generated Code Examples
### Class Wrapper
```c
// Header: skeleton.h
typedef struct spine_skeleton* spine_skeleton;
spine_skeleton spine_skeleton_new(spine_skeleton_data data);
void spine_skeleton_dispose(spine_skeleton self);
void spine_skeleton_update_cache(spine_skeleton self);
float spine_skeleton_get_x(const spine_skeleton self);
void spine_skeleton_set_x(spine_skeleton self, float value);
// Implementation: skeleton.cpp
spine_skeleton spine_skeleton_new(spine_skeleton_data data) {
return (spine_skeleton) new (__FILE__, __LINE__) Skeleton((SkeletonData*)data);
}
void spine_skeleton_update_cache(spine_skeleton self) {
((Skeleton*)self)->updateCache();
}
```
### Enum Wrapper
```c
// Header: blend_mode.h
typedef enum spine_blend_mode {
SPINE_BLEND_MODE_NORMAL = 0,
SPINE_BLEND_MODE_ADDITIVE = 1,
SPINE_BLEND_MODE_MULTIPLY = 2,
SPINE_BLEND_MODE_SCREEN = 3
} spine_blend_mode;
// Implementation: blend_mode.cpp
spine_blend_mode spine_blend_mode_from_cpp(BlendMode value) {
return (spine_blend_mode)value;
}
BlendMode spine_blend_mode_to_cpp(spine_blend_mode value) {
return (BlendMode)value;
}
```
### Array Specialization
```c
// Header: array_float.h
typedef struct spine_array_float* spine_array_float;
spine_array_float spine_array_float_new(int32_t capacity);
void spine_array_float_dispose(spine_array_float self);
int32_t spine_array_float_get_size(const spine_array_float self);
float spine_array_float_get(const spine_array_float self, int32_t index);
void spine_array_float_set(spine_array_float self, int32_t index, float value);
// Implementation: array_float.cpp
struct spine_array_float {
Array<float> data;
};
spine_array_float spine_array_float_new(int32_t capacity) {
auto* arr = new (__FILE__, __LINE__) spine_array_float();
arr->data.setCapacity(capacity);
return arr;
}
```
## Implementation Details
### Memory Management
- All C++ objects inheriting from `SpineObject` use location-based `operator new`
- Constructors use `new (__FILE__, __LINE__)` for memory tracking
- Destructors call `delete` on the C++ object
- Array types are wrapped in structs to maintain C++ semantics
### Constructor Generation
- Only generates constructors for non-abstract classes
- Only generates constructors for classes inheriting from `SpineObject`
- Requires at least one public constructor or explicit exclusion
- Constructor overloads are numbered: `_new`, `_new2`, `_new3`
### Field Accessor Generation
- Generates getters for all non-static public fields
- Generates setters for non-const, non-reference fields
- Uses direct field access, not C++ getter/setter methods
- Handles nested field access (e.g., `obj.field.x`)
### Method Overloading
- Constructor overloads are numbered: `_new`, `_new2`, `_new3`
- Other overloads must be excluded (C doesn't support overloading)
- Const/non-const conflicts are detected and reported
### RTTI Handling
- Uses Spine's custom RTTI system (`getRTTI().instanceOf()`)
- No C++ RTTI or exceptions are used
- RTTI checks are performed in generated code where needed
### Warning System
- Collects non-fatal issues during generation
- Reports abstract classes, missing constructors, etc.
- Warnings don't stop generation but are reported at the end
## Troubleshooting
### Common Errors
1. **"Unknown type: X"**
- The type is not a primitive and not in the extracted types
- Solution: Add to exclusions or check spelling
2. **"Multi-level pointers are not supported"**
- Type contains `**` or more pointers
- Solution: Refactor C++ code or exclude
3. **"Array<String> is not supported"**
- String arrays need special handling
- Solution: Use `const char**` in C++ or exclude
4. **"No public constructors"**
- Class has no public constructors for instantiation
- Solution: Add public constructor or exclude
5. **"Method/type name conflict"**
- Generated function name collides with a type name
- Solution: Rename method or exclude
### Debugging Tips
1. Check `spine-cpp-types.json` for extracted type information
2. Look for "Excluding" messages in console output
3. Verify inheritance with "inherits from SpineObject" messages
4. Array specializations are listed with element type mapping
5. Check warnings at the end of generation for issues
### Adding New Types
1. No action needed - automatically detected from spine-cpp
1. Ensure the type is in spine-cpp headers
2. Remove from exclusions.txt if previously excluded
3. Check that all dependent types are included
4. Run generator and fix any reported issues
5. Verify generated code compiles
### Excluding Types/Methods
### Performance Considerations
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
- Type extraction uses Clang AST parsing (slow but accurate)
- File generation is parallelized where possible
- Array scanning happens after type filtering for efficiency
- Validation checks run before generation to fail fast
- Incremental generation avoids regenerating unchanged files