2025-07-30 19:10:13 +02:00

1682 lines
56 KiB
TypeScript

import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { CClassOrStruct, CEnum, CMethod, CParameter } from '../../../spine-c/codegen/src/c-types.js';
import { toSnakeCase } from '../../../spine-c/codegen/src/types.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const LICENSE_HEADER = fs.readFileSync(path.join(__dirname, '../../../spine-cpp/src/spine/Skeleton.cpp'), 'utf8')
.split('\n')
.slice(0, 28)
.map((line, index, array) => {
// Convert C++ block comment format to Dart line comment format
if (index === 0 && line.startsWith('/****')) {
return `//${line.substring(4).replace(/\*+/g, '')}`;
} else if (index === array.length - 1 && (line.startsWith(' ****') || line.trim() === '*/')) {
return `//${line.substring(line.indexOf('*') + 1).replace(/\*+/g, '').replace(/\//g, '')}`;
} else if (line.startsWith(' ****') || line.trim() === '*/') {
return `// ${line.substring(4)}`;
} else if (line.startsWith(' * ')) {
return `// ${line.substring(3)}`;
} else if (line.startsWith(' *')) {
return `//${line.substring(2)}`;
} else {
return line;
}
})
.join('\n');
// Internal data model interfaces (from spec)
interface DartClass {
name: string; // Dart class name (e.g., "Animation")
type: 'concrete' | 'abstract' | 'interface';
inheritance: {
extends?: string; // Single parent class
implements: string[]; // Multiple interfaces (replaces mixins)
};
imports: string[]; // All required imports
members: DartMember[]; // All class members
hasRtti: boolean; // Whether this class needs RTTI switching
needsPackageFfi: boolean; // Whether to import package:ffi
}
interface DartMember {
type: 'constructor' | 'method' | 'getter' | 'setter' | 'static_method';
name: string; // Dart member name
dartReturnType: string; // Dart return type
parameters: DartParameter[]; // Parameters (excluding 'self')
isOverride: boolean; // Whether to add @override
implementation: string; // The actual Dart code body
cMethodName?: string; // Original C method name (for reference)
}
interface DartParameter {
name: string;
dartType: string;
cType: string; // Original C type for conversion
}
interface DartEnum {
name: string;
values: DartEnumValue[];
}
interface DartEnumValue {
name: string;
value: number;
}
/** New Dart writer with clean architecture - following the specification exactly */
export class DartWriter {
private enumNames = new Set<string>();
private classMap = new Map<string, CClassOrStruct>(); // name -> class
private inheritance: Record<string, { extends?: string, mixins: string[] }> = {};
private isInterface: Record<string, boolean> = {};
private supertypes: Record<string, string[]> = {}; // for RTTI switching
constructor (private outputDir: string) {
this.cleanOutputDirectory();
}
private cleanOutputDirectory (): void {
if (fs.existsSync(this.outputDir)) {
fs.rmSync(this.outputDir, { recursive: true, force: true });
}
fs.mkdirSync(this.outputDir, { recursive: true });
}
// Step 1: Transform C types to clean Dart model (from spec)
private transformToDartModel (
cTypes: CClassOrStruct[],
cEnums: CEnum[],
inheritance: Record<string, { extends?: string, mixins: string[] }>,
isInterface: Record<string, boolean>,
supertypes: Record<string, string[]>
): { classes: DartClass[], enums: DartEnum[] } {
// Store data for reference
this.inheritance = inheritance;
this.isInterface = isInterface;
this.supertypes = supertypes;
for (const cType of cTypes) {
this.classMap.set(cType.name, cType);
}
for (const cEnum of cEnums) {
this.enumNames.add(cEnum.name);
}
const dartClasses: DartClass[] = [];
const dartEnums: DartEnum[] = [];
// Transform enums
for (const cEnum of cEnums) {
dartEnums.push(this.transformEnum(cEnum));
}
// Transform classes in dependency order
const sortedTypes = this.sortByInheritance(cTypes);
for (const cType of sortedTypes) {
dartClasses.push(this.transformClass(cType));
}
return { classes: dartClasses, enums: dartEnums };
}
// Step 2: Generate Dart code from clean model (from spec)
private generateDartCode (dartClass: DartClass): string {
const lines: string[] = [];
// Header (same for all)
lines.push(this.generateHeader());
// Imports (unified logic)
lines.push(...this.generateImports(dartClass.imports, dartClass.hasRtti));
// Class declaration (unified)
lines.push(this.generateClassDeclaration(dartClass));
// Class body (same template for all types)
if (dartClass.type === 'interface') {
lines.push(...this.generateInterfaceBody(dartClass));
} else {
lines.push(...this.generateClassBody(dartClass));
}
lines.push('}');
return lines.join('\n');
}
// Step 3: Write files
async writeAll (
cTypes: CClassOrStruct[],
cEnums: CEnum[],
cArrayTypes: CClassOrStruct[],
inheritance: Record<string, { extends?: string, mixins: string[] }> = {},
isInterface: Record<string, boolean> = {},
supertypes: Record<string, string[]> = {}
): Promise<void> {
// Step 1: Transform to clean model
const { classes, enums } = this.transformToDartModel(cTypes, cEnums, inheritance, isInterface, supertypes);
// Step 2 & 3: Generate and write files
for (const dartEnum of enums) {
const content = this.generateEnumCode(dartEnum);
const fileName = `${toSnakeCase(dartEnum.name)}.dart`;
const filePath = path.join(this.outputDir, fileName);
fs.writeFileSync(filePath, content);
}
for (const dartClass of classes) {
const content = this.generateDartCode(dartClass);
const fileName = `${toSnakeCase(dartClass.name)}.dart`;
const filePath = path.join(this.outputDir, fileName);
fs.writeFileSync(filePath, content);
}
// Generate arrays.dart (crucial - this was missing!)
await this.writeArraysFile(cArrayTypes);
// Generate web init file with all opaque types
await this.writeWebInitFile(cTypes, cArrayTypes);
// Write main export file
await this.writeExportFile(classes, enums);
}
// Class type resolution (from spec)
private determineClassType (cType: CClassOrStruct): 'concrete' | 'abstract' | 'interface' {
if (this.isInterface[cType.name]) return 'interface';
if (cType.cppType.isAbstract) return 'abstract';
return 'concrete';
}
// Inheritance resolution - Use implements instead of mixins (from spec)
private resolveInheritance (cType: CClassOrStruct): { extends?: string, implements: string[] } {
const inheritanceInfo = this.inheritance[cType.name];
return {
extends: inheritanceInfo?.extends ? this.toDartTypeName(inheritanceInfo.extends) : undefined,
implements: (inheritanceInfo?.mixins || []).map(mixin => this.toDartTypeName(mixin)) // Convert mixins to implements
};
}
private transformEnum (cEnum: CEnum): DartEnum {
return {
name: this.toDartTypeName(cEnum.name),
values: cEnum.values.map((value, index) => ({
name: this.toDartEnumValueName(value.name, cEnum.name),
// C enums without explicit values are implicitly numbered 0, 1, 2, etc.
value: value.value !== undefined ? Number.parseInt(value.value) : index
}))
};
}
private transformClass (cType: CClassOrStruct): DartClass {
const dartName = this.toDartTypeName(cType.name);
const classType = this.determineClassType(cType);
const inheritance = this.resolveInheritance(cType);
return {
name: dartName,
type: classType,
inheritance,
imports: this.collectImports(cType),
members: this.processMembers(cType, classType),
hasRtti: this.hasRttiMethod(cType),
needsPackageFfi: this.needsStringConversions(cType)
};
}
// Unified Method Processing (from spec)
private processMembers (cType: CClassOrStruct, classType: 'concrete' | 'abstract' | 'interface'): DartMember[] {
const members: DartMember[] = [];
// Add constructors for concrete classes only
if (classType === 'concrete') {
for (const constr of cType.constructors) {
members.push(this.createConstructor(constr, cType));
}
}
// Add destructor as dispose method for concrete classes
if (classType === 'concrete' && cType.destructor) {
members.push(this.createDisposeMethod(cType.destructor));
}
// Process methods with unified logic - Apply SAME logic for ALL class types
const validMethods = cType.methods.filter(method => {
if (this.hasRawPointerParameters(method)) {
return false;
}
if (this.isMethodInherited(method, cType)) {
return false;
}
return true;
});
const renumberedMethods = this.renumberMethods(validMethods, cType.name);
// Create a map of overloaded setter methods for isSetter to check
const overloadedSetters = this.findOverloadedSetters(renumberedMethods);
for (const methodInfo of renumberedMethods) {
const { method, renamedMethod } = methodInfo;
members.push(this.processMethod(method, cType, classType, renamedMethod, overloadedSetters));
}
return members;
}
private processMethod (method: CMethod, cType: CClassOrStruct, classType: 'concrete' | 'abstract' | 'interface', renamedMethod?: string, overloadedSetters?: Set<string>): DartMember {
// Apply SAME logic for ALL class types (concrete, abstract, interface) - from spec
if (this.isGetter(method)) {
return this.createGetter(method, cType, classType, renamedMethod);
} else if (this.isSetter(method, overloadedSetters)) {
return this.createSetter(method, cType, classType, renamedMethod);
} else {
return this.createMethod(method, cType, classType, renamedMethod);
}
}
// Unified getter detection for ALL classes (from spec)
private isGetter (method: CMethod): boolean {
return (method.name.includes('_get_') && method.parameters.length === 1) ||
(method.returnType === 'bool' && method.parameters.length === 1 &&
(method.name.includes('_has_') || method.name.includes('_is_') || method.name.includes('_was_')));
}
private isSetter (method: CMethod, overloadedSetters?: Set<string>): boolean {
const isBasicSetter = method.returnType === 'void' &&
method.parameters.length === 2 &&
method.name.includes('_set_');
if (!isBasicSetter) return false;
// If this setter has overloads (multiple methods with same base name),
// don't generate it as a setter - generate as regular method instead
if (overloadedSetters?.has(method.name)) {
return false;
}
return true;
}
private findOverloadedSetters (renumberedMethods: Array<{ method: CMethod, renamedMethod?: string }>): Set<string> {
const setterBasenames = new Map<string, string[]>();
// Group setter methods by their base name
for (const methodInfo of renumberedMethods) {
const method = methodInfo.method;
if (method.returnType === 'void' &&
method.parameters.length === 2 &&
method.name.includes('_set_')) {
// Extract base name by removing numbered suffix
const match = method.name.match(/^(.+?)_(\d+)$/);
const baseName = match ? match[1] : method.name;
if (!setterBasenames.has(baseName)) {
setterBasenames.set(baseName, []);
}
setterBasenames.get(baseName)?.push(method.name);
}
}
// Find setters that have multiple methods with the same base name
const overloadedSetters = new Set<string>();
for (const [_baseName, methodNames] of setterBasenames) {
if (methodNames.length > 1) {
// Multiple setters with same base name - mark all as overloaded
for (const methodName of methodNames) {
overloadedSetters.add(methodName);
}
}
}
return overloadedSetters;
}
private createDisposeMethod (destructor: CMethod): DartMember {
const implementation = `SpineBindings.bindings.${destructor.name}(_ptr);`;
return {
type: 'method',
name: 'dispose',
dartReturnType: 'void',
parameters: [],
isOverride: false,
implementation,
cMethodName: destructor.name
};
}
private createConstructor (constr: CMethod, cType: CClassOrStruct): DartMember {
const dartClassName = this.toDartTypeName(cType.name);
const params = constr.parameters.map(p => ({
name: p.name,
dartType: this.toDartParameterType(p),
cType: p.cType
}));
const args = constr.parameters.map(p => this.convertDartToC(p.name, p)).join(', ');
const implementation = `final ptr = SpineBindings.bindings.${constr.name}(${args});
return ${dartClassName}.fromPointer(ptr);`;
return {
type: 'constructor',
name: this.getConstructorName(constr, cType),
dartReturnType: dartClassName,
parameters: params,
isOverride: false,
implementation,
cMethodName: constr.name
};
}
private createGetter (method: CMethod, cType: CClassOrStruct, classType: 'concrete' | 'abstract' | 'interface', renamedMethod?: string): DartMember {
const propertyName = renamedMethod || this.extractPropertyName(method.name, cType.name);
const dartReturnType = this.toDartReturnType(method.returnType, method.returnTypeNullable);
// Interface methods have no implementation (from spec)
let implementation = '';
if (classType !== 'interface') {
implementation = `final result = SpineBindings.bindings.${method.name}(_ptr);
${this.generateReturnConversion(method.returnType, 'result', method.returnTypeNullable)}`;
}
// Check if this is an override
const isOverride = this.isMethodOverride(method, cType);
return {
type: 'getter',
name: propertyName,
dartReturnType,
parameters: [],
isOverride,
implementation,
cMethodName: method.name
};
}
private createSetter (method: CMethod, cType: CClassOrStruct, classType: 'concrete' | 'abstract' | 'interface', renamedMethod?: string): DartMember {
let propertyName = renamedMethod || this.extractPropertyName(method.name, cType.name);
const param = method.parameters[1]; // First param is self
const dartParam = {
name: 'value',
dartType: this.toDartParameterType(param),
cType: param.cType
};
// Handle numeric suffixes in setter names (only when no renamed method provided)
if (!renamedMethod) {
const match = propertyName.match(/^(\w+)_(\d+)$/);
if (match) {
propertyName = `${match[1]}${match[2]}`;
} else if (/^\d+$/.test(propertyName)) {
propertyName = `set${propertyName}`;
}
}
// Interface methods have no implementation (from spec)
let implementation = '';
if (classType !== 'interface') {
implementation = `SpineBindings.bindings.${method.name}(_ptr, ${this.convertDartToC('value', param)});`;
}
const isOverride = this.isMethodOverride(method, cType);
return {
type: 'setter',
name: propertyName,
dartReturnType: 'void',
parameters: [dartParam],
isOverride,
implementation,
cMethodName: method.name
};
}
private createMethod (method: CMethod, cType: CClassOrStruct, classType: 'concrete' | 'abstract' | 'interface', renamedMethod?: string): DartMember {
let methodName = renamedMethod || this.toDartMethodName(method.name, cType.name);
const dartReturnType = this.toDartReturnType(method.returnType, method.returnTypeNullable);
// Check if this is a static method
const isStatic = method.parameters.length === 0 ||
(method.parameters[0].name !== 'self' &&
!method.parameters[0].cType.startsWith(cType.name));
// Rename static rtti method to avoid conflict with getter
if (isStatic && methodName === 'rtti') {
methodName = 'rttiStatic';
}
// Parameters (skip 'self' parameter for instance methods)
const paramStartIndex = isStatic ? 0 : 1;
const params = method.parameters.slice(paramStartIndex).map(p => ({
name: p.name,
dartType: this.toDartParameterType(p),
cType: p.cType
}));
// Interface methods have no implementation (from spec)
// Exception: rttiStatic() needs implementation even in interfaces
let implementation = '';
if (classType !== 'interface' || methodName === 'rttiStatic') {
const args = method.parameters.map((p, i) => {
if (!isStatic && i === 0) return '_ptr'; // self parameter
return this.convertDartToC(p.name, p);
}).join(', ');
if (method.returnType === 'void') {
implementation = `SpineBindings.bindings.${method.name}(${args});`;
} else {
implementation = `final result = SpineBindings.bindings.${method.name}(${args});
${this.generateReturnConversion(method.returnType, 'result', method.returnTypeNullable)}`;
}
}
const isOverride = this.isMethodOverride(method, cType);
return {
type: isStatic ? 'static_method' : 'method',
name: methodName,
dartReturnType,
parameters: params,
isOverride,
implementation,
cMethodName: method.name
};
}
// Code generation methods (from spec)
private generateHeader (): string {
return `${LICENSE_HEADER}
// AUTO GENERATED FILE, DO NOT EDIT.`;
}
private generateImports (imports: string[], hasRtti: boolean): string[] {
const lines: string[] = [];
lines.push('');
lines.push("import '../ffi_proxy.dart';");
lines.push("import 'spine_dart_bindings_generated.dart';");
lines.push("import '../spine_bindings.dart';");
if (hasRtti) {
lines.push("import 'rtti.dart';");
}
// Add other imports
for (const importFile of imports.sort()) {
if (!['rtti.dart'].includes(importFile)) { // Skip duplicates
lines.push(`import '${importFile}';`);
}
}
return lines;
}
// Class declaration generation (from spec)
private generateClassDeclaration (dartClass: DartClass): string {
let declaration = '';
if (dartClass.type === 'interface') {
declaration = `abstract class ${dartClass.name}`;
} else {
declaration = `class ${dartClass.name}`;
if (dartClass.type === 'abstract') {
declaration = `abstract ${declaration}`;
}
}
// Inheritance
if (dartClass.inheritance.extends) {
declaration += ` extends ${dartClass.inheritance.extends}`;
}
// Implements clause
const implementsClasses: string[] = [];
// Add interfaces
implementsClasses.push(...dartClass.inheritance.implements);
if (implementsClasses.length > 0) {
declaration += ` implements ${implementsClasses.join(', ')}`;
}
return `
/// ${dartClass.name} wrapper
${declaration} {`;
}
private generateInterfaceBody (dartClass: DartClass): string[] {
const lines: string[] = [];
// Add nativePtr getter for interfaces that can be used as method parameters
// This allows the generated code to call .nativePtr.cast() on interface instances
lines.push(' Pointer get nativePtr;');
// Generate abstract method signatures for interfaces
for (const member of dartClass.members) {
lines.push(this.generateInterfaceMember(member));
}
return lines;
}
// Class body generation (from spec)
private generateClassBody (dartClass: DartClass): string[] {
const lines: string[] = [];
// Pointer field (only for concrete/abstract classes)
const cTypeName = this.toCTypeName(dartClass.name);
lines.push(` final Pointer<${cTypeName}_wrapper> _ptr;`);
lines.push('');
// Constructor
lines.push(this.generatePointerConstructor(dartClass));
lines.push('');
// Native pointer getter
lines.push(' /// Get the native pointer for FFI calls');
lines.push(' Pointer get nativePtr => _ptr;');
lines.push('');
// Members
for (const member of dartClass.members) {
lines.push(this.generateMember(member));
lines.push('');
}
return lines;
}
private generatePointerConstructor (dartClass: DartClass): string {
if (dartClass.inheritance.extends) {
return ` ${dartClass.name}.fromPointer(this._ptr) : super.fromPointer(_ptr.cast());`;
} else {
return ` ${dartClass.name}.fromPointer(this._ptr);`;
}
}
private generateInterfaceMember (member: DartMember): string {
const params = member.parameters.map(p => `${p.dartType} ${p.name}`).join(', ');
switch (member.type) {
case 'getter':
return ` ${member.dartReturnType} get ${member.name};`;
case 'setter':
return ` set ${member.name}(${params});`;
case 'method':
return ` ${member.dartReturnType} ${member.name}(${params});`;
case 'static_method':
// Special case: rttiStatic() needs implementation even in abstract classes
if (member.name === 'rttiStatic') {
return ` static ${member.dartReturnType} ${member.name}(${params}) {
${member.implementation}
}`;
} else {
return ` static ${member.dartReturnType} ${member.name}(${params});`;
}
default:
return '';
}
}
// Member generation (from spec)
private generateMember (member: DartMember): string {
const override = member.isOverride ? '@override\n ' : ' ';
switch (member.type) {
case 'constructor':
return this.generateConstructorMember(member);
case 'getter':
return `${override}${member.dartReturnType} get ${member.name} {
${member.implementation}
}`;
case 'setter': {
const param = member.parameters[0];
return `${override}set ${member.name}(${param.dartType} ${param.name}) {
${member.implementation}
}`;
}
case 'method':
case 'static_method': {
const params = member.parameters.map(p => `${p.dartType} ${p.name}`).join(', ');
const static_ = member.type === 'static_method' ? 'static ' : '';
return `${override}${static_}${member.dartReturnType} ${member.name}(${params}) {
${member.implementation}
}`;
}
default:
return '';
}
}
private generateConstructorMember (member: DartMember): string {
const params = member.parameters.map(p => `${p.dartType} ${p.name}`).join(', ');
const factoryName = member.name === member.dartReturnType ? '' : `.${member.name}`;
return ` factory ${member.dartReturnType}${factoryName}(${params}) {
${member.implementation}
}`;
}
private generateEnumCode (dartEnum: DartEnum): string {
const lines: string[] = [];
lines.push(this.generateHeader());
lines.push('');
lines.push(`/// ${dartEnum.name} enum`);
lines.push(`enum ${dartEnum.name} {`);
// Write 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('');
lines.push(` static ${dartEnum.name} fromValue(int value) {`);
lines.push(' return values.firstWhere(');
lines.push(' (e) => e.value == value,');
lines.push(` orElse: () => throw ArgumentError('Invalid ${dartEnum.name} value: \$value'),`);
lines.push(' );');
lines.push(' }');
lines.push('}');
return lines.join('\n');
}
// Generate arrays.dart file (this was missing!)
private async writeArraysFile (cArrayTypes: CClassOrStruct[]): Promise<void> {
const lines: string[] = [];
lines.push(this.generateHeader());
lines.push('');
lines.push("import '../ffi_proxy.dart';");
lines.push("import 'spine_dart_bindings_generated.dart';");
lines.push("import '../spine_bindings.dart';");
lines.push("import '../native_array.dart';");
// Collect all imports needed for all array types
const imports = new Set<string>();
for (const arrayType of cArrayTypes) {
const elementType = this.extractArrayElementType(arrayType.name);
if (!this.isPrimitiveArrayType(elementType)) {
imports.add(`import '${toSnakeCase(elementType)}.dart';`);
// If this element type is abstract, we need to import all its concrete subclasses too
const cElementType = `spine_${toSnakeCase(elementType)}`;
const cClass = this.classMap.get(cElementType);
if (cClass && this.isAbstract(cClass)) {
const concreteSubclasses = this.getConcreteSubclasses(cElementType);
for (const subclass of concreteSubclasses) {
const dartSubclass = this.toDartTypeName(subclass);
imports.add(`import '${toSnakeCase(dartSubclass)}.dart';`);
}
}
}
}
// Add RTTI import if needed
if (Array.from(imports).some(imp => {
const arrayType = cArrayTypes.find(at => imp.includes(toSnakeCase(this.extractArrayElementType(at.name))));
if (arrayType) {
const elementType = this.extractArrayElementType(arrayType.name);
const cElementType = `spine_${toSnakeCase(elementType)}`;
const cClass = this.classMap.get(cElementType);
return cClass && this.isAbstract(cClass);
}
return false;
})) {
lines.push("import 'rtti.dart';");
}
// Add sorted imports
for (const imp of Array.from(imports).sort()) {
lines.push(imp);
}
// Generate all array classes in one file (from spec)
for (const arrayType of cArrayTypes) {
lines.push('');
lines.push(...this.generateArrayClassLines(arrayType));
}
const filePath = path.join(this.outputDir, 'arrays.dart');
fs.writeFileSync(filePath, lines.join('\n'));
}
// Array generation (proper implementation from old writer)
private generateArrayClassLines (arrayType: CClassOrStruct): string[] {
const lines: string[] = [];
const dartClassName = this.toDartTypeName(arrayType.name);
const elementType = this.extractArrayElementType(arrayType.name);
lines.push(`/// ${dartClassName} wrapper`);
lines.push(`class ${dartClassName} extends NativeArray<${this.toDartElementType(elementType)}> {`);
lines.push(' final bool _ownsMemory;');
lines.push('');
// Generate typed constructor - arrays use the array wrapper type
const arrayWrapperType = `${arrayType.name}_wrapper`;
lines.push(` ${dartClassName}.fromPointer(Pointer<${arrayWrapperType}> ptr, {bool ownsMemory = false}) : _ownsMemory = ownsMemory, super(ptr);`);
lines.push('');
// Find create methods for constructors
const createMethod = arrayType.constructors?.find(m => m.name === `${arrayType.name}_create`);
const createWithCapacityMethod = arrayType.constructors?.find(m => m.name === `${arrayType.name}_create_with_capacity`);
// Add default constructor
if (createMethod) {
lines.push(' /// Create a new empty array');
lines.push(` factory ${dartClassName}() {`);
lines.push(` final ptr = SpineBindings.bindings.${createMethod.name}();`);
lines.push(` return ${dartClassName}.fromPointer(ptr.cast(), ownsMemory: true);`);
lines.push(' }');
lines.push('');
}
// Add constructor with initial capacity
if (createWithCapacityMethod) {
lines.push(' /// Create a new array with the specified initial capacity');
lines.push(` factory ${dartClassName}.withCapacity(int initialCapacity) {`);
lines.push(` final ptr = SpineBindings.bindings.${createWithCapacityMethod.name}(initialCapacity);`);
lines.push(` return ${dartClassName}.fromPointer(ptr.cast(), ownsMemory: true);`);
lines.push(' }');
lines.push('');
}
// Find size and buffer methods
const sizeMethod = arrayType.methods.find(m => m.name.endsWith('_size') && !m.name.endsWith('_set_size'));
const bufferMethod = arrayType.methods.find(m => m.name.endsWith('_buffer'));
const setMethod = arrayType.methods.find(m => m.name.endsWith('_set') && m.parameters.length === 3); // self, index, value
const setSizeMethod = arrayType.methods.find(m => m.name.endsWith('_set_size'));
const addMethod = arrayType.methods.find(m => m.name.endsWith('_add') && !m.name.endsWith('_add_all'));
const clearMethod = arrayType.methods.find(m => m.name.endsWith('_clear') && !m.name.endsWith('_clear_and_add_all'));
const removeAtMethod = arrayType.methods.find(m => m.name.endsWith('_remove_at'));
const ensureCapacityMethod = arrayType.methods.find(m => m.name.endsWith('_ensure_capacity'));
if (sizeMethod) {
lines.push(' @override');
lines.push(' int get length {');
lines.push(` return SpineBindings.bindings.${sizeMethod.name}(nativePtr.cast());`);
lines.push(' }');
lines.push('');
}
if (bufferMethod) {
lines.push(' @override');
lines.push(` ${this.toDartElementType(elementType)} operator [](int index) {`);
lines.push(' if (index < 0 || index >= length) {');
lines.push(' throw RangeError.index(index, this, \'index\');');
lines.push(' }');
lines.push(` final buffer = SpineBindings.bindings.${bufferMethod.name}(nativePtr.cast());`);
// Handle different element types
if (elementType === 'int') {
lines.push(' return buffer.cast<Int32>()[index];');
} else if (elementType === 'float') {
lines.push(' return buffer.cast<Float>()[index];');
} else if (elementType === 'bool') {
lines.push(' return buffer.cast<Int32>()[index] != 0;');
} else if (elementType === 'unsigned_short') {
lines.push(' return buffer.cast<Uint16>()[index];');
} else if (elementType === 'property_id') {
// PropertyId buffer returns int instead of Pointer<Int64> due to C codegen bug
lines.push(' // NOTE: This will not compile due to C API bug - buffer() returns int instead of Pointer');
lines.push(' return buffer.cast<Int64>()[index];');
} else {
// For object types, the buffer contains pointers
const dartElementType = this.toDartTypeName(`spine_${toSnakeCase(elementType)}`);
const cElementType = `spine_${toSnakeCase(elementType)}`;
const cClass = this.classMap.get(cElementType);
if (cClass && this.isAbstract(cClass)) {
// Use RTTI to determine concrete type for abstract classes - handle null case
lines.push(` if (buffer[index].address == 0) return null;`);
const rttiCode = this.generateRttiBasedInstantiation(dartElementType, 'buffer[index]', cClass);
lines.push(` ${rttiCode}`);
} else {
// For array elements, check if the pointer is null
lines.push(` return buffer[index].address == 0 ? null : ${dartElementType}.fromPointer(buffer[index]);`);
}
}
lines.push(' }');
lines.push('');
}
// Override []= if there's a set method
if (setMethod) {
lines.push(' @override');
lines.push(` void operator []=(int index, ${this.toDartElementType(elementType)} value) {`);
lines.push(' if (index < 0 || index >= length) {');
lines.push(' throw RangeError.index(index, this, \'index\');');
lines.push(' }');
// Convert value to C type
const param = setMethod.parameters[2]; // The value parameter
// Create a copy of the parameter with nullable flag for proper conversion
const nullableParam = { ...param, isNullable: !this.isPrimitiveArrayType(elementType) };
const convertedValue = this.convertDartToC('value', nullableParam);
lines.push(` SpineBindings.bindings.${setMethod.name}(nativePtr.cast(), index, ${convertedValue});`);
lines.push(' }');
lines.push('');
}
// Override set length if there's a set_size method
if (setSizeMethod) {
lines.push(' @override');
lines.push(' set length(int newLength) {');
// For primitive types, set_size takes a default value
if (this.isPrimitiveArrayType(elementType)) {
let defaultValue = '0';
if (elementType === 'float') defaultValue = '0.0';
else if (elementType === 'bool') defaultValue = 'false';
lines.push(` SpineBindings.bindings.${setSizeMethod.name}(nativePtr.cast(), newLength, ${defaultValue});`);
} else {
// For object types, set_size takes null
lines.push(` SpineBindings.bindings.${setSizeMethod.name}(nativePtr.cast(), newLength, Pointer.fromAddress(0));`);
}
lines.push(' }');
lines.push('');
}
// Add method if available
if (addMethod) {
lines.push(' /// Adds a value to the end of this array.');
lines.push(` void add(${this.toDartElementType(elementType)} value) {`);
// Convert value to C type
const param = addMethod.parameters[1]; // The value parameter
const nullableParam = { ...param, isNullable: !this.isPrimitiveArrayType(elementType) };
const convertedValue = this.convertDartToC('value', nullableParam);
lines.push(` SpineBindings.bindings.${addMethod.name}(nativePtr.cast(), ${convertedValue});`);
lines.push(' }');
lines.push('');
}
// Clear method if available
if (clearMethod) {
lines.push(' /// Removes all elements from this array.');
lines.push(' @override');
lines.push(' void clear() {');
lines.push(` SpineBindings.bindings.${clearMethod.name}(nativePtr.cast());`);
lines.push(' }');
lines.push('');
}
// RemoveAt method if available
if (removeAtMethod) {
lines.push(' /// Removes the element at the given index.');
lines.push(' @override');
lines.push(` ${this.toDartElementType(elementType)} removeAt(int index) {`);
lines.push(' if (index < 0 || index >= length) {');
lines.push(' throw RangeError.index(index, this, \'index\');');
lines.push(' }');
lines.push(` final value = this[index];`);
lines.push(` SpineBindings.bindings.${removeAtMethod.name}(nativePtr.cast(), index);`);
lines.push(' return value;');
lines.push(' }');
lines.push('');
}
// EnsureCapacity method if available
if (ensureCapacityMethod) {
lines.push(' /// Ensures this array has at least the given capacity.');
lines.push(' void ensureCapacity(int capacity) {');
lines.push(` SpineBindings.bindings.${ensureCapacityMethod.name}(nativePtr.cast(), capacity);`);
lines.push(' }');
lines.push('');
}
// Find dispose method for arrays - check in destructor
if (arrayType.destructor) {
lines.push(' /// Dispose of the native array');
lines.push(' /// Throws an error if the array was not created by this class (i.e., it was obtained from C)');
lines.push(' void dispose() {');
lines.push(' if (!_ownsMemory) {');
lines.push(` throw StateError('Cannot dispose ${dartClassName} that was created from C. Only arrays created via factory constructors can be disposed.');`);
lines.push(' }');
lines.push(` SpineBindings.bindings.${arrayType.destructor.name}(nativePtr.cast());`);
lines.push(' }');
}
lines.push('}');
return lines;
}
private extractArrayElementType (arrayTypeName: string): string {
// spine_array_animation -> animation
// spine_array_int -> int
const match = arrayTypeName.match(/spine_array_(.+)/);
if (match) {
const rawType = match[1];
// For primitive types, return the raw type
if (['int', 'float', 'bool', 'unsigned_short', 'property_id'].includes(rawType)) {
return rawType;
}
// For object types, return the raw type (will be converted later)
return rawType;
}
return 'dynamic';
}
private toDartElementType (elementType: string): string {
// Handle pointer types
if (elementType.endsWith('*')) {
const baseType = elementType.slice(0, -1).trim();
return `${this.toDartTypeName(`spine_${toSnakeCase(baseType)}`)}?`;
}
// For primitive types, return the Dart type directly
if (elementType === 'int' || elementType === 'int32_t' || elementType === 'uint32_t' || elementType === 'size_t') {
return 'int';
}
if (elementType === 'unsigned_short') {
return 'int'; // Dart doesn't have unsigned short, use int
}
if (elementType === 'property_id' || elementType === 'int64_t') {
return 'int'; // PropertyId is int64_t which maps to int in Dart
}
if (elementType === 'float' || elementType === 'double') {
return 'double';
}
if (elementType === 'bool') {
return 'bool';
}
// For object types, convert to PascalCase and make nullable since arrays can contain null pointers
return `${this.toPascalCase(elementType)}?`;
}
private isPrimitiveArrayType (elementType: string): boolean {
return ['int', 'float', 'bool', 'unsigned_short', 'property_id'].includes(elementType.toLowerCase());
}
// Helper methods
private sortByInheritance (cTypes: CClassOrStruct[]): CClassOrStruct[] {
const sorted: CClassOrStruct[] = [];
const processed = new Set<string>();
const processClass = (cType: CClassOrStruct) => {
if (processed.has(cType.name)) {
return;
}
// Process concrete parent first (skip interfaces)
const inheritanceInfo = this.inheritance[cType.name];
if (inheritanceInfo?.extends) {
const parent = this.classMap.get(inheritanceInfo.extends);
if (parent) {
processClass(parent);
}
}
// Process interface dependencies
for (const interfaceName of inheritanceInfo?.mixins || []) {
const interfaceClass = this.classMap.get(interfaceName);
if (interfaceClass) {
processClass(interfaceClass);
}
}
sorted.push(cType);
processed.add(cType.name);
};
for (const cType of cTypes) {
processClass(cType);
}
return sorted;
}
private hasRttiMethod (cType: CClassOrStruct): boolean {
return cType.methods.some(m => m.name === `${cType.name}_rtti` && m.parameters.length === 0);
}
private collectImports (cType: CClassOrStruct): string[] {
const imports = new Set<string>();
const currentTypeName = this.toDartTypeName(cType.name);
const currentFileName = `${toSnakeCase(currentTypeName)}.dart`;
// Add parent class import if needed
const parentName = this.inheritance[cType.name]?.extends;
if (parentName) {
const parentDartName = this.toDartTypeName(parentName);
imports.add(`${toSnakeCase(parentDartName)}.dart`);
}
// Add interface imports
for (const interfaceName of this.inheritance[cType.name]?.mixins || []) {
const interfaceDartName = this.toDartTypeName(interfaceName);
const interfaceFileName = `${toSnakeCase(interfaceDartName)}.dart`;
if (interfaceFileName !== currentFileName) {
imports.add(interfaceFileName);
}
}
// Collect from methods and constructors
let hasArrays = false;
for (const method of [...cType.methods, ...cType.constructors]) {
if (this.hasRawPointerParameters(method)) continue;
// Return type
if (method.returnType.startsWith('spine_array_')) {
hasArrays = true;
} else if (method.returnType.startsWith('spine_')) {
const cleanType = method.returnType.replace('*', '').trim();
if (!this.isPrimitive(cleanType)) {
const typeName = this.toDartTypeName(cleanType);
const fileName = `${toSnakeCase(typeName)}.dart`;
if (fileName !== currentFileName) {
imports.add(fileName);
}
// If return type is abstract, add imports for all concrete subclasses
// that could be referenced in the RTTI-based switch statement
const cClass = this.classMap.get(cleanType);
if (cClass && this.isAbstract(cClass)) {
const concreteSubclasses = this.getConcreteSubclasses(cleanType);
for (const subclass of concreteSubclasses) {
const dartSubclass = this.toDartTypeName(subclass);
const subclassFileName = `${toSnakeCase(dartSubclass)}.dart`;
if (subclassFileName !== currentFileName) {
imports.add(subclassFileName);
}
}
}
}
}
// Parameters
for (const param of method.parameters) {
if (param.name === 'self') continue;
if (param.cType.startsWith('spine_array_')) {
hasArrays = true;
} else if (this.enumNames.has(param.cType)) {
const enumType = this.toDartTypeName(param.cType);
imports.add(`${toSnakeCase(enumType)}.dart`);
} else if (param.cType.startsWith('spine_')) {
const cleanType = param.cType.replace('*', '').trim();
if (!this.isPrimitive(cleanType)) {
const typeName = this.toDartTypeName(cleanType);
const fileName = `${toSnakeCase(typeName)}.dart`;
if (fileName !== currentFileName) {
imports.add(fileName);
}
}
}
}
}
if (hasArrays) {
imports.add('arrays.dart');
}
return Array.from(imports).sort();
}
private isPrimitive (type: string): boolean {
return ['float', 'double', 'int', 'bool', 'size_t', 'int32_t', 'uint32_t'].includes(type);
}
private isMethodOverride (method: CMethod, cType: CClassOrStruct): boolean {
// Static methods cannot be overridden in Dart
const isStatic = method.parameters.length === 0 ||
(method.parameters[0].name !== 'self' &&
!method.parameters[0].cType.startsWith(cType.name));
if (isStatic) {
return false;
}
// Check if this method exists in parent classes or interfaces
const parentName = this.inheritance[cType.name]?.extends;
if (parentName) {
const parent = this.classMap.get(parentName);
if (parent) {
const methodSuffix = this.getMethodSuffix(method.name, cType.name);
const parentMethodName = `${parentName}_${methodSuffix}`;
if (parent.methods.some(m => m.name === parentMethodName)) {
return true;
}
}
}
// Check interfaces
for (const interfaceName of this.inheritance[cType.name]?.mixins || []) {
const interfaceClass = this.classMap.get(interfaceName);
if (interfaceClass) {
const methodSuffix = this.getMethodSuffix(method.name, cType.name);
const interfaceMethodName = `${interfaceName}_${methodSuffix}`;
if (interfaceClass.methods.some(m => m.name === interfaceMethodName)) {
return true;
}
}
}
return false;
}
// Utility methods - keeping from previous implementation
private toDartTypeName (cTypeName: string): string {
if (cTypeName.startsWith('spine_')) {
const name = cTypeName.slice(6);
return this.toPascalCase(name);
}
return this.toPascalCase(cTypeName);
}
private toCTypeName (dartTypeName: string): string {
return `spine_${toSnakeCase(dartTypeName)}`;
}
private toDartMethodName (cMethodName: string, cTypeName: string): string {
const prefix = `${cTypeName}_`;
if (cMethodName.startsWith(prefix)) {
return this.toCamelCase(cMethodName.slice(prefix.length));
}
return this.toCamelCase(cMethodName);
}
private toDartEnumValueName (cValueName: string, cEnumName: string): string {
const enumNameUpper = cEnumName.toUpperCase();
const prefixes = [
`SPINE_${enumNameUpper}_`,
`${enumNameUpper}_`,
'SPINE_'
];
let name = cValueName;
for (const prefix of prefixes) {
if (name.startsWith(prefix)) {
name = name.slice(prefix.length);
break;
}
}
const enumValue = this.toCamelCase(name.toLowerCase());
if (cEnumName === 'spine_mix_direction' && ['in', 'out'].includes(enumValue)) {
return `direction${this.toPascalCase(enumValue)}`;
}
return enumValue;
}
private toDartReturnType (cType: string, nullable?: boolean): string {
let baseType: string;
if (cType === 'void') return 'void';
if (cType === 'char*' || cType === 'char *' || cType === 'const char*' || cType === 'const char *') baseType = 'String';
else if (cType === 'float' || cType === 'double') baseType = 'double';
else if (cType === 'int' || cType === 'size_t' || cType === 'int32_t' || cType === 'uint32_t') baseType = 'int';
else if (cType === 'bool') baseType = 'bool';
// Handle primitive pointer types
else if (cType === 'void*' || cType === 'void *') baseType = 'Pointer<Void>';
else if (cType === 'float*' || cType === 'float *') baseType = 'Pointer<Float>';
else if (cType === 'uint32_t*' || cType === 'uint32_t *') baseType = 'Pointer<Uint32>';
else if (cType === 'uint16_t*' || cType === 'uint16_t *') baseType = 'Pointer<Uint16>';
else if (cType === 'int*' || cType === 'int *') baseType = 'Pointer<Int32>';
else baseType = this.toDartTypeName(cType);
return nullable ? `${baseType}?` : baseType;
}
private toDartParameterType (param: CParameter): string {
if (param.cType === 'char*' || param.cType === 'char *' || param.cType === 'const char*' || param.cType === 'const char *') {
return 'String';
}
// Handle void* parameters as Pointer<Void>
if (param.cType === 'void*' || param.cType === 'void *') {
return 'Pointer<Void>';
}
return this.toDartReturnType(param.cType, param.isNullable);
}
private convertDartToC (dartValue: string, param: CParameter): string {
if (param.cType === 'char*' || param.cType === 'char *' || param.cType === 'const char*' || param.cType === 'const char *') {
return `${dartValue}.toNativeUtf8().cast<Char>()`;
}
if (this.enumNames.has(param.cType)) {
if (param.isNullable) {
return `${dartValue}?.value ?? 0`;
}
return `${dartValue}.value`;
}
if (param.cType.startsWith('spine_')) {
if (param.isNullable) {
return `${dartValue}?.nativePtr.cast() ?? Pointer.fromAddress(0)`;
}
return `${dartValue}.nativePtr.cast()`;
}
return dartValue;
}
private generateReturnConversion (cReturnType: string, resultVar: string, nullable?: boolean): string {
if (cReturnType === 'char*' || cReturnType === 'char *' || cReturnType === 'const char*' || cReturnType === 'const char *') {
if (nullable) {
return `return ${resultVar}.address == 0 ? null : ${resultVar}.cast<Utf8>().toDartString();`;
}
return `return ${resultVar}.cast<Utf8>().toDartString();`;
}
if (this.enumNames.has(cReturnType)) {
const dartType = this.toDartTypeName(cReturnType);
if (nullable) {
return `return ${resultVar} == 0 ? null : ${dartType}.fromValue(${resultVar});`;
}
return `return ${dartType}.fromValue(${resultVar});`;
}
if (cReturnType.startsWith('spine_array_')) {
const dartType = this.toDartTypeName(cReturnType);
if (nullable) {
return `return ${resultVar}.address == 0 ? null : ${dartType}.fromPointer(${resultVar});`;
}
return `return ${dartType}.fromPointer(${resultVar});`;
}
if (cReturnType.startsWith('spine_')) {
const dartType = this.toDartTypeName(cReturnType);
const cClass = this.classMap.get(cReturnType);
if (nullable) {
if (cClass && this.isAbstract(cClass)) {
return `if (${resultVar}.address == 0) return null;
${this.generateRttiBasedInstantiation(dartType, resultVar, cClass)}`;
}
return `return ${resultVar}.address == 0 ? null : ${dartType}.fromPointer(${resultVar});`;
} else {
if (cClass && this.isAbstract(cClass)) {
return this.generateRttiBasedInstantiation(dartType, resultVar, cClass);
}
return `return ${dartType}.fromPointer(${resultVar});`;
}
}
return `return ${resultVar};`;
}
private generateRttiBasedInstantiation (abstractType: string, resultVar: string, abstractClass: CClassOrStruct): string {
const lines: string[] = [];
const concreteSubclasses = this.getConcreteSubclasses(abstractClass.name);
if (concreteSubclasses.length === 0) {
return `throw UnsupportedError('Cannot instantiate abstract class ${abstractType} from pointer - no concrete subclasses found');`;
}
lines.push(`final rtti = SpineBindings.bindings.${abstractClass.name}_get_rtti(${resultVar});`);
lines.push(`final className = SpineBindings.bindings.spine_rtti_get_class_name(rtti).cast<Utf8>().toDartString();`);
lines.push(`switch (className) {`);
for (const subclass of concreteSubclasses) {
const dartSubclass = this.toDartTypeName(subclass);
lines.push(` case '${subclass}':`);
lines.push(` return ${dartSubclass}.fromPointer(${resultVar}.cast());`);
}
lines.push(` default:`);
lines.push(` throw UnsupportedError('Unknown concrete type: \$className for abstract class ${abstractType}');`);
lines.push(`}`);
return lines.join('\n ');
}
private getConcreteSubclasses (abstractClassName: string): string[] {
const concreteSubclasses: string[] = [];
for (const [childName, supertypeList] of Object.entries(this.supertypes || {})) {
if (supertypeList.includes(abstractClassName)) {
const childClass = this.classMap.get(childName);
if (childClass && !this.isAbstract(childClass)) {
concreteSubclasses.push(childName);
}
}
}
return concreteSubclasses;
}
private isAbstract (cType: CClassOrStruct): boolean {
return cType.cppType.isAbstract === true;
}
private getConstructorName (constr: CMethod, cType: CClassOrStruct): string {
const dartClassName = this.toDartTypeName(cType.name);
const cTypeName = this.toCTypeName(dartClassName);
let constructorName = constr.name.replace(`${cTypeName}_create`, '');
if (constructorName) {
if (/^\d+$/.test(constructorName)) {
if (cType.name === 'spine_color' && constr.name === 'spine_color_create2') {
constructorName = 'fromRGBA';
} else if (constr.parameters.length > 0) {
const firstParamType = constr.parameters[0].cType.replace('*', '').trim();
if (firstParamType === cType.name) {
constructorName = 'from';
} else {
constructorName = `variant${constructorName}`;
}
} else {
constructorName = `variant${constructorName}`;
}
} else if (constructorName.startsWith('_')) {
constructorName = this.toCamelCase(constructorName.slice(1));
}
}
return constructorName || dartClassName;
}
private extractPropertyName (methodName: string, typeName: string): string {
const prefix = `${typeName}_`;
let name = methodName.startsWith(prefix) ? methodName.slice(prefix.length) : methodName;
if (name.startsWith('get_')) {
name = name.slice(4);
} else if (name.startsWith('set_')) {
name = name.slice(4);
}
if (name === 'update_cache') {
return 'updateCacheList';
}
const typeNames = ['int', 'float', 'double', 'bool', 'string'];
if (typeNames.includes(name.toLowerCase())) {
return `${this.toCamelCase(name)}Value`;
}
return this.toCamelCase(name);
}
private hasRawPointerParameters (method: CMethod): boolean {
for (const param of method.parameters) {
if (this.isRawPointer(param.cType)) {
return true;
}
}
return false;
}
private isRawPointer (cType: string): boolean {
if (cType === 'char*' || cType === 'char *' || cType === 'const char*' || cType === 'const char *') {
return false;
}
if (cType.includes('*')) {
const cleanType = cType.replace('*', '').trim();
if (!cleanType.startsWith('spine_')) {
return true;
}
}
return false;
}
private isMethodInherited (method: CMethod, cType: CClassOrStruct): boolean {
const parentName = this.inheritance[cType.name]?.extends;
if (!parentName) {
return false;
}
const parent = this.classMap.get(parentName);
if (!parent) {
return false;
}
const methodSuffix = this.getMethodSuffix(method.name, cType.name);
// Don't consider dispose methods as inherited - they're type-specific
if (methodSuffix === 'dispose') {
return false;
}
const parentMethodName = `${parentName}_${methodSuffix}`;
const hasInParent = parent.methods.some(m => m.name === parentMethodName);
if (hasInParent) {
return true;
}
return this.isMethodInherited(method, parent);
}
private getMethodSuffix (methodName: string, typeName: string): string {
const prefix = `${typeName}_`;
if (methodName.startsWith(prefix)) {
return methodName.slice(prefix.length);
}
return methodName;
}
private renumberMethods (methods: CMethod[], typeName: string): Array<{ method: CMethod, renamedMethod?: string }> {
const result: Array<{ method: CMethod, renamedMethod?: string }> = [];
const methodGroups = new Map<string, CMethod[]>();
for (const method of methods) {
const match = method.name.match(/^(.+?)_(\d+)$/);
if (match) {
const baseName = match[1];
if (!methodGroups.has(baseName)) {
methodGroups.set(baseName, []);
}
methodGroups.get(baseName)!.push(method);
} else {
result.push({ method });
}
}
for (const [baseName, groupedMethods] of methodGroups) {
if (groupedMethods.length === 1) {
const method = groupedMethods[0];
const dartMethodName = this.toDartMethodName(baseName, typeName);
result.push({ method, renamedMethod: dartMethodName });
} else {
groupedMethods.sort((a, b) => {
const aNum = parseInt(a.name.match(/_(\d+)$/)![1]);
const bNum = parseInt(b.name.match(/_(\d+)$/)![1]);
return aNum - bNum;
});
for (let i = 0; i < groupedMethods.length; i++) {
const method = groupedMethods[i];
const newNumber = i + 1;
const currentNumber = parseInt(method.name.match(/_(\d+)$/)![1]);
const baseDartName = this.toDartMethodName(baseName, typeName);
if (i === 0) {
// First method in the group - remove the number
result.push({ method, renamedMethod: baseDartName });
} else if (newNumber !== currentNumber) {
// Need to renumber
result.push({ method, renamedMethod: `${baseDartName}${newNumber}` });
} else {
// Number is correct, no renamed method needed
result.push({ method });
}
}
}
}
return result;
}
private needsStringConversions (cType: CClassOrStruct): boolean {
for (const method of [...cType.methods, ...cType.constructors]) {
if (method.returnType === 'char*' || method.returnType === 'char *' ||
method.returnType === 'const char*' || method.returnType === 'const char *') {
return true;
}
for (const param of method.parameters) {
if (param.cType === 'char*' || param.cType === 'char *' ||
param.cType === 'const char*' || param.cType === 'const char *') {
return true;
}
}
// Check if method returns abstract types (which use RTTI and need Utf8)
if (method.returnType.startsWith('spine_')) {
const cleanType = method.returnType.replace('*', '').trim();
const returnClass = this.classMap.get(cleanType);
if (returnClass && this.isAbstract(returnClass)) {
return true;
}
}
}
return false;
}
private async writeExportFile (classes: DartClass[], enums: DartEnum[]): Promise<void> {
const lines: string[] = [];
lines.push(LICENSE_HEADER);
lines.push('');
lines.push('// AUTO GENERATED FILE, DO NOT EDIT.');
lines.push('');
lines.push('// Export all generated types');
lines.push('');
// Export enums
if (enums.length > 0) {
lines.push('// Enums');
for (const dartEnum of enums) {
lines.push(`export '${toSnakeCase(dartEnum.name)}.dart';`);
}
lines.push('');
}
// Export classes
if (classes.length > 0) {
lines.push('// Classes');
for (const dartClass of classes) {
lines.push(`export '${toSnakeCase(dartClass.name)}.dart';`);
}
lines.push('');
}
// Export arrays
lines.push('// Arrays');
lines.push(`export 'arrays.dart';`);
const filePath = path.join(path.dirname(path.dirname(this.outputDir)), 'lib/generated/api.dart');
fs.writeFileSync(filePath, lines.join('\n'));
}
private toPascalCase (str: string): string {
return str.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
}
private toCamelCase (str: string): string {
const pascal = this.toPascalCase(str);
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
}
private async writeWebInitFile(cTypes: CClassOrStruct[], cArrayTypes: CClassOrStruct[]): Promise<void> {
const lines: string[] = [];
lines.push(LICENSE_HEADER);
lines.push('');
lines.push('// AUTO GENERATED FILE, DO NOT EDIT.');
lines.push('');
lines.push('// ignore_for_file: type_argument_not_matching_bounds');
lines.push(`import 'package:flutter/services.dart';`);
lines.push(`import 'package:inject_js/inject_js.dart' as js;`);
lines.push(`import 'web_ffi/web_ffi.dart';`);
lines.push(`import 'web_ffi/web_ffi_modules.dart';`);
lines.push('');
lines.push(`import 'generated/spine_dart_bindings_generated.dart';`);
lines.push('');
lines.push('Module? _module;');
lines.push('');
lines.push('class SpineDartFFI {');
lines.push(' final DynamicLibrary dylib;');
lines.push(' final Allocator allocator;');
lines.push('');
lines.push(' SpineDartFFI(this.dylib, this.allocator);');
lines.push('}');
lines.push('');
lines.push('Future<SpineDartFFI> initSpineDartFFI(bool useStaticLinkage) async {');
lines.push(' if (_module == null) {');
lines.push(' Memory.init();');
lines.push('');
// Collect all wrapper types
const wrapperTypes = new Set<string>();
// Add regular types
for (const cType of cTypes) {
wrapperTypes.add(`${cType.name}_wrapper`);
}
// Add array types
for (const arrayType of cArrayTypes) {
wrapperTypes.add(`${arrayType.name}_wrapper`);
}
// Add special types that might not be in the regular types list
wrapperTypes.add('spine_atlas_result_wrapper');
wrapperTypes.add('spine_skeleton_data_result_wrapper');
wrapperTypes.add('spine_skeleton_drawable_wrapper');
wrapperTypes.add('spine_animation_state_events_wrapper');
wrapperTypes.add('spine_skin_entry_wrapper');
wrapperTypes.add('spine_skin_entries_wrapper');
wrapperTypes.add('spine_texture_loader_wrapper');
// Sort and write all registerOpaqueType calls
const sortedTypes = Array.from(wrapperTypes).sort();
for (const type of sortedTypes) {
lines.push(` registerOpaqueType<${type}>();`);
}
lines.push('');
lines.push(` await js.importLibrary('assets/packages/spine_flutter/lib/assets/libspine_flutter.js');`);
lines.push(` Uint8List wasmBinaries = (await rootBundle.load(`);
lines.push(` 'packages/spine_flutter/lib/assets/libspine_flutter.wasm',`);
lines.push(` ))`);
lines.push(` .buffer`);
lines.push(` .asUint8List();`);
lines.push(` _module = await EmscriptenModule.compile(wasmBinaries, 'libspine_flutter');`);
lines.push(' }');
lines.push(' Module? m = _module;');
lines.push(' if (m != null) {');
lines.push(' final dylib = DynamicLibrary.fromModule(m);');
lines.push(' return SpineDartFFI(dylib, dylib.boundMemory);');
lines.push(' } else {');
lines.push(` throw Exception("Couldn't load libspine-flutter.js/.wasm");`);
lines.push(' }');
lines.push('}');
const filePath = path.join(path.dirname(this.outputDir), 'spine_dart_init_web.dart');
fs.writeFileSync(filePath, lines.join('\n'));
}
}