[c] null-analysis tool.

This commit is contained in:
Mario Zechner 2025-07-25 14:03:09 +02:00
parent 3e622605b3
commit 736f5148f1
6 changed files with 420 additions and 95 deletions

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 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;
// 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<float> 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<float>();
}
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
@ -461,3 +553,33 @@ spine_array_float spine_array_float_new(int32_t capacity) {
- Array scanning happens after type filtering for efficiency
- Validation checks run before generation to fail fast
- 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

View File

@ -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",

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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 =

View File

@ -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;
}