# 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 _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().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 { ArrayAnimation.fromPointer(Pointer 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` ### 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` ### 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().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().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++