Mario Zechner пре 1 месец
родитељ
комит
736f5148f1

+ 161 - 39
spine-c/codegen/README.md

@@ -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 using Clang's AST 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. The generator also builds inheritance maps and interface information for multi-language binding generation.
 
 ## Table of Contents
 
@@ -15,7 +15,8 @@ This TypeScript-based code generator automatically creates a C wrapper API for t
 9. [Array Specializations](#array-specializations)
 10. [Generated Code Examples](#generated-code-examples)
 11. [Implementation Details](#implementation-details)
-12. [Troubleshooting](#troubleshooting)
+12. [Development Tools](#development-tools)
+13. [Troubleshooting](#troubleshooting)
 
 ## Overview
 
@@ -28,6 +29,7 @@ The code generator performs static analysis on the spine-cpp headers to automati
 - Array specializations for different element types
 - Field accessors (getters/setters) for public fields
 - Automatic validation and conflict detection
+- Inheritance analysis and interface detection for multi-language bindings
 
 ## Architecture
 
@@ -67,7 +69,13 @@ The generator follows a multi-stage pipeline:
    - 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`)
+   - Creates main include files (`types.h`)
+
+7. **Inheritance Analysis**
+   - Builds inheritance maps for single-inheritance languages (Dart, Swift, Java)
+   - Identifies pure interfaces vs concrete classes
+   - Detects multiple concrete inheritance (not supported)
+   - Generates inheritance information for language binding generators
 
 ## Type System
 
@@ -106,6 +114,7 @@ codegen/
 ├── src/
 │   ├── index.ts           # Main entry point and orchestration
 │   ├── type-extractor.ts  # Clang AST parsing
+│   ├── cpp-check.ts       # C++ nullability analysis tool
 │   ├── types.ts           # Type definitions and conversion logic
 │   ├── c-types.ts         # C IR type definitions
 │   ├── array-scanner.ts   # Array specialization detection
@@ -114,18 +123,22 @@ codegen/
 │   ├── ir-generator.ts    # C++ to C IR conversion
 │   ├── c-writer.ts        # File generation
 │   └── warnings.ts        # Warning collection
+├── dist/                  # TypeScript compilation output
 ├── exclusions.txt         # Type/method exclusions
 ├── spine-cpp-types.json   # Extracted type information
+├── nullable.md            # C++ nullability analysis results
+├── out.json              # Debug output file
 ├── package.json           # Node.js configuration
 ├── tsconfig.json          # TypeScript configuration
-└── generated/             # Output directory (temporary)
+├── tsfmt.json            # TypeScript formatter configuration
+├── biome.json            # Biome linter configuration
+└── node_modules/         # Dependencies
 ```
 
 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
 
@@ -133,14 +146,36 @@ Generated files are output to `../src/generated/`:
 # Install dependencies
 npm install
 
-npx -y tsx src/index.ts
+# Run the code generator
+npx tsx src/index.ts
+
+# Or export JSON for debugging
+npx tsx src/index.ts --export-json
+
 # The generated files will be in ../src/generated/
 ```
 
+### C++ Nullability Analysis Tool
+
+The codegen includes a tool to analyze spine-cpp for nullability patterns:
+
+```bash
+# Generate nullable.md with clickable links to methods with nullable inputs/outputs
+npm run cpp-check
+```
+
+This tool identifies all methods that either:
+- Return pointer types (nullable return values)
+- Take pointer parameters (nullable inputs)
+
+The output `nullable.md` contains clickable markdown links for easy navigation in VS Code. This is useful for cleaning up the spine-cpp API to use references vs pointers appropriately to signal nullability.
+
 The generator automatically:
 - Detects when spine-cpp headers have changed
 - Regenerates only when necessary
 - Reports warnings and errors during generation
+- Formats the generated C++ code using the project's formatter
+- Builds inheritance maps for multi-language binding generation
 
 ## Type Conversion Rules
 
@@ -320,14 +355,14 @@ Array<PropertyId> → spine_array_property_id
 // Header: skeleton.h
 typedef struct spine_skeleton* spine_skeleton;
 
-spine_skeleton spine_skeleton_new(spine_skeleton_data data);
+spine_skeleton spine_skeleton_create(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) {
+spine_skeleton spine_skeleton_create(spine_skeleton_data data) {
     return (spine_skeleton) new (__FILE__, __LINE__) Skeleton((SkeletonData*)data);
 }
 
@@ -339,46 +374,83 @@ void spine_skeleton_update_cache(spine_skeleton self) {
 ### Enum Wrapper
 ```c
 // Header: blend_mode.h
+#ifndef SPINE_SPINE_BLEND_MODE_H
+#define SPINE_SPINE_BLEND_MODE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 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_ADDITIVE,
+    SPINE_BLEND_MODE_MULTIPLY,
+    SPINE_BLEND_MODE_SCREEN
 } spine_blend_mode;
 
-// Implementation: blend_mode.cpp
-spine_blend_mode spine_blend_mode_from_cpp(BlendMode value) {
-    return (spine_blend_mode)value;
+#ifdef __cplusplus
 }
+#endif
 
-BlendMode spine_blend_mode_to_cpp(spine_blend_mode value) {
-    return (BlendMode)value;
-}
+#endif /* SPINE_SPINE_BLEND_MODE_H */
 ```
 
 ### Array Specialization
+Arrays are generated as opaque types with complete CRUD operations. All arrays are consolidated into `arrays.h` and `arrays.cpp`.
+
 ```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;
-};
+// Header: arrays.h
+SPINE_OPAQUE_TYPE(spine_array_float)
+
+// Creation functions
+spine_array_float spine_array_float_create(void);
+spine_array_float spine_array_float_create_with_capacity(size_t initialCapacity);
+
+// Memory management
+void spine_array_float_dispose(spine_array_float array);
+void spine_array_float_clear(spine_array_float array);
+
+// Size and capacity operations
+size_t spine_array_float_get_capacity(spine_array_float array);
+size_t spine_array_float_size(spine_array_float array);
+spine_array_float spine_array_float_set_size(spine_array_float array, size_t newSize, float defaultValue);
+void spine_array_float_ensure_capacity(spine_array_float array, size_t newCapacity);
+
+// Element operations
+void spine_array_float_add(spine_array_float array, float inValue);
+void spine_array_float_add_all(spine_array_float array, spine_array_float inValue);
+void spine_array_float_clear_and_add_all(spine_array_float array, spine_array_float inValue);
+void spine_array_float_remove_at(spine_array_float array, size_t inIndex);
+
+// Search operations
+bool spine_array_float_contains(spine_array_float array, float inValue);
+int spine_array_float_index_of(spine_array_float array, float inValue);
+
+// Direct buffer access
+float *spine_array_float_buffer(spine_array_float array);
+
+// Implementation: arrays.cpp
+spine_array_float spine_array_float_create(void) {
+    return (spine_array_float) new (__FILE__, __LINE__) Array<float>();
+}
 
-spine_array_float spine_array_float_new(int32_t capacity) {
-    auto* arr = new (__FILE__, __LINE__) spine_array_float();
-    arr->data.setCapacity(capacity);
-    return arr;
+void spine_array_float_dispose(spine_array_float array) {
+    delete (Array<float> *) array;
+}
+
+void spine_array_float_add(spine_array_float array, float inValue) {
+    Array<float> *_array = (Array<float> *) array;
+    _array->add(inValue);
+}
+
+float *spine_array_float_buffer(spine_array_float array) {
+    Array<float> *_array = (Array<float> *) array;
+    return _array->buffer();
 }
 ```
 
+Arrays are generated for all basic types (`float`, `int`, `unsigned_short`, `property_id`) and all object types used in collections throughout the API. The implementation directly casts the opaque handle to the underlying `Array<T>*` type.
+
 ## Implementation Details
 
 ### Memory Management
@@ -391,7 +463,7 @@ spine_array_float spine_array_float_new(int32_t capacity) {
 - 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`
+- Constructor overloads are numbered: `_create`, `_create2`, `_create3`
 
 ### Field Accessor Generation
 - Generates getters for all non-static public fields
@@ -400,8 +472,9 @@ spine_array_float spine_array_float_new(int32_t capacity) {
 - 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)
+- Constructor overloads are numbered: `_create`, `_create2`, `_create3`, etc.
+- Method overloads are numbered with suffixes: `_1`, `_2`, `_3`, etc.
+- Methods named "create" get `_method` suffix to avoid constructor conflicts
 - Const/non-const conflicts are detected and reported
 
 ### RTTI Handling
@@ -410,10 +483,21 @@ spine_array_float spine_array_float_new(int32_t capacity) {
 - RTTI checks are performed in generated code where needed
 
 ### Warning System
-- Collects non-fatal issues during generation
+- Collects non-fatal issues during generation using `WarningsCollector`
 - Reports abstract classes, missing constructors, etc.
+- Groups warnings by pattern to avoid repetition
 - Warnings don't stop generation but are reported at the end
 
+### Interface Detection
+- Automatically identifies pure interfaces (classes with only pure virtual methods)
+- Distinguishes between concrete classes and interfaces for inheritance mapping
+- Used to determine extends vs implements relationships for target languages
+
+### Multiple Inheritance Handling
+- Detects multiple concrete inheritance scenarios
+- Fails generation with clear error messages when unsupported patterns are found
+- Provides guidance on converting concrete classes to interfaces
+
 ## Troubleshooting
 
 ### Common Errors
@@ -438,6 +522,11 @@ spine_array_float spine_array_float_new(int32_t capacity) {
    - Generated function name collides with a type name
    - Solution: Rename method or exclude
 
+6. **"Multiple concrete inheritance detected"**
+   - A class inherits from multiple concrete (non-interface) classes
+   - Solution: Convert one of the parent classes to a pure interface
+   - Check the error message for specific guidance on which classes to modify
+
 ### Debugging Tips
 
 1. Check `spine-cpp-types.json` for extracted type information
@@ -445,6 +534,9 @@ spine_array_float spine_array_float_new(int32_t capacity) {
 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
+6. Use `--export-json` flag to export inheritance and type information as JSON
+7. Check `out.json` for debug output when troubleshooting
+8. Review console output for inheritance mapping information (extends/mixins)
 
 ### Adding New Types
 
@@ -460,4 +552,34 @@ spine_array_float spine_array_float_new(int32_t capacity) {
 - 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
+- Incremental generation avoids regenerating unchanged files
+
+## Development Tools
+
+The codegen project includes several development tools and configurations:
+
+### Biome Configuration (`biome.json`)
+- Linting enabled with recommended rules
+- Formatting disabled (uses external formatter)
+- Helps maintain code quality during development
+
+### TypeScript Formatter (`tsfmt.json`)
+- Comprehensive formatting rules for TypeScript code
+- Configures indentation, spacing, and code style
+- Used for consistent code formatting across the project
+
+### Build Output (`dist/`)
+- Contains compiled TypeScript files
+- Generated JavaScript and declaration files
+- Source maps for debugging
+
+### Debug Output (`out.json`)
+- Contains debug information from the generation process
+- Useful for troubleshooting and understanding the generated data structure
+
+### Dependencies
+The project uses minimal dependencies for maximum compatibility:
+- `@types/node` - Node.js type definitions
+- `tsx` - TypeScript execution engine
+- `typescript-formatter` - Code formatting
+- `@biomejs/biome` - Fast linter for code quality

+ 4 - 0
spine-c/codegen/package.json

@@ -1,6 +1,10 @@
 {
   "name": "spine-c-codegen",
   "type": "module",
+  "scripts": {
+    "generate": "tsx src/index.ts",
+    "null-analysis": "tsx src/null-analysis.ts"
+  },
   "devDependencies": {
     "@types/node": "^20.0.0",
     "tsx": "^4.0.0",

+ 205 - 0
spine-c/codegen/src/null-analysis.ts

@@ -0,0 +1,205 @@
+#!/usr/bin/env node
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { extractTypes } from './type-extractor';
+import type { ClassOrStruct, Method, Type } from './types';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+/**
+ * Checks if a type string represents a pointer to a class instance
+ */
+function isPointerToClass(typeStr: string, allTypes: Type[]): boolean {
+    // Remove const, references, and whitespace
+    const cleanType = typeStr.replace(/\bconst\b/g, '').replace(/&/g, '').trim();
+    
+    // Check if it ends with * (pointer)
+    if (!cleanType.endsWith('*')) {
+        return false;
+    }
+    
+    // Extract the base type (remove the *)
+    const baseType = cleanType.replace(/\*+$/, '').trim();
+    
+    // Check if the base type is a class/struct in our type list
+    return allTypes.some(type => 
+        type.kind !== 'enum' && type.name === baseType
+    );
+}
+
+/**
+ * Checks if a type string represents a class instance (for return types)
+ */
+function isClassType(typeStr: string, allTypes: Type[]): boolean {
+    // Remove const, references, and whitespace  
+    const cleanType = typeStr.replace(/\bconst\b/g, '').replace(/&/g, '').replace(/\*/g, '').trim();
+    
+    // Check if the base type is a class/struct in our type list
+    return allTypes.some(type => 
+        type.kind !== 'enum' && type.name === cleanType
+    );
+}
+
+/**
+ * Analyzes all methods to find those with nullable inputs or outputs
+ */
+function analyzeNullableMethods(): void {
+    console.log('Extracting type information...');
+    const allTypes = extractTypes();
+    
+    const nullableMethods: Array<{
+        filename: string;
+        line: number;
+        signature: string;
+        reason: string;
+    }> = [];
+    
+    // Process each type
+    for (const type of allTypes) {
+        if (type.kind === 'enum') continue;
+        
+        const classType = type as ClassOrStruct;
+        if (!classType.members) continue;
+        
+        // Get the source file name relative to the nullable file location
+        const filename = `../../spine-cpp/include/spine/${classType.name}.h`;
+        
+        // Process each method
+        for (const member of classType.members) {
+            if (member.kind !== 'method') continue;
+            
+            const method = member as Method;
+            const signature = buildMethodSignature(classType.name, method);
+            
+            // Check return type - if it returns a pointer to a class
+            if (method.returnType) {
+                const cleanReturnType = method.returnType.replace(/\bconst\b/g, '').trim();
+                if (isPointerToClass(cleanReturnType, allTypes)) {
+                    nullableMethods.push({
+                        filename,
+                        line: method.loc.line,
+                        signature,
+                        reason: `returns nullable pointer: ${method.returnType}`
+                    });
+                }
+            }
+            
+            // Check parameters - if any parameter is a pointer to a class
+            if (method.parameters) {
+                for (const param of method.parameters) {
+                    if (isPointerToClass(param.type, allTypes)) {
+                        nullableMethods.push({
+                            filename,
+                            line: method.loc.line,
+                            signature,
+                            reason: `takes nullable parameter '${param.name}': ${param.type}`
+                        });
+                        break; // Only report once per method
+                    }
+                }
+            }
+        }
+    }
+    
+    // Sort by filename and line
+    nullableMethods.sort((a, b) => {
+        if (a.filename !== b.filename) {
+            return a.filename.localeCompare(b.filename);
+        }
+        return a.line - b.line;
+    });
+    
+    // Write results to nullable.md file
+    const outputPath = path.join(__dirname, '../nullable.md');
+    
+    const instructions = `# Spine C++ Nullability Cleanup
+
+## Instructions
+
+**Phase 1: Enrich nullable.md (if implementations not yet inlined)**
+If checkboxes don't contain concrete implementations:
+1. Use parallel Task agents to find implementations (agents do NOT write to file)
+2. Each agent researches 10-15 methods and returns structured data:
+   \`\`\`
+   METHOD: [method signature]
+   CPP_HEADER: [file:line] [declaration]
+   CPP_IMPL: [file:line] [implementation code]
+   JAVA_IMPL: [file:line] [java method code]
+   ---
+   \`\`\`
+3. Collect all agent results and do ONE MultiEdit to update nullable.md
+4. Inline implementations BELOW each existing checkbox (keep original checkbox text):
+   \`\`\`
+   - [ ] [keep original checkbox line exactly as is]
+     **C++ Implementation:**
+     \`\`\`cpp
+     // Header: [file:line]
+     [declaration]
+     // Implementation: [file:line] 
+     [implementation body]
+     \`\`\`
+     **Java Implementation:**
+     \`\`\`java
+     // [file:line]
+     [java method body]
+     \`\`\`
+   \`\`\`
+
+**Phase 2: Review and Update**
+For each unchecked checkbox (now with implementations inlined):
+1. **Present both implementations** from the checkbox
+2. **Ask if we need to change the C++ signature** based on Java nullability patterns (y/n)
+3. **Make changes if needed**
+   - Change the signature in the header file
+   - Update the implementation in the corresponding .cpp file
+   - Run \`../../spine-cpp/build.sh\` to confirm the changes compile successfully
+4. **Confirm changes**
+   - Summarize what was changed
+   - Ask for confirmation that the changes are correct (y/n)
+   - If yes, check the checkbox and move to the next unchecked item
+
+## Methods to Review
+
+`;
+
+    const methodsList = nullableMethods.map(m => 
+        `- [ ] [${m.filename}:${m.line}](${m.filename}#L${m.line}) ${m.signature} // ${m.reason}`
+    ).join('\n');
+    
+    fs.writeFileSync(outputPath, instructions + methodsList + '\n');
+    
+    console.log(`Found ${nullableMethods.length} methods with nullable inputs/outputs`);
+    console.log(`Results written to: ${outputPath}`);
+    
+    // Print summary statistics
+    const byReason = new Map<string, number>();
+    for (const method of nullableMethods) {
+        const reasonType = method.reason.startsWith('returns') ? 'nullable return' : 'nullable parameter';
+        byReason.set(reasonType, (byReason.get(reasonType) || 0) + 1);
+    }
+    
+    console.log('\nSummary:');
+    for (const [reason, count] of byReason) {
+        console.log(`  ${reason}: ${count} methods`);
+    }
+}
+
+/**
+ * Builds a method signature string
+ */
+function buildMethodSignature(className: string, method: Method): string {
+    const params = method.parameters?.map(p => `${p.type} ${p.name}`).join(', ') || '';
+    const constStr = method.isConst ? ' const' : '';
+    return `${method.returnType || 'void'} ${className}::${method.name}(${params})${constStr}`;
+}
+
+// Main execution
+if (import.meta.url === `file://${process.argv[1]}`) {
+    try {
+        analyzeNullableMethods();
+    } catch (error) {
+        console.error('Error during analysis:', error);
+        process.exit(1);
+    }
+}

+ 32 - 4
spine-c/codegen/src/type-extractor.ts

@@ -122,12 +122,19 @@ function extractMember(inner: any, parent: any): Member & { access?: 'public' |
 
     switch (inner.kind) {
         case 'FieldDecl': {
+            if (!inner.loc) {
+                throw new Error(`Failed to extract location for field '${inner.name || 'unknown'}' in ${parent.name || 'unknown'}`);
+            }
             const field: Field & { access?: 'public' | 'protected' } = {
                 kind: 'field',
                 name: inner.name || '',
                 type: inner.type?.qualType || '',
                 isStatic: inner.storageClass === 'static',
-                access: 'public' // Will be set correctly later
+                access: 'public', // Will be set correctly later
+                loc: {
+                    line: inner.loc.line || 0,
+                    col: inner.loc.col || 0
+                }
             };
             return field;
         }
@@ -136,6 +143,9 @@ function extractMember(inner: any, parent: any): Member & { access?: 'public' |
             // Skip operators - not needed for C wrapper generation
             if (inner.name.startsWith('operator')) return null;
 
+            if (!inner.loc) {
+                throw new Error(`Failed to extract location for method '${inner.name}' in ${parent.name || 'unknown'}`);
+            }
             const method: Method & { access?: 'public' | 'protected' } = {
                 kind: 'method',
                 name: inner.name,
@@ -145,27 +155,45 @@ function extractMember(inner: any, parent: any): Member & { access?: 'public' |
                 isVirtual: inner.virtual || false,
                 isPure: inner.pure || false,
                 isConst: inner.constQualifier || false,
-                access: 'public' // Will be set correctly later
+                access: 'public', // Will be set correctly later
+                loc: {
+                    line: inner.loc.line || 0,
+                    col: inner.loc.col || 0
+                }
             };
             return method;
         }
         case 'CXXConstructorDecl': {
+            if (!inner.loc) {
+                throw new Error(`Failed to extract location for constructor '${inner.name || parent.name || 'unknown'}' in ${parent.name || 'unknown'}`);
+            }
             const constr: Constructor & { access?: 'public' | 'protected' } = {
                 kind: 'constructor',
                 name: inner.name || parent.name || '',
                 parameters: extractParameters(inner),
-                access: 'public' // Will be set correctly later
+                access: 'public', // Will be set correctly later
+                loc: {
+                    line: inner.loc.line || 0,
+                    col: inner.loc.col || 0
+                }
             };
             return constr;
         }
         case 'CXXDestructorDecl': {
             // Include destructors for completeness
+            if (!inner.loc) {
+                throw new Error(`Failed to extract location for destructor '${inner.name || `~${parent.name}`}' in ${parent.name || 'unknown'}`);
+            }
             const destructor: Destructor & { access?: 'public' | 'protected' } = {
                 kind: 'destructor',
                 name: inner.name || `~${parent.name}`,
                 isVirtual: inner.virtual || false,
                 isPure: inner.pure || false,
-                access: 'public' // Will be set correctly later
+                access: 'public', // Will be set correctly later
+                loc: {
+                    line: inner.loc.line || 0,
+                    col: inner.loc.col || 0
+                }
             };
             return destructor;
         }

+ 16 - 0
spine-c/codegen/src/types.ts

@@ -9,6 +9,10 @@ export type Field = {
     type: string;
     isStatic?: boolean;
     fromSupertype?: string;
+    loc: {
+        line: number;
+        col: number;
+    };
 }
 
 export type Method = {
@@ -21,6 +25,10 @@ export type Method = {
     isPure?: boolean;
     isConst?: boolean;
     fromSupertype?: string;
+    loc: {
+        line: number;
+        col: number;
+    };
 }
 
 export type Constructor = {
@@ -28,6 +36,10 @@ export type Constructor = {
     name: string;
     parameters?: Parameter[];
     fromSupertype?: string;
+    loc: {
+        line: number;
+        col: number;
+    };
 }
 
 export type Destructor = {
@@ -36,6 +48,10 @@ export type Destructor = {
     isVirtual?: boolean;
     isPure?: boolean;
     fromSupertype?: string;
+    loc: {
+        line: number;
+        col: number;
+    };
 };
 
 export type Member =

+ 4 - 54
spine-flutter/codegen/src/index.ts

@@ -9,58 +9,7 @@ import { DartWriter } from './dart-writer.js';
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
 async function generateFFIBindings(spineCDir: string): Promise<void> {
-    console.log('Finding all header files...');
-    const generatedDir = path.join(spineCDir, 'src/generated');
-    const headerFiles = fs.readdirSync(generatedDir)
-        .filter(f => f.endsWith('.h'))
-        .map(f => path.join('src/spine-c/src/generated', f))
-        .sort();
-
-    console.log(`Found ${headerFiles.length} header files`);
-
-    // Generate ffigen.yaml configuration
-    console.log('Generating ffigen.yaml configuration...');
-    const ffigenConfig = `# Run with \`dart run ffigen --config ffigen.yaml\`.
-name: SpineDartBindings
-description: |
-  Bindings for Spine C headers.
-
-  Regenerate bindings with \`dart run ffigen --config ffigen.yaml\`.
-output: 'lib/generated/spine_dart_bindings_generated.dart'
-llvm-path:
-    - '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/'
-headers:
-  entry-points:
-    - 'src/spine-c/include/spine-c.h'
-compiler-opts:
-  - '-Isrc/spine-c/include'
-  - '-Isrc/spine-c/src'
-  - '-Isrc/spine-c/src/generated'
-  - '-xc'
-  - '-std=c99'
-functions:
-  include:
-    - 'spine_.*'
-structs:
-  include:
-    - 'spine_.*'
-enums:
-  include:
-    - 'spine_.*'
-typedefs:
-  include:
-    - 'spine_.*'
-preamble: |
-  // ignore_for_file: always_specify_types, constant_identifier_names
-  // ignore_for_file: camel_case_types
-  // ignore_for_file: non_constant_identifier_names
-comments:
-  style: any
-  length: full
-`;
-
-    const ffigenPath = path.join(__dirname, '../../ffigen.yaml');
-    fs.writeFileSync(ffigenPath, ffigenConfig);
+    const ffigenPath = await generateFFigenYaml(spineCDir);
 
     // Run ffigen to generate bindings
     console.log('Running ffigen...');
@@ -93,7 +42,7 @@ comments:
     console.log('✅ FFI bindings generated successfully!');
 }
 
-async function generateFFigenYamlOnly(spineCDir: string): Promise<void> {
+async function generateFFigenYaml(spineCDir: string): Promise<string> {
     console.log('Finding all header files...');
     const generatedDir = path.join(spineCDir, 'src/generated');
     const headerFiles = fs.readdirSync(generatedDir)
@@ -147,6 +96,7 @@ comments:
     const ffigenPath = path.join(__dirname, '../../ffigen.yaml');
     fs.writeFileSync(ffigenPath, ffigenConfig);
     console.log(`FFigen config written to: ${ffigenPath}`);
+    return ffigenPath;
 }
 
 async function main() {
@@ -158,7 +108,7 @@ async function main() {
 
         // Generate FFI bindings YAML config only
         const spineCDir = path.join(__dirname, '../../src/spine-c');
-        await generateFFigenYamlOnly(spineCDir);
+        await generateFFigenYaml(spineCDir);
         console.log('✅ ffigen.yaml generated successfully!');
         return;
     }