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

14 KiB

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

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:

  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, boolint, double, bool
  • const char*String (with UTF-8 marshaling)
  • void*Pointer<Void>

Object Types

  • spine_skeletonSkeleton class with pointer wrapping
  • Nullable types use Dart's null safety: Skeleton?
  • Abstract types use RTTI for concrete instantiation

Arrays

  • spine_array_floatArrayFloat 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:

// 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 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

# 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:

  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

// 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.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++