mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-02-04 14:24:53 +08:00
911 lines
28 KiB
Markdown
911 lines
28 KiB
Markdown
# Dart FFI Wrapper Generator
|
|
|
|
## Overview
|
|
|
|
A TypeScript-based code generator that extends the existing `spine-c/codegen` system to automatically generate clean, type-safe Dart wrapper classes over the raw FFI bindings. This eliminates the need for manual wrapper implementation while providing a native Dart API experience.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
spine-cpp (C++ source)
|
|
↓ (existing type-extractor.ts)
|
|
spine-cpp-types.json (type definitions)
|
|
↓ (existing ir-generator.ts + c-writer.ts)
|
|
spine-c (C bindings: .h/.c files)
|
|
↓ (ffigen via generate_bindings.sh)
|
|
spine_flutter_bindings_generated.dart (raw FFI)
|
|
↓ (NEW: dart-generator.ts)
|
|
spine_flutter.dart (clean Dart API)
|
|
```
|
|
|
|
## Key Insights
|
|
|
|
### 1. Derivation vs Parsing
|
|
Instead of parsing the generated FFI bindings, we **derive** what ffigen produces using the same C type information that generates the headers. This works because ffigen follows predictable transformation rules:
|
|
|
|
### 2. Predictable Disposal Pattern
|
|
All C types follow a consistent disposal pattern (`spine_type_name` → `spine_type_name_dispose`), so we can derive disposal function names automatically instead of storing them.
|
|
|
|
### 3. Array Properties vs Regular Getters
|
|
C functions that return `spine_array_*` types become special **Array<T> properties** in Dart, requiring different generation logic than primitive getters.
|
|
|
|
### FFigen Transformation Rules
|
|
|
|
| C Code | Generated Dart FFI |
|
|
|--------|-------------------|
|
|
| `SPINE_C_API float spine_animation_get_duration(spine_animation self);` | `double spine_animation_get_duration(Pointer<spine_animation_wrapper> self)` |
|
|
| `SPINE_C_API void spine_animation_dispose(spine_animation self);` | `void spine_animation_dispose(Pointer<spine_animation_wrapper> self)` |
|
|
| `typedef enum { BLEND_NORMAL, BLEND_ADDITIVE } spine_blend_mode;` | `enum spine_blend_mode { BLEND_NORMAL(0), BLEND_ADDITIVE(1) }` |
|
|
| `SPINE_OPAQUE_TYPE(spine_animation)` | `typedef struct spine_animation_wrapper { char _dummy; } spine_animation_wrapper; typedef spine_animation_wrapper *spine_animation;` |
|
|
| `spine_array_timeline spine_animation_get_timelines(spine_animation self);` | `Pointer<spine_array_timeline_wrapper> spine_animation_get_timelines(Pointer<spine_animation_wrapper> self)` → **Array<Timeline> property** |
|
|
|
|
## Generator Components
|
|
|
|
### 1. Type Mapping System (`dart-types.ts`)
|
|
|
|
Defines the structure for Dart wrapper types:
|
|
|
|
```typescript
|
|
interface DartClass {
|
|
name: string; // "Animation"
|
|
cName: string; // "spine_animation"
|
|
nativeType: string; // "Pointer<spine_animation_wrapper>"
|
|
constructors: DartConstructor[];
|
|
methods: DartMethod[];
|
|
getters: DartGetter[];
|
|
setters: DartSetter[];
|
|
arrays: DartArrayGetter[]; // Properties that return Array<T> (e.g., animation.timelines)
|
|
}
|
|
|
|
interface DartMethod {
|
|
name: string; // "apply"
|
|
cFunctionName: string; // "spine_animation_apply"
|
|
returnType: DartType;
|
|
parameters: DartParameter[];
|
|
isStatic?: boolean;
|
|
}
|
|
|
|
interface DartGetter {
|
|
name: string; // "duration"
|
|
cFunctionName: string; // "spine_animation_get_duration"
|
|
returnType: DartType;
|
|
}
|
|
|
|
interface DartSetter {
|
|
name: string; // "duration"
|
|
cFunctionName: string; // "spine_animation_set_duration"
|
|
parameterType: DartType;
|
|
}
|
|
|
|
interface DartArrayGetter {
|
|
name: string; // "timelines" (Dart property name)
|
|
elementType: string; // "Timeline" (element type inside Array<T>)
|
|
cArrayType: string; // "spine_array_timeline" (C array type returned)
|
|
cGetterName: string; // "spine_animation_get_timelines" (C function to call)
|
|
}
|
|
|
|
interface DartConstructor {
|
|
cFunctionName: string; // "spine_animation_create"
|
|
parameters: DartParameter[];
|
|
}
|
|
|
|
interface DartParameter {
|
|
name: string;
|
|
type: DartType;
|
|
cType: string; // Original C type for conversion
|
|
}
|
|
|
|
interface DartType {
|
|
dart: string; // "double", "String", "Animation"
|
|
isNullable?: boolean;
|
|
isNative?: boolean; // true for primitives, false for wrapper classes
|
|
}
|
|
|
|
interface DartEnum {
|
|
name: string; // "BlendMode"
|
|
cName: string; // "spine_blend_mode"
|
|
values: DartEnumValue[];
|
|
}
|
|
|
|
interface DartEnumValue {
|
|
name: string; // "normal"
|
|
cName: string; // "SPINE_BLEND_MODE_NORMAL"
|
|
value: number;
|
|
}
|
|
```
|
|
|
|
### 2. Type Mapping Rules (`dart-type-mapper.ts`)
|
|
|
|
Converts C types to Dart types:
|
|
|
|
```typescript
|
|
class DartTypeMapper {
|
|
// Maps C types to Dart types
|
|
private static readonly TYPE_MAP: Record<string, DartType> = {
|
|
'float': { dart: 'double', isNative: true },
|
|
'double': { dart: 'double', isNative: true },
|
|
'int': { dart: 'int', isNative: true },
|
|
'bool': { dart: 'bool', isNative: true },
|
|
'char*': { dart: 'String', isNative: true },
|
|
'const char*': { dart: 'String', isNative: true },
|
|
'void': { dart: 'void', isNative: true },
|
|
'spine_skeleton': { dart: 'Skeleton', isNative: false },
|
|
'spine_animation': { dart: 'Animation', isNative: false },
|
|
// ... more mappings
|
|
};
|
|
|
|
static mapCTypeToDart(cType: string): DartType {
|
|
// Handle pointers
|
|
if (cType.endsWith('*')) {
|
|
const baseType = cType.slice(0, -1).trim();
|
|
const mapped = this.TYPE_MAP[baseType];
|
|
return mapped ? { ...mapped, isNullable: true } : this.mapSpineType(baseType);
|
|
}
|
|
|
|
// Handle direct mappings
|
|
return this.TYPE_MAP[cType] || this.mapSpineType(cType);
|
|
}
|
|
|
|
private static mapSpineType(cType: string): DartType {
|
|
if (cType.startsWith('spine_')) {
|
|
// spine_animation -> Animation
|
|
const dartName = toPascalCase(cType.replace('spine_', ''));
|
|
return { dart: dartName, isNative: false };
|
|
}
|
|
|
|
// Default to dynamic for unknown types
|
|
return { dart: 'dynamic', isNative: false };
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Naming Convention Utilities (`dart-naming.ts`)
|
|
|
|
Handles C to Dart naming conversions:
|
|
|
|
```typescript
|
|
export function toPascalCase(snakeCase: string): string {
|
|
return snakeCase
|
|
.split('_')
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
.join('');
|
|
}
|
|
|
|
export function toCamelCase(snakeCase: string): string {
|
|
const pascal = toPascalCase(snakeCase);
|
|
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
}
|
|
|
|
export function extractMethodName(cFunctionName: string, className: string): string {
|
|
// spine_animation_get_duration -> getDuration
|
|
// spine_animation_set_duration -> setDuration
|
|
// spine_animation_apply -> apply
|
|
|
|
const prefix = `spine_${toSnakeCase(className)}_`;
|
|
if (!cFunctionName.startsWith(prefix)) {
|
|
throw new Error(`Function ${cFunctionName} doesn't match expected prefix ${prefix}`);
|
|
}
|
|
|
|
const methodPart = cFunctionName.slice(prefix.length);
|
|
return toCamelCase(methodPart);
|
|
}
|
|
|
|
export function isGetter(methodName: string): boolean {
|
|
return methodName.startsWith('get_');
|
|
}
|
|
|
|
export function isSetter(methodName: string): boolean {
|
|
return methodName.startsWith('set_');
|
|
}
|
|
|
|
export function extractPropertyName(methodName: string): string {
|
|
// get_duration -> duration
|
|
// set_duration -> duration
|
|
if (methodName.startsWith('get_') || methodName.startsWith('set_')) {
|
|
return methodName.slice(4);
|
|
}
|
|
return methodName;
|
|
}
|
|
```
|
|
|
|
### 4. Dart Code Generator (`dart-writer.ts`)
|
|
|
|
Generates the actual Dart wrapper files:
|
|
|
|
```typescript
|
|
export class DartWriter {
|
|
constructor(private outputDir: string, private bindingsImport: string) {}
|
|
|
|
async writeClass(dartClass: DartClass): Promise<void> {
|
|
const lines: string[] = [];
|
|
|
|
// File header
|
|
lines.push("// AUTO GENERATED FILE, DO NOT EDIT.");
|
|
lines.push("// Generated by spine-c dart-wrapper generator");
|
|
lines.push("");
|
|
lines.push(`import '${this.bindingsImport}';`);
|
|
lines.push("import 'dart:ffi';");
|
|
lines.push("import 'array.dart';");
|
|
lines.push("");
|
|
|
|
// Class declaration
|
|
lines.push(`class ${dartClass.name} {`);
|
|
lines.push(` final ${dartClass.nativeType} _ptr;`);
|
|
lines.push(` final SpineFlutterBindings _bindings;`);
|
|
lines.push("");
|
|
|
|
// Private constructor
|
|
lines.push(` ${dartClass.name}._(this._ptr, this._bindings);`);
|
|
lines.push("");
|
|
|
|
// Public constructors
|
|
for (const constructor of dartClass.constructors) {
|
|
lines.push(this.writeConstructor(dartClass, constructor));
|
|
lines.push("");
|
|
}
|
|
|
|
// Getters
|
|
for (const getter of dartClass.getters) {
|
|
lines.push(this.writeGetter(getter));
|
|
}
|
|
|
|
// Setters
|
|
for (const setter of dartClass.setters) {
|
|
lines.push(this.writeSetter(setter));
|
|
}
|
|
|
|
// Array getters
|
|
for (const arrayGetter of dartClass.arrays) {
|
|
lines.push(this.writeArrayGetter(arrayGetter));
|
|
}
|
|
|
|
// Methods
|
|
for (const method of dartClass.methods) {
|
|
lines.push(this.writeMethod(method));
|
|
lines.push("");
|
|
}
|
|
|
|
// Disposal method (derived from class name)
|
|
lines.push(` void dispose() => _bindings.${dartClass.cName}_dispose(_ptr);`);
|
|
|
|
lines.push("}");
|
|
|
|
// Write to file
|
|
const fileName = `${toSnakeCase(dartClass.name)}.dart`;
|
|
const filePath = path.join(this.outputDir, fileName);
|
|
await fs.writeFile(filePath, lines.join('\n'));
|
|
}
|
|
|
|
private writeConstructor(dartClass: DartClass, constructor: DartConstructor): string {
|
|
const params = constructor.parameters.map(p =>
|
|
`${p.type.dart} ${p.name}`
|
|
).join(', ');
|
|
|
|
const args = constructor.parameters.map(p =>
|
|
this.convertDartToC(p.name, p.type, p.cType)
|
|
).join(', ');
|
|
|
|
return [
|
|
` factory ${dartClass.name}.create(SpineFlutterBindings bindings${params ? ', ' + params : ''}) {`,
|
|
` final ptr = bindings.${constructor.cFunctionName}(${args});`,
|
|
` return ${dartClass.name}._(ptr, bindings);`,
|
|
` }`
|
|
].join('\n');
|
|
}
|
|
|
|
private writeGetter(getter: DartGetter): string {
|
|
if (getter.returnType.isNative) {
|
|
return ` ${getter.returnType.dart} get ${getter.name} => _bindings.${getter.cFunctionName}(_ptr);`;
|
|
} else {
|
|
return [
|
|
` ${getter.returnType.dart} get ${getter.name} {`,
|
|
` final ptr = _bindings.${getter.cFunctionName}(_ptr);`,
|
|
` return ${getter.returnType.dart}._(ptr, _bindings);`,
|
|
` }`
|
|
].join('\n');
|
|
}
|
|
}
|
|
|
|
private writeSetter(setter: DartSetter): string {
|
|
const conversion = this.convertDartToC('value', setter.parameterType, '');
|
|
return ` set ${setter.name}(${setter.parameterType.dart} value) => _bindings.${setter.cFunctionName}(_ptr, ${conversion});`;
|
|
}
|
|
|
|
private writeArrayGetter(arrayGetter: DartArrayGetter): string {
|
|
return [
|
|
` Array<${arrayGetter.elementType}> get ${arrayGetter.name} {`,
|
|
` final array = _bindings.${arrayGetter.cGetterName}(_ptr);`,
|
|
` return Array.create<${arrayGetter.elementType}>(array, _bindings, ${arrayGetter.elementType}._);`,
|
|
` }`
|
|
].join('\n');
|
|
}
|
|
|
|
private writeMethod(method: DartMethod): string {
|
|
const params = method.parameters.map(p =>
|
|
`${p.type.dart} ${p.name}`
|
|
).join(', ');
|
|
|
|
const args = ['_ptr', ...method.parameters.map(p =>
|
|
this.convertDartToC(p.name, p.type, p.cType)
|
|
)].join(', ');
|
|
|
|
if (method.returnType.dart === 'void') {
|
|
return [
|
|
` void ${method.name}(${params}) {`,
|
|
` _bindings.${method.cFunctionName}(${args});`,
|
|
` }`
|
|
].join('\n');
|
|
} else if (method.returnType.isNative) {
|
|
return [
|
|
` ${method.returnType.dart} ${method.name}(${params}) {`,
|
|
` return _bindings.${method.cFunctionName}(${args});`,
|
|
` }`
|
|
].join('\n');
|
|
} else {
|
|
return [
|
|
` ${method.returnType.dart} ${method.name}(${params}) {`,
|
|
` final ptr = _bindings.${method.cFunctionName}(${args});`,
|
|
` return ${method.returnType.dart}._(ptr, _bindings);`,
|
|
` }`
|
|
].join('\n');
|
|
}
|
|
}
|
|
|
|
private convertDartToC(dartValue: string, dartType: DartType, cType: string): string {
|
|
if (dartType.isNative) {
|
|
// Handle string conversion
|
|
if (dartType.dart === 'String') {
|
|
return `${dartValue}.toNativeUtf8().cast<Char>()`;
|
|
}
|
|
return dartValue;
|
|
} else {
|
|
// Wrapper class - extract the pointer
|
|
return `${dartValue}._ptr`;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5. Generic Array Implementation (`dart-array-writer.ts`)
|
|
|
|
Generates a single reusable Array class:
|
|
|
|
```typescript
|
|
export class DartArrayWriter {
|
|
async writeArrayClass(outputDir: string): Promise<void> {
|
|
const content = `
|
|
// AUTO GENERATED FILE, DO NOT EDIT.
|
|
// Generated by spine-c dart-wrapper generator
|
|
|
|
import 'dart:collection';
|
|
import 'dart:ffi';
|
|
import 'spine_flutter_bindings_generated.dart';
|
|
|
|
class Array<T> extends ListBase<T> {
|
|
final Pointer _nativeArray;
|
|
final SpineFlutterBindings _bindings;
|
|
final T Function(Pointer) _elementFactory;
|
|
final String _arrayTypeName;
|
|
|
|
Array._(
|
|
this._nativeArray,
|
|
this._bindings,
|
|
this._elementFactory,
|
|
this._arrayTypeName,
|
|
);
|
|
|
|
static Array<T> create<T>(
|
|
Pointer nativeArray,
|
|
SpineFlutterBindings bindings,
|
|
T Function(Pointer) elementFactory,
|
|
) {
|
|
final typeName = T.toString().toLowerCase();
|
|
return Array._(nativeArray, bindings, elementFactory, 'spine_array_\$typeName');
|
|
}
|
|
|
|
@override
|
|
int get length {
|
|
// Use dynamic function lookup based on type
|
|
switch (_arrayTypeName) {
|
|
case 'spine_array_timeline':
|
|
return _bindings.spine_array_timeline_get_size(_nativeArray.cast());
|
|
case 'spine_array_bone_data':
|
|
return _bindings.spine_array_bone_data_get_size(_nativeArray.cast());
|
|
case 'spine_array_slot_data':
|
|
return _bindings.spine_array_slot_data_get_size(_nativeArray.cast());
|
|
// Add more cases as needed
|
|
default:
|
|
throw UnsupportedError('Unknown array type: \$_arrayTypeName');
|
|
}
|
|
}
|
|
|
|
@override
|
|
T operator [](int index) {
|
|
if (index < 0 || index >= length) {
|
|
throw RangeError.index(index, this);
|
|
}
|
|
|
|
Pointer elementPtr;
|
|
switch (_arrayTypeName) {
|
|
case 'spine_array_timeline':
|
|
elementPtr = _bindings.spine_array_timeline_get(_nativeArray.cast(), index);
|
|
break;
|
|
case 'spine_array_bone_data':
|
|
elementPtr = _bindings.spine_array_bone_data_get(_nativeArray.cast(), index);
|
|
break;
|
|
case 'spine_array_slot_data':
|
|
elementPtr = _bindings.spine_array_slot_data_get(_nativeArray.cast(), index);
|
|
break;
|
|
// Add more cases as needed
|
|
default:
|
|
throw UnsupportedError('Unknown array type: \$_arrayTypeName');
|
|
}
|
|
|
|
return _elementFactory(elementPtr);
|
|
}
|
|
|
|
@override
|
|
void operator []=(int index, T value) {
|
|
throw UnsupportedError('Array is read-only');
|
|
}
|
|
|
|
@override
|
|
set length(int newLength) {
|
|
throw UnsupportedError('Array is read-only');
|
|
}
|
|
}
|
|
`;
|
|
|
|
const filePath = path.join(outputDir, 'array.dart');
|
|
await fs.writeFile(filePath, content.trim());
|
|
}
|
|
}
|
|
```
|
|
|
|
### 6. Enum Generator (`dart-enum-writer.ts`)
|
|
|
|
Generates Dart enum wrappers:
|
|
|
|
```typescript
|
|
export class DartEnumWriter {
|
|
async writeEnum(dartEnum: DartEnum, outputDir: string): Promise<void> {
|
|
const lines: string[] = [];
|
|
|
|
// File header
|
|
lines.push("// AUTO GENERATED FILE, DO NOT EDIT.");
|
|
lines.push("// Generated by spine-c dart-wrapper generator");
|
|
lines.push("");
|
|
|
|
// Enum declaration
|
|
lines.push(`enum ${dartEnum.name} {`);
|
|
|
|
// Enum values
|
|
for (let i = 0; i < dartEnum.values.length; i++) {
|
|
const value = dartEnum.values[i];
|
|
const comma = i < dartEnum.values.length - 1 ? ',' : '';
|
|
lines.push(` ${value.name}(${value.value})${comma}`);
|
|
}
|
|
|
|
lines.push("");
|
|
lines.push(" const ${dartEnum.name}(this.value);");
|
|
lines.push(" final int value;");
|
|
lines.push("");
|
|
|
|
// From native conversion
|
|
lines.push(` static ${dartEnum.name} fromNative(int value) {`);
|
|
lines.push(" switch (value) {");
|
|
for (const value of dartEnum.values) {
|
|
lines.push(` case ${value.value}: return ${dartEnum.name}.${value.name};`);
|
|
}
|
|
lines.push(` default: throw ArgumentError('Unknown ${dartEnum.name} value: \$value');`);
|
|
lines.push(" }");
|
|
lines.push(" }");
|
|
lines.push("}");
|
|
|
|
// Write to file
|
|
const fileName = `${toSnakeCase(dartEnum.name)}.dart`;
|
|
const filePath = path.join(outputDir, fileName);
|
|
await fs.writeFile(filePath, lines.join('\n'));
|
|
}
|
|
}
|
|
```
|
|
|
|
### 7. Main Generator Orchestrator (`dart-generator.ts`)
|
|
|
|
Coordinates the entire generation process:
|
|
|
|
```typescript
|
|
import { CClassOrStruct, CEnum, CArrayType } from './c-types';
|
|
import { DartClass, DartEnum } from './dart-types';
|
|
import { DartTypeMapper } from './dart-type-mapper';
|
|
import { DartWriter } from './dart-writer';
|
|
import { DartEnumWriter } from './dart-enum-writer';
|
|
import { DartArrayWriter } from './dart-array-writer';
|
|
import { extractMethodName, isGetter, isSetter, extractPropertyName } from './dart-naming';
|
|
|
|
export class DartGenerator {
|
|
private typeMapper = new DartTypeMapper();
|
|
private writer: DartWriter;
|
|
private enumWriter = new DartEnumWriter();
|
|
private arrayWriter = new DartArrayWriter();
|
|
|
|
constructor(
|
|
private cTypes: CClassOrStruct[],
|
|
private cEnums: CEnum[],
|
|
private cArrayTypes: CArrayType[],
|
|
private outputDir: string,
|
|
private bindingsImport: string = 'spine_flutter_bindings_generated.dart'
|
|
) {
|
|
this.writer = new DartWriter(outputDir, bindingsImport);
|
|
}
|
|
|
|
async generateAll(): Promise<void> {
|
|
// Ensure output directory exists
|
|
await fs.mkdir(this.outputDir, { recursive: true });
|
|
|
|
console.log(`Generating ${this.cTypes.length} Dart wrapper classes...`);
|
|
|
|
// Generate wrapper classes
|
|
for (const cType of this.cTypes) {
|
|
const dartClass = this.convertCTypeToDartClass(cType);
|
|
await this.writer.writeClass(dartClass);
|
|
console.log(`Generated: ${dartClass.name}`);
|
|
}
|
|
|
|
console.log(`Generating ${this.cEnums.length} Dart enums...`);
|
|
|
|
// Generate enums
|
|
for (const cEnum of this.cEnums) {
|
|
const dartEnum = this.convertCEnumToDartEnum(cEnum);
|
|
await this.enumWriter.writeEnum(dartEnum, this.outputDir);
|
|
console.log(`Generated enum: ${dartEnum.name}`);
|
|
}
|
|
|
|
// Generate generic Array class
|
|
await this.arrayWriter.writeArrayClass(this.outputDir);
|
|
console.log('Generated: Array<T>');
|
|
|
|
console.log('Dart wrapper generation complete!');
|
|
}
|
|
|
|
private convertCTypeToDartClass(cType: CClassOrStruct): DartClass {
|
|
const className = toPascalCase(cType.name.replace('spine_', ''));
|
|
|
|
// Separate getters, setters, and regular methods
|
|
const getters: DartGetter[] = [];
|
|
const setters: DartSetter[] = [];
|
|
const methods: DartMethod[] = [];
|
|
const constructors: DartConstructor[] = [];
|
|
const arrays: DartArrayGetter[] = [];
|
|
|
|
for (const method of cType.methods) {
|
|
const methodName = extractMethodName(method.name, cType.name);
|
|
|
|
if (method.name.includes('_create')) {
|
|
// Constructor
|
|
constructors.push({
|
|
cFunctionName: method.name,
|
|
parameters: method.parameters.slice(0, -1).map(p => ({
|
|
name: p.name,
|
|
type: this.typeMapper.mapCTypeToDart(p.type),
|
|
cType: p.type
|
|
}))
|
|
});
|
|
} else if (isGetter(methodName)) {
|
|
// Getter
|
|
const propertyName = extractPropertyName(methodName);
|
|
const returnType = this.typeMapper.mapCTypeToDart(method.returnType);
|
|
|
|
// Check if this is an array getter
|
|
if (method.returnType.startsWith('spine_array_')) {
|
|
const elementType = this.extractArrayElementType(method.returnType);
|
|
arrays.push({
|
|
name: propertyName,
|
|
elementType,
|
|
cArrayType: method.returnType,
|
|
cGetterName: method.name
|
|
});
|
|
} else {
|
|
getters.push({
|
|
name: propertyName,
|
|
cFunctionName: method.name,
|
|
returnType
|
|
});
|
|
}
|
|
} else if (isSetter(methodName)) {
|
|
// Setter
|
|
const propertyName = extractPropertyName(methodName);
|
|
const paramType = this.typeMapper.mapCTypeToDart(method.parameters[1].type);
|
|
setters.push({
|
|
name: propertyName,
|
|
cFunctionName: method.name,
|
|
parameterType: paramType
|
|
});
|
|
} else {
|
|
// Regular method
|
|
methods.push({
|
|
name: methodName,
|
|
cFunctionName: method.name,
|
|
returnType: this.typeMapper.mapCTypeToDart(method.returnType),
|
|
parameters: method.parameters.slice(1).map(p => ({
|
|
name: p.name,
|
|
type: this.typeMapper.mapCTypeToDart(p.type),
|
|
cType: p.type
|
|
}))
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
name: className,
|
|
cName: cType.name,
|
|
nativeType: `Pointer<${cType.name}_wrapper>`,
|
|
constructors,
|
|
methods,
|
|
getters,
|
|
setters,
|
|
arrays
|
|
};
|
|
}
|
|
|
|
private convertCEnumToDartEnum(cEnum: CEnum): DartEnum {
|
|
return {
|
|
name: toPascalCase(cEnum.name.replace('spine_', '')),
|
|
cName: cEnum.name,
|
|
values: cEnum.values.map(value => ({
|
|
name: toCamelCase(value.name.replace(/^SPINE_.*?_/, '')),
|
|
cName: value.name,
|
|
value: value.value
|
|
}))
|
|
};
|
|
}
|
|
|
|
private extractArrayElementType(arrayType: string): string {
|
|
// spine_array_timeline -> Timeline
|
|
// spine_array_bone_data -> BoneData
|
|
const match = arrayType.match(/spine_array_(.+)/);
|
|
if (!match) throw new Error(`Invalid array type: ${arrayType}`);
|
|
|
|
return toPascalCase(match[1]);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 8. Integration with Existing Codegen (`index.ts` modifications)
|
|
|
|
Extend the existing main function:
|
|
|
|
```typescript
|
|
// Add to existing imports
|
|
import { DartGenerator } from './dart-generator';
|
|
|
|
// Add to existing main() function after C generation
|
|
async function main() {
|
|
// ... existing C generation code ...
|
|
|
|
// Write all C files to disk
|
|
const cWriter = new CWriter(path.join(__dirname, '../../src/generated'));
|
|
await cWriter.writeAll(cTypes, cEnums, cArrayTypes);
|
|
|
|
console.log('C code generation complete!');
|
|
|
|
// NEW: Generate Dart wrappers
|
|
const dartOutputDir = path.join(__dirname, '../../../lib/src/generated');
|
|
const dartGenerator = new DartGenerator(
|
|
cTypes,
|
|
cEnums,
|
|
cArrayTypes,
|
|
dartOutputDir
|
|
);
|
|
|
|
await dartGenerator.generateAll();
|
|
|
|
console.log('All code generation complete!');
|
|
}
|
|
```
|
|
|
|
## Generated Output Structure
|
|
|
|
After running the generator, the output structure will be:
|
|
|
|
```
|
|
lib/
|
|
├── spine_flutter.dart # Main export file
|
|
├── spine_flutter_bindings_generated.dart # Raw FFI bindings (from ffigen)
|
|
└── src/
|
|
└── generated/
|
|
├── array.dart # Generic Array<T> implementation
|
|
├── animation.dart # Animation wrapper class
|
|
├── skeleton.dart # Skeleton wrapper class
|
|
├── bone_data.dart # BoneData wrapper class
|
|
├── blend_mode.dart # BlendMode enum
|
|
└── ... # More generated classes and enums
|
|
```
|
|
|
|
## Usage Examples
|
|
|
|
### Before (Manual FFI Usage)
|
|
```dart
|
|
// Raw FFI - error-prone and verbose
|
|
final skeletonDataPtr = bindings.spine_skeleton_data_create_from_file('skeleton.json'.toNativeUtf8().cast());
|
|
final bonesArrayPtr = bindings.spine_skeleton_data_get_bones(skeletonDataPtr);
|
|
final numBones = bindings.spine_skeleton_data_get_num_bones(skeletonDataPtr);
|
|
|
|
for (int i = 0; i < numBones; i++) {
|
|
final bonePtr = bindings.spine_array_bone_data_get(bonesArrayPtr, i);
|
|
final namePtr = bindings.spine_bone_data_get_name(bonePtr);
|
|
final name = namePtr.cast<Utf8>().toDartString();
|
|
print('Bone: $name');
|
|
}
|
|
|
|
bindings.spine_skeleton_data_dispose(skeletonDataPtr);
|
|
```
|
|
|
|
### After (Generated Wrapper Usage)
|
|
```dart
|
|
// Clean, type-safe API
|
|
final skeletonData = SkeletonData.fromFile(bindings, 'skeleton.json');
|
|
final bones = skeletonData.bones; // Array<BoneData>
|
|
|
|
for (final bone in bones) { // Natural Dart iteration
|
|
print('Bone: ${bone.name}'); // Direct property access
|
|
}
|
|
|
|
skeletonData.dispose(); // Clean disposal
|
|
```
|
|
|
|
### Advanced Usage
|
|
```dart
|
|
// Complex operations made simple
|
|
final skeleton = Skeleton.create(bindings, skeletonData);
|
|
final animState = AnimationState.create(bindings, animStateData);
|
|
|
|
// Arrays work like native Dart Lists
|
|
final animations = skeleton.data.animations;
|
|
final firstAnim = animations[0];
|
|
final animCount = animations.length;
|
|
|
|
// Functional programming support
|
|
final animNames = animations.map((a) => a.name).toList();
|
|
final longAnims = animations.where((a) => a.duration > 5.0).toList();
|
|
|
|
// Enum usage
|
|
skeleton.setSkin('goblin');
|
|
animState.setAnimation(0, firstAnim, true);
|
|
animState.setMixByName('idle', 'walk', 0.2);
|
|
|
|
// Chaining operations
|
|
skeleton.data.bones
|
|
.where((bone) => bone.parent != null)
|
|
.forEach((bone) => print('Child bone: ${bone.name}'));
|
|
```
|
|
|
|
## Build Integration
|
|
|
|
### 1. Update `generate_bindings.sh`
|
|
|
|
Add Dart wrapper generation to the existing script:
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
|
|
# ... existing FFI generation code ...
|
|
|
|
# Generate Dart wrappers
|
|
echo "Generating Dart wrapper classes..."
|
|
cd src/spine-c/codegen
|
|
npm run generate-dart
|
|
|
|
echo "✅ All code generation completed!"
|
|
```
|
|
|
|
### 2. Add npm script to `spine-c/codegen/package.json`
|
|
|
|
```json
|
|
{
|
|
"scripts": {
|
|
"build": "tsc",
|
|
"generate": "npm run build && node dist/index.js",
|
|
"generate-dart": "npm run build && node dist/index.js --dart-only",
|
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Update `pubspec.yaml` dependencies
|
|
|
|
The generated code will require:
|
|
|
|
```yaml
|
|
dependencies:
|
|
flutter:
|
|
sdk: flutter
|
|
ffi: ^2.1.0
|
|
# ... existing dependencies
|
|
```
|
|
|
|
## Benefits
|
|
|
|
### 1. **Developer Experience**
|
|
- **Type Safety**: Compile-time checking prevents FFI pointer errors
|
|
- **IDE Support**: Full autocomplete, documentation, and refactoring
|
|
- **Familiar API**: Uses standard Dart patterns (getters, setters, Lists)
|
|
- **Memory Safety**: Automatic lifetime management with dispose patterns
|
|
|
|
### 2. **Performance**
|
|
- **Zero Copy**: Arrays provide lazy access to native data
|
|
- **Minimal Overhead**: Thin wrappers over FFI calls
|
|
- **Batch Operations**: Array operations can be optimized in native code
|
|
|
|
### 3. **Maintainability**
|
|
- **Automatic Updates**: Changes to C++ API automatically flow through
|
|
- **Consistent Patterns**: All classes follow the same conventions
|
|
- **Documentation**: Generated code includes comprehensive docs
|
|
- **Testing**: Type-safe API makes unit testing straightforward
|
|
|
|
### 4. **Reliability**
|
|
- **No Manual Coding**: Eliminates human error in wrapper implementation
|
|
- **Validated Generation**: Uses proven C type information
|
|
- **Comprehensive Coverage**: Every C function gets a Dart wrapper
|
|
|
|
## Implementation Timeline
|
|
|
|
### Phase 1: Core Infrastructure (1-2 weeks)
|
|
- [ ] Set up TypeScript generator project structure
|
|
- [ ] Implement basic type mapping system
|
|
- [ ] Create simple class generator for basic types
|
|
- [ ] Test with a few example classes (Animation, Skeleton)
|
|
|
|
### Phase 2: Array Support (1 week)
|
|
- [ ] Implement generic Array<T> class
|
|
- [ ] Add array detection and generation logic
|
|
- [ ] Test array functionality with timeline/bone arrays
|
|
|
|
### Phase 3: Enum Support (3-5 days)
|
|
- [ ] Add enum detection and generation
|
|
- [ ] Implement enum value mapping
|
|
- [ ] Test with BlendMode and other enums
|
|
|
|
### Phase 4: Advanced Features (1 week)
|
|
- [ ] Add constructor detection and generation
|
|
- [ ] Implement property getter/setter pairing
|
|
- [ ] Add disposal pattern detection
|
|
- [ ] Memory management best practices
|
|
|
|
### Phase 5: Integration & Polish (3-5 days)
|
|
- [ ] Integrate with existing build pipeline
|
|
- [ ] Add comprehensive documentation
|
|
- [ ] Create migration guide from manual FFI
|
|
- [ ] Performance testing and optimization
|
|
|
|
### Phase 6: Testing & Validation (1 week)
|
|
- [ ] Unit tests for all generated wrappers
|
|
- [ ] Integration tests with actual Spine files
|
|
- [ ] Performance benchmarks vs raw FFI
|
|
- [ ] Documentation and examples
|
|
|
|
**Total Estimated Time: 4-6 weeks**
|
|
|
|
## Future Enhancements
|
|
|
|
### 1. **Advanced Memory Management**
|
|
- Automatic disposal when objects go out of scope
|
|
- Reference counting for shared objects
|
|
- Memory leak detection in debug mode
|
|
|
|
### 2. **Performance Optimizations**
|
|
- Cached property access
|
|
- Batch array operations
|
|
- Native collection implementations
|
|
|
|
### 3. **Developer Tools**
|
|
- Debug visualizations for Spine objects
|
|
- Memory usage monitoring
|
|
- Performance profiling integration
|
|
|
|
### 4. **API Enhancements**
|
|
- Async/Future support for long operations
|
|
- Stream-based event handling
|
|
- Reactive programming patterns
|
|
|
|
This comprehensive generator will transform the Spine Flutter experience from low-level FFI manipulation to a clean, type-safe, and performant Dart API that feels native to the Flutter ecosystem. |