mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2025-12-22 02:06:03 +08:00
[c] null-analysis tool.
This commit is contained in:
parent
3e622605b3
commit
736f5148f1
@ -1,6 +1,6 @@
|
|||||||
# Spine C API Code Generator
|
# 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
|
## 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)
|
9. [Array Specializations](#array-specializations)
|
||||||
10. [Generated Code Examples](#generated-code-examples)
|
10. [Generated Code Examples](#generated-code-examples)
|
||||||
11. [Implementation Details](#implementation-details)
|
11. [Implementation Details](#implementation-details)
|
||||||
12. [Troubleshooting](#troubleshooting)
|
12. [Development Tools](#development-tools)
|
||||||
|
13. [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ The code generator performs static analysis on the spine-cpp headers to automati
|
|||||||
- Array specializations for different element types
|
- Array specializations for different element types
|
||||||
- Field accessors (getters/setters) for public fields
|
- Field accessors (getters/setters) for public fields
|
||||||
- Automatic validation and conflict detection
|
- Automatic validation and conflict detection
|
||||||
|
- Inheritance analysis and interface detection for multi-language bindings
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@ -67,7 +69,13 @@ The generator follows a multi-stage pipeline:
|
|||||||
- Writes header files with C function declarations
|
- Writes header files with C function declarations
|
||||||
- Writes implementation files with C++ wrapper code
|
- Writes implementation files with C++ wrapper code
|
||||||
- Generates array specialization files
|
- 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
|
## Type System
|
||||||
|
|
||||||
@ -106,6 +114,7 @@ codegen/
|
|||||||
├── src/
|
├── src/
|
||||||
│ ├── index.ts # Main entry point and orchestration
|
│ ├── index.ts # Main entry point and orchestration
|
||||||
│ ├── type-extractor.ts # Clang AST parsing
|
│ ├── type-extractor.ts # Clang AST parsing
|
||||||
|
│ ├── cpp-check.ts # C++ nullability analysis tool
|
||||||
│ ├── types.ts # Type definitions and conversion logic
|
│ ├── types.ts # Type definitions and conversion logic
|
||||||
│ ├── c-types.ts # C IR type definitions
|
│ ├── c-types.ts # C IR type definitions
|
||||||
│ ├── array-scanner.ts # Array specialization detection
|
│ ├── array-scanner.ts # Array specialization detection
|
||||||
@ -114,18 +123,22 @@ codegen/
|
|||||||
│ ├── ir-generator.ts # C++ to C IR conversion
|
│ ├── ir-generator.ts # C++ to C IR conversion
|
||||||
│ ├── c-writer.ts # File generation
|
│ ├── c-writer.ts # File generation
|
||||||
│ └── warnings.ts # Warning collection
|
│ └── warnings.ts # Warning collection
|
||||||
|
├── dist/ # TypeScript compilation output
|
||||||
├── exclusions.txt # Type/method exclusions
|
├── exclusions.txt # Type/method exclusions
|
||||||
├── spine-cpp-types.json # Extracted type information
|
├── spine-cpp-types.json # Extracted type information
|
||||||
|
├── nullable.md # C++ nullability analysis results
|
||||||
|
├── out.json # Debug output file
|
||||||
├── package.json # Node.js configuration
|
├── package.json # Node.js configuration
|
||||||
├── tsconfig.json # TypeScript 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/`:
|
Generated files are output to `../src/generated/`:
|
||||||
- Individual files per type (e.g., `skeleton.h`, `skeleton.cpp`)
|
- Individual files per type (e.g., `skeleton.h`, `skeleton.cpp`)
|
||||||
- `types.h` - Forward declarations for all types
|
- `types.h` - Forward declarations for all types
|
||||||
- `arrays.h/cpp` - Array specializations
|
- `arrays.h/cpp` - Array specializations
|
||||||
- `spine-c.h` - Main include file
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@ -133,14 +146,36 @@ Generated files are output to `../src/generated/`:
|
|||||||
# Install dependencies
|
# Install dependencies
|
||||||
npm install
|
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/
|
# 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:
|
The generator automatically:
|
||||||
- Detects when spine-cpp headers have changed
|
- Detects when spine-cpp headers have changed
|
||||||
- Regenerates only when necessary
|
- Regenerates only when necessary
|
||||||
- Reports warnings and errors during generation
|
- 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
|
## Type Conversion Rules
|
||||||
|
|
||||||
@ -320,14 +355,14 @@ Array<PropertyId> → spine_array_property_id
|
|||||||
// Header: skeleton.h
|
// Header: skeleton.h
|
||||||
typedef struct spine_skeleton* spine_skeleton;
|
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_dispose(spine_skeleton self);
|
||||||
void spine_skeleton_update_cache(spine_skeleton self);
|
void spine_skeleton_update_cache(spine_skeleton self);
|
||||||
float spine_skeleton_get_x(const spine_skeleton self);
|
float spine_skeleton_get_x(const spine_skeleton self);
|
||||||
void spine_skeleton_set_x(spine_skeleton self, float value);
|
void spine_skeleton_set_x(spine_skeleton self, float value);
|
||||||
|
|
||||||
// Implementation: skeleton.cpp
|
// 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);
|
return (spine_skeleton) new (__FILE__, __LINE__) Skeleton((SkeletonData*)data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,46 +374,83 @@ void spine_skeleton_update_cache(spine_skeleton self) {
|
|||||||
### Enum Wrapper
|
### Enum Wrapper
|
||||||
```c
|
```c
|
||||||
// Header: blend_mode.h
|
// 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 {
|
typedef enum spine_blend_mode {
|
||||||
SPINE_BLEND_MODE_NORMAL = 0,
|
SPINE_BLEND_MODE_NORMAL = 0,
|
||||||
SPINE_BLEND_MODE_ADDITIVE = 1,
|
SPINE_BLEND_MODE_ADDITIVE,
|
||||||
SPINE_BLEND_MODE_MULTIPLY = 2,
|
SPINE_BLEND_MODE_MULTIPLY,
|
||||||
SPINE_BLEND_MODE_SCREEN = 3
|
SPINE_BLEND_MODE_SCREEN
|
||||||
} spine_blend_mode;
|
} spine_blend_mode;
|
||||||
|
|
||||||
// Implementation: blend_mode.cpp
|
#ifdef __cplusplus
|
||||||
spine_blend_mode spine_blend_mode_from_cpp(BlendMode value) {
|
|
||||||
return (spine_blend_mode)value;
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
BlendMode spine_blend_mode_to_cpp(spine_blend_mode value) {
|
#endif /* SPINE_SPINE_BLEND_MODE_H */
|
||||||
return (BlendMode)value;
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Array Specialization
|
### Array Specialization
|
||||||
|
Arrays are generated as opaque types with complete CRUD operations. All arrays are consolidated into `arrays.h` and `arrays.cpp`.
|
||||||
|
|
||||||
```c
|
```c
|
||||||
// Header: array_float.h
|
// Header: arrays.h
|
||||||
typedef struct spine_array_float* spine_array_float;
|
SPINE_OPAQUE_TYPE(spine_array_float)
|
||||||
|
|
||||||
spine_array_float spine_array_float_new(int32_t capacity);
|
// Creation functions
|
||||||
void spine_array_float_dispose(spine_array_float self);
|
spine_array_float spine_array_float_create(void);
|
||||||
int32_t spine_array_float_get_size(const spine_array_float self);
|
spine_array_float spine_array_float_create_with_capacity(size_t initialCapacity);
|
||||||
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
|
// Memory management
|
||||||
struct spine_array_float {
|
void spine_array_float_dispose(spine_array_float array);
|
||||||
Array<float> data;
|
void spine_array_float_clear(spine_array_float array);
|
||||||
};
|
|
||||||
|
|
||||||
spine_array_float spine_array_float_new(int32_t capacity) {
|
// Size and capacity operations
|
||||||
auto* arr = new (__FILE__, __LINE__) spine_array_float();
|
size_t spine_array_float_get_capacity(spine_array_float array);
|
||||||
arr->data.setCapacity(capacity);
|
size_t spine_array_float_size(spine_array_float array);
|
||||||
return arr;
|
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
|
## Implementation Details
|
||||||
|
|
||||||
### Memory Management
|
### 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 non-abstract classes
|
||||||
- Only generates constructors for classes inheriting from `SpineObject`
|
- Only generates constructors for classes inheriting from `SpineObject`
|
||||||
- Requires at least one public constructor or explicit exclusion
|
- 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
|
### Field Accessor Generation
|
||||||
- Generates getters for all non-static public fields
|
- 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`)
|
- Handles nested field access (e.g., `obj.field.x`)
|
||||||
|
|
||||||
### Method Overloading
|
### Method Overloading
|
||||||
- Constructor overloads are numbered: `_new`, `_new2`, `_new3`
|
- Constructor overloads are numbered: `_create`, `_create2`, `_create3`, etc.
|
||||||
- Other overloads must be excluded (C doesn't support overloading)
|
- 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
|
- Const/non-const conflicts are detected and reported
|
||||||
|
|
||||||
### RTTI Handling
|
### 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
|
- RTTI checks are performed in generated code where needed
|
||||||
|
|
||||||
### Warning System
|
### Warning System
|
||||||
- Collects non-fatal issues during generation
|
- Collects non-fatal issues during generation using `WarningsCollector`
|
||||||
- Reports abstract classes, missing constructors, etc.
|
- Reports abstract classes, missing constructors, etc.
|
||||||
|
- Groups warnings by pattern to avoid repetition
|
||||||
- Warnings don't stop generation but are reported at the end
|
- 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
|
## Troubleshooting
|
||||||
|
|
||||||
### Common Errors
|
### Common Errors
|
||||||
@ -438,6 +522,11 @@ spine_array_float spine_array_float_new(int32_t capacity) {
|
|||||||
- Generated function name collides with a type name
|
- Generated function name collides with a type name
|
||||||
- Solution: Rename method or exclude
|
- 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
|
### Debugging Tips
|
||||||
|
|
||||||
1. Check `spine-cpp-types.json` for extracted type information
|
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
|
3. Verify inheritance with "inherits from SpineObject" messages
|
||||||
4. Array specializations are listed with element type mapping
|
4. Array specializations are listed with element type mapping
|
||||||
5. Check warnings at the end of generation for issues
|
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
|
### 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
|
- Array scanning happens after type filtering for efficiency
|
||||||
- Validation checks run before generation to fail fast
|
- 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
|
||||||
@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "spine-c-codegen",
|
"name": "spine-c-codegen",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"generate": "tsx src/index.ts",
|
||||||
|
"null-analysis": "tsx src/null-analysis.ts"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"tsx": "^4.0.0",
|
"tsx": "^4.0.0",
|
||||||
|
|||||||
205
spine-c/codegen/src/null-analysis.ts
Normal file
205
spine-c/codegen/src/null-analysis.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -122,12 +122,19 @@ function extractMember(inner: any, parent: any): Member & { access?: 'public' |
|
|||||||
|
|
||||||
switch (inner.kind) {
|
switch (inner.kind) {
|
||||||
case 'FieldDecl': {
|
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' } = {
|
const field: Field & { access?: 'public' | 'protected' } = {
|
||||||
kind: 'field',
|
kind: 'field',
|
||||||
name: inner.name || '',
|
name: inner.name || '',
|
||||||
type: inner.type?.qualType || '',
|
type: inner.type?.qualType || '',
|
||||||
isStatic: inner.storageClass === 'static',
|
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;
|
return field;
|
||||||
}
|
}
|
||||||
@ -136,6 +143,9 @@ function extractMember(inner: any, parent: any): Member & { access?: 'public' |
|
|||||||
// Skip operators - not needed for C wrapper generation
|
// Skip operators - not needed for C wrapper generation
|
||||||
if (inner.name.startsWith('operator')) return null;
|
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' } = {
|
const method: Method & { access?: 'public' | 'protected' } = {
|
||||||
kind: 'method',
|
kind: 'method',
|
||||||
name: inner.name,
|
name: inner.name,
|
||||||
@ -145,27 +155,45 @@ function extractMember(inner: any, parent: any): Member & { access?: 'public' |
|
|||||||
isVirtual: inner.virtual || false,
|
isVirtual: inner.virtual || false,
|
||||||
isPure: inner.pure || false,
|
isPure: inner.pure || false,
|
||||||
isConst: inner.constQualifier || 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;
|
return method;
|
||||||
}
|
}
|
||||||
case 'CXXConstructorDecl': {
|
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' } = {
|
const constr: Constructor & { access?: 'public' | 'protected' } = {
|
||||||
kind: 'constructor',
|
kind: 'constructor',
|
||||||
name: inner.name || parent.name || '',
|
name: inner.name || parent.name || '',
|
||||||
parameters: extractParameters(inner),
|
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;
|
return constr;
|
||||||
}
|
}
|
||||||
case 'CXXDestructorDecl': {
|
case 'CXXDestructorDecl': {
|
||||||
// Include destructors for completeness
|
// 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' } = {
|
const destructor: Destructor & { access?: 'public' | 'protected' } = {
|
||||||
kind: 'destructor',
|
kind: 'destructor',
|
||||||
name: inner.name || `~${parent.name}`,
|
name: inner.name || `~${parent.name}`,
|
||||||
isVirtual: inner.virtual || false,
|
isVirtual: inner.virtual || false,
|
||||||
isPure: inner.pure || 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;
|
return destructor;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,10 @@ export type Field = {
|
|||||||
type: string;
|
type: string;
|
||||||
isStatic?: boolean;
|
isStatic?: boolean;
|
||||||
fromSupertype?: string;
|
fromSupertype?: string;
|
||||||
|
loc: {
|
||||||
|
line: number;
|
||||||
|
col: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Method = {
|
export type Method = {
|
||||||
@ -21,6 +25,10 @@ export type Method = {
|
|||||||
isPure?: boolean;
|
isPure?: boolean;
|
||||||
isConst?: boolean;
|
isConst?: boolean;
|
||||||
fromSupertype?: string;
|
fromSupertype?: string;
|
||||||
|
loc: {
|
||||||
|
line: number;
|
||||||
|
col: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Constructor = {
|
export type Constructor = {
|
||||||
@ -28,6 +36,10 @@ export type Constructor = {
|
|||||||
name: string;
|
name: string;
|
||||||
parameters?: Parameter[];
|
parameters?: Parameter[];
|
||||||
fromSupertype?: string;
|
fromSupertype?: string;
|
||||||
|
loc: {
|
||||||
|
line: number;
|
||||||
|
col: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Destructor = {
|
export type Destructor = {
|
||||||
@ -36,6 +48,10 @@ export type Destructor = {
|
|||||||
isVirtual?: boolean;
|
isVirtual?: boolean;
|
||||||
isPure?: boolean;
|
isPure?: boolean;
|
||||||
fromSupertype?: string;
|
fromSupertype?: string;
|
||||||
|
loc: {
|
||||||
|
line: number;
|
||||||
|
col: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Member =
|
export type Member =
|
||||||
|
|||||||
@ -9,58 +9,7 @@ import { DartWriter } from './dart-writer.js';
|
|||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
async function generateFFIBindings(spineCDir: string): Promise<void> {
|
async function generateFFIBindings(spineCDir: string): Promise<void> {
|
||||||
console.log('Finding all header files...');
|
const ffigenPath = await generateFFigenYaml(spineCDir);
|
||||||
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);
|
|
||||||
|
|
||||||
// Run ffigen to generate bindings
|
// Run ffigen to generate bindings
|
||||||
console.log('Running ffigen...');
|
console.log('Running ffigen...');
|
||||||
@ -93,7 +42,7 @@ comments:
|
|||||||
console.log('✅ FFI bindings generated successfully!');
|
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...');
|
console.log('Finding all header files...');
|
||||||
const generatedDir = path.join(spineCDir, 'src/generated');
|
const generatedDir = path.join(spineCDir, 'src/generated');
|
||||||
const headerFiles = fs.readdirSync(generatedDir)
|
const headerFiles = fs.readdirSync(generatedDir)
|
||||||
@ -147,6 +96,7 @@ comments:
|
|||||||
const ffigenPath = path.join(__dirname, '../../ffigen.yaml');
|
const ffigenPath = path.join(__dirname, '../../ffigen.yaml');
|
||||||
fs.writeFileSync(ffigenPath, ffigenConfig);
|
fs.writeFileSync(ffigenPath, ffigenConfig);
|
||||||
console.log(`FFigen config written to: ${ffigenPath}`);
|
console.log(`FFigen config written to: ${ffigenPath}`);
|
||||||
|
return ffigenPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@ -158,7 +108,7 @@ async function main() {
|
|||||||
|
|
||||||
// Generate FFI bindings YAML config only
|
// Generate FFI bindings YAML config only
|
||||||
const spineCDir = path.join(__dirname, '../../src/spine-c');
|
const spineCDir = path.join(__dirname, '../../src/spine-c');
|
||||||
await generateFFigenYamlOnly(spineCDir);
|
await generateFFigenYaml(spineCDir);
|
||||||
console.log('✅ ffigen.yaml generated successfully!');
|
console.log('✅ ffigen.yaml generated successfully!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user