mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2025-12-21 01:36:02 +08:00
385 lines
14 KiB
Markdown
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++ |