2025-07-29 21:39:14 +02:00

385 lines
14 KiB
Markdown

# spine-flutter Code Generator
This directory contains the TypeScript-based code generator that automatically creates idiomatic Dart wrapper classes for the spine-flutter runtime.
## Overview
The spine-flutter runtime is built on a multi-layer architecture:
1. **spine-cpp**: The core C++ implementation of the Spine runtime
2. **spine-c**: A C wrapper API around spine-cpp (generated by ../spine-c/codegen)
3. **FFI bindings**: Low-level Dart FFI bindings to spine-c (generated by Dart's ffigen)
4. **Dart wrappers**: Idiomatic, type-safe Dart classes (generated by this codegen)
This code generator creates the top-level Dart wrapper classes that provide a clean, native Dart API for Flutter developers.
## Architecture
The complete code generation pipeline:
```
spine-cpp (C++ headers)
[spine-c/codegen] → Parses C++ using Clang AST
C++ Type Information (spine-cpp-types.json)
[spine-c/codegen] → Generates C wrapper API
spine-c Core API (generated) spine-c Extensions (manual)
├─ generated/*.h/.cpp ├─ extensions.h/.cpp
└─ Auto-generated from C++ └─ Hand-written C code
↓ ↓
┌─────────────────────────────────────────────────────────────────┐
│ This Codegen (2 outputs) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Dart Wrapper Classes │
│ ↓ │
│ [dart-writer.ts] │
│ ↓ │
│ Idiomatic Dart API │
│ (lib/generated/*.dart) │
│ ✓ Auto-generated for core API │
│ │
│ 2. FFI Bindings Generation │
│ ↓ │
│ [ffigen configuration] │
│ ↓ │
│ Low-level FFI bindings │
│ (spine_dart_bindings_generated.dart) │
│ ✓ Includes both core API and extensions │
│ │
└─────────────────────────────────────────────────────────────────┘
Manual Dart Extension Wrappers
(lib/spine_dart.dart)
✓ Hand-written wrappers for extensions
```
## How It Works
### Step 1: Import C Intermediate Representation
The generator imports the `generate()` function from spine-c's codegen, which provides:
- **cTypes**: All C wrapper types (classes/structs) with nullability information
- **cEnums**: All C enum types
- **cArrayTypes**: Array specializations for different element types
- **inheritance**: Inheritance relationships (extends/implements)
- **isInterface**: Map of which types are pure interfaces
- **supertypes**: Type hierarchy for RTTI-based instantiation
The CIR includes crucial nullability information for each method:
- `returnTypeNullable`: Whether the return value can be null
- `isNullable` on parameters: Whether each parameter accepts null
### Step 2: Transform to Dart Model
The `dart-writer.ts` transforms the C IR into a clean Dart model:
- Converts C naming conventions to Dart (snake_case → PascalCase/camelCase)
- Resolves inheritance relationships (single inheritance + interfaces)
- Determines member types (constructors, methods, getters, setters)
- Handles method overloading with numbered suffixes
### Step 3: Generate Dart Wrapper Classes
For each type, the generator creates:
#### Concrete Classes
```dart
class Animation extends Updatable {
final Pointer<spine_animation_wrapper> _ptr;
Animation.fromPointer(this._ptr) : super.fromPointer(_ptr.cast());
// Native pointer for FFI calls
Pointer get nativePtr => _ptr;
// Type-safe getter
String get name {
final result = SpineBindings.bindings.spine_animation_get_name(_ptr);
return result.cast<Utf8>().toDartString();
}
// Methods with proper marshaling
void apply(Skeleton skeleton, double lastTime, double time, ...) {
SpineBindings.bindings.spine_animation_apply(
_ptr, skeleton.nativePtr.cast(), lastTime, time, ...
);
}
}
```
#### Abstract Classes and Interfaces
```dart
abstract class Constraint {
Pointer get nativePtr;
// Abstract getters/setters
ConstraintData get data;
set order(int value);
// Abstract methods
void update(Physics physics);
// Static RTTI method for type identification
static RTTI rttiStatic() {
final result = SpineBindings.bindings.spine_constraint_rtti();
return RTTI.fromPointer(result);
}
}
```
#### Array Classes
```dart
class ArrayAnimation extends NativeArray<Animation?> {
ArrayAnimation.fromPointer(Pointer<spine_array_animation_wrapper> ptr) : super(ptr);
@override
int get length {
return SpineBindings.bindings.spine_array_animation_size(nativePtr.cast());
}
@override
Animation? operator [](int index) {
final buffer = SpineBindings.bindings.spine_array_animation_buffer(nativePtr.cast());
return buffer[index].address == 0 ? null : Animation.fromPointer(buffer[index]);
}
}
```
### Step 4: Generate FFI Bindings
The generator also configures and runs Dart's `ffigen` tool to create low-level FFI bindings:
1. Generates `ffigen.yaml` configuration pointing to spine-c headers
2. Runs `dart run ffigen` to generate `spine_dart_bindings_generated.dart`
3. Replaces the dart:ffi import with a custom proxy for web compatibility
### Step 5: Format and Finalize
Finally, the generator:
1. Runs `dart fix --apply` to remove unused imports
2. Runs the project's Dart formatter
3. Generates `api.dart` to export all generated types
## Type Conversion
The generator handles complex type conversions between C and Dart:
### Primitive Types
- `int`, `float`, `bool``int`, `double`, `bool`
- `const char*``String` (with UTF-8 marshaling)
- `void*``Pointer<Void>`
### Object Types
- `spine_skeleton``Skeleton` class with pointer wrapping
- Nullable types use Dart's null safety: `Skeleton?`
- Abstract types use RTTI for concrete instantiation
### Arrays
- `spine_array_float``ArrayFloat` with indexed access
- Object arrays support null elements: `Array<Bone?>`
### Enums
- C enums → Dart enums with value mapping
- Provides `fromValue()` factory method
## Special Features
### RTTI-Based Instantiation
For abstract types, the generator creates runtime type checking:
```dart
// When returning an abstract Attachment
final rtti = SpineBindings.bindings.spine_attachment_get_rtti(result);
final className = SpineBindings.bindings.spine_rtti_get_class_name(rtti)
.cast<Utf8>().toDartString();
switch (className) {
case 'spine_region_attachment':
return RegionAttachment.fromPointer(result.cast());
case 'spine_mesh_attachment':
return MeshAttachment.fromPointer(result.cast());
// ... other concrete types
}
```
### Method Overloading
C doesn't support overloading, so the generator handles numbered methods:
```dart
// spine_color_create() → Color()
// spine_color_create2() → Color.fromRGBA()
// spine_bone_world_to_parent() → worldToParent()
// spine_bone_world_to_parent_2() → worldToParent2()
```
### Property Detection
The generator identifies getter/setter patterns:
- `spine_bone_get_x()``double get x`
- `spine_bone_set_x()``set x(double value)`
- `spine_bone_is_active()``bool get isActive`
## Generated Files
- `lib/generated/` - All generated Dart wrapper classes
- `lib/generated/arrays.dart` - All array type implementations
- `lib/generated/api.dart` - Exports all generated types
- `lib/generated/spine_dart_bindings_generated.dart` - Low-level FFI bindings
- Individual class files like `skeleton.dart`, `animation.dart`, etc.
## Extensions System
### Overview
In addition to the automatically generated wrapper API, spine-c includes hand-written extensions in `extensions.h` and `extensions.cpp`. These provide additional functionality that isn't part of the core spine-cpp API, such as:
- Data loading utilities (e.g., `spine_load_atlas`, `spine_load_skeleton_data`)
- Rendering utilities (e.g., `spine_skeleton_drawable_*` functions)
- Version information and debugging utilities
- Skin entry manipulation functions
- Platform-specific helpers
- Convenience functions for common operations
### How Extensions Work
1. **spine-c extensions**: Hand-written C functions in `src/spine-c/src/extensions.h/.cpp`
2. **FFI bindings**: FFigen automatically generates bindings for these functions
3. **Manual Dart wrappers**: Must be written manually in `lib/spine_dart.dart`
The key difference is that extensions are **not** part of the C Intermediate Representation (CIR) because they don't come from spine-cpp. Therefore:
- The spine-c codegen doesn't know about them
- This Dart codegen can't generate wrappers for them
- Developers must manually write idiomatic Dart wrappers
### Extension Wrappers in spine_dart.dart
All extension wrappers go into `spine_dart.dart` and are implemented in pure Dart without any Flutter dependencies. This ensures they work in both Flutter and headless Dart environments (as demonstrated by the test/headless_runner.dart).
Current extension wrappers:
- **Data loading**: `loadAtlas()`, `loadSkeletonData()` - already wrapped and tested
- **Version info**: `spine_major_version()`, `spine_minor_version()` - already wrapped
- **Debugging**: `report_leaks()` - already wrapped
Extension wrappers that need to be implemented:
- **Skin entries**: `spine_skin_entry_*` functions
- **Rendering**: `spine_skeleton_drawable_*` functions (these work with pure Dart data structures)
### Migration Note
The old spine-flutter had manually written wrappers for the entire API (found in `lib/extensions.dart`). With the new code generation approach:
- Core API wrappers are now generated automatically
- Only extension wrappers need to be written manually
- The old `extensions.dart` can serve as a reference but needs updates to work with the new generated classes
- All extension wrappers are in `spine_dart.dart` for pure Dart compatibility
## Development
### Setup
```bash
# Install dependencies
npm install
```
### Generate Everything
```bash
# This runs the complete generation pipeline
npm run generate
```
### Generate Only FFigen Config
```bash
# Useful for debugging ffigen issues
npx tsx src/index.ts --yaml-only
```
## Integration with Build Process
The main build script (`generate-bindings.sh`) orchestrates the complete process:
1. Installs dependencies
2. Copies spine-c/spine-cpp sources
3. Runs this codegen (which internally runs spine-c codegen)
4. Builds the test shared library
## Implementation Details
### Memory Management
- All wrapper classes hold a pointer to the underlying C object
- The `dispose()` method calls the C destructor
- No automatic memory management - users must call dispose()
### Null Safety
The generator implements comprehensive null safety based on the C++ API's nullability annotations:
#### Nullability Information Flow
1. **spine-cpp**: Uses pointer types to indicate nullability (pointers can be null, references cannot)
2. **spine-c codegen**: Analyzes C++ types and encodes nullability in the CIR:
- Methods have `returnTypeNullable: true/false`
- Parameters have `isNullable: true/false`
3. **This codegen**: Translates CIR nullability to Dart's null safety system
#### Examples
**Nullable Return Values**
```dart
// C IR: returnTypeNullable: true
Animation? get currentAnimation {
final result = SpineBindings.bindings.spine_animation_state_get_current(_ptr);
return result.address == 0 ? null : Animation.fromPointer(result);
}
```
**Nullable Parameters**
```dart
// C IR: parameter.isNullable: true
void setMixDuration(Animation? from, Animation? to, double duration) {
SpineBindings.bindings.spine_animation_state_data_set_mix_duration(
_ptr,
from?.nativePtr.cast() ?? Pointer.fromAddress(0),
to?.nativePtr.cast() ?? Pointer.fromAddress(0),
duration
);
}
```
**Non-nullable Types**
```dart
// C IR: returnTypeNullable: false
String get name { // No '?' - guaranteed non-null
final result = SpineBindings.bindings.spine_animation_get_name(_ptr);
return result.cast<Utf8>().toDartString();
}
```
This automatic nullability handling ensures type safety and prevents null pointer exceptions at the Dart level.
### Web Compatibility
- Uses `ffi_proxy.dart` instead of direct `dart:ffi`
- Allows the same API to work on web via WASM
## Troubleshooting
### Common Issues
1. **FFigen can't find headers**
- Check that spine-c has been built
- Verify paths in generated ffigen.yaml
2. **Missing types in Dart wrappers**
- Check spine-c/codegen/exclusions.txt
- Ensure the type isn't filtered as a template
3. **Type conversion errors**
- Review the C type in spine-c headers
- Check dart-writer.ts conversion logic
4. **RTTI switching fails**
- Ensure all concrete subclasses are generated
- Check that RTTI information is available in C++