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:
- spine-cpp: The core C++ implementation of the Spine runtime
- spine-c: A C wrapper API around spine-cpp (generated by ../spine-c/codegen)
- FFI bindings: Low-level Dart FFI bindings to spine-c (generated by Dart's ffigen)
- 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 nullisNullableon 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
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
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
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:
- Generates
ffigen.yamlconfiguration pointing to spine-c headers - Runs
dart run ffigento generatespine_dart_bindings_generated.dart - Replaces the dart:ffi import with a custom proxy for web compatibility
Step 5: Format and Finalize
Finally, the generator:
- Runs
dart fix --applyto remove unused imports - Runs the project's Dart formatter
- Generates
api.dartto export all generated types
Type Conversion
The generator handles complex type conversions between C and Dart:
Primitive Types
int,float,bool→int,double,boolconst char*→String(with UTF-8 marshaling)void*→Pointer<Void>
Object Types
spine_skeleton→Skeletonclass with pointer wrapping- Nullable types use Dart's null safety:
Skeleton? - Abstract types use RTTI for concrete instantiation
Arrays
spine_array_float→ArrayFloatwith 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:
// 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:
// 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 xspine_bone_set_x()→set x(double value)spine_bone_is_active()→bool get isActive
Generated Files
lib/generated/- All generated Dart wrapper classeslib/generated/arrays.dart- All array type implementationslib/generated/api.dart- Exports all generated typeslib/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
- spine-c extensions: Hand-written C functions in
src/spine-c/src/extensions.h/.cpp - FFI bindings: FFigen automatically generates bindings for these functions
- 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.dartcan serve as a reference but needs updates to work with the new generated classes - All extension wrappers are in
spine_dart.dartfor pure Dart compatibility
Development
Setup
# Install dependencies
npm install
Generate Everything
# This runs the complete generation pipeline
npm run generate
Generate Only FFigen Config
# 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:
- Installs dependencies
- Copies spine-c/spine-cpp sources
- Runs this codegen (which internally runs spine-c codegen)
- 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
- spine-cpp: Uses pointer types to indicate nullability (pointers can be null, references cannot)
- spine-c codegen: Analyzes C++ types and encodes nullability in the CIR:
- Methods have
returnTypeNullable: true/false - Parameters have
isNullable: true/false
- Methods have
- This codegen: Translates CIR nullability to Dart's null safety system
Examples
Nullable Return Values
// 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
// 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
// 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.dartinstead of directdart:ffi - Allows the same API to work on web via WASM
Troubleshooting
Common Issues
-
FFigen can't find headers
- Check that spine-c has been built
- Verify paths in generated ffigen.yaml
-
Missing types in Dart wrappers
- Check spine-c/codegen/exclusions.txt
- Ensure the type isn't filtered as a template
-
Type conversion errors
- Review the C type in spine-c headers
- Check dart-writer.ts conversion logic
-
RTTI switching fails
- Ensure all concrete subclasses are generated
- Check that RTTI information is available in C++