diff --git a/spine-c/codegen/README.md b/spine-c/codegen/README.md index 6250b03bf..fdfadfc7e 100644 --- a/spine-c/codegen/README.md +++ b/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 → 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; +// Header: arrays.h +SPINE_OPAQUE_TYPE(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); +// Creation functions +spine_array_float spine_array_float_create(void); +spine_array_float spine_array_float_create_with_capacity(size_t initialCapacity); -// Implementation: array_float.cpp -struct spine_array_float { - Array data; -}; +// Memory management +void spine_array_float_dispose(spine_array_float array); +void spine_array_float_clear(spine_array_float array); -spine_array_float spine_array_float_new(int32_t capacity) { - auto* arr = new (__FILE__, __LINE__) spine_array_float(); - arr->data.setCapacity(capacity); - return arr; +// 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(); +} + +void spine_array_float_dispose(spine_array_float array) { + delete (Array *) array; +} + +void spine_array_float_add(spine_array_float array, float inValue) { + Array *_array = (Array *) array; + _array->add(inValue); +} + +float *spine_array_float_buffer(spine_array_float array) { + Array *_array = (Array *) 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*` 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 \ No newline at end of file +- 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 \ No newline at end of file diff --git a/spine-c/codegen/package.json b/spine-c/codegen/package.json index 2500b5875..7960c01a5 100644 --- a/spine-c/codegen/package.json +++ b/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", diff --git a/spine-c/codegen/src/null-analysis.ts b/spine-c/codegen/src/null-analysis.ts new file mode 100644 index 000000000..3254ced45 --- /dev/null +++ b/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(); + 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); + } +} \ No newline at end of file diff --git a/spine-c/codegen/src/type-extractor.ts b/spine-c/codegen/src/type-extractor.ts index 1a8145b02..859ab1b70 100644 --- a/spine-c/codegen/src/type-extractor.ts +++ b/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; } diff --git a/spine-c/codegen/src/types.ts b/spine-c/codegen/src/types.ts index ac358df62..0f28d8249 100644 --- a/spine-c/codegen/src/types.ts +++ b/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 = diff --git a/spine-flutter/codegen/src/index.ts b/spine-flutter/codegen/src/index.ts index 1d459285d..ab9a6a642 100644 --- a/spine-flutter/codegen/src/index.ts +++ b/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 { - 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 { +async function generateFFigenYaml(spineCDir: string): Promise { 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; }