Mario Zechner c79031cc75 Remove npx install prompts and rely on auto-download
- Use npx -y for tsx to avoid install prompts
- Remove tsx from devDependencies since we use npx
- Remove npm install checks from format-ts.sh
2025-07-16 05:05:33 +02:00

14 KiB

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 using Clang's AST and generates a complete C API with opaque types, following systematic type conversion rules.

Table of Contents

  1. Overview
  2. Architecture
  3. Type System
  4. File Structure
  5. Usage
  6. Type Conversion Rules
  7. Exclusions System
  8. Validation Checks
  9. Array Specializations
  10. Generated Code Examples
  11. Implementation Details
  12. Troubleshooting

Overview

The code generator performs static analysis on the spine-cpp headers to automatically generate a C API that wraps the C++ classes. It handles:

  • 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

The generator follows a multi-stage pipeline:

  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
  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
  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 categorizes types for systematic conversion:

  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 → spine_array_)
  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

C++ to C Type Mapping

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)

Special Types

  • Stringconst char*
  • PropertyIdint64_t
  • Array<T>spine_array_T (specialized types)

Primitive Types

  • Primitives pass through unchanged: int, float, bool, etc.
  • Non-const primitive references become pointers: float&float*

File Structure

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)

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

Usage

# Install dependencies
npm install

npx -y tsx src/index.ts
# The generated files will be in ../src/generated/

The generator automatically:

  • Detects when spine-cpp headers have changed
  • Regenerates only when necessary
  • Reports warnings and errors during generation

Type Conversion Rules

Primitive Types

Primitive types are "pass-through".

C++ Type          → C Type
─────────────────────────────────
int               → int
float*            → float*
const char*       → const char*
bool              → bool (stdbool.h)
size_t            → size_t

Class Types

C++ Type          → C Type
─────────────────────────────────
Bone*             → spine_bone
const Bone*       → const spine_bone
Bone&             → spine_bone
const Bone&       → spine_bone

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

Output Parameters

C++ Type          → C Type
─────────────────────────────────
float&            → float* (output param)
int&              → int* (output param)

Function Naming

C++ Method                → C Function
─────────────────────────────────────────
Skeleton::updateCache()   → spine_skeleton_update_cache()
AnimationState::apply()   → spine_animation_state_apply()
Bone::getX()              → spine_bone_get_x()

Exclusions System

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:

T& getValue();              // Non-const version
const T& getValue() const;  // Const version

2. Multi-Level Pointers

Rejects types with multiple pointer levels:

char** strings;  // Not supported
void*** ptr;     // Not supported

3. Field Accessor Conflicts

Detects when generated accessors would conflict with existing methods:

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:

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:

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

Array<float>       spine_array_float
Array<int>         spine_array_int
Array<unsigned short>  spine_array_unsigned_short

Pointer Arrays

Array<Bone*>       spine_array_bone
Array<Slot*>       spine_array_slot
Array<float*>      spine_array_float_ptr

Enum Arrays

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

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

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

// 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 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. 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

Performance Considerations

  • 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