Forráskód Böngészése

[flutter] Full array capabilities, still needs checking against Dart ListBase.

Mario Zechner 1 hónapja
szülő
commit
59829c3f69

+ 198 - 20
spine-flutter/codegen/src/dart-writer.ts

@@ -80,7 +80,6 @@ export class DartWriter {
 
 	private cleanOutputDirectory (): void {
 		if (fs.existsSync(this.outputDir)) {
-			console.log(`[NEW] Cleaning ${this.outputDir}...`);
 			fs.rmSync(this.outputDir, { recursive: true, force: true });
 		}
 		fs.mkdirSync(this.outputDir, { recursive: true });
@@ -108,8 +107,6 @@ export class DartWriter {
 			this.enumNames.add(cEnum.name);
 		}
 
-		console.log('[NEW] Transforming to Dart model...');
-
 		const dartClasses: DartClass[] = [];
 		const dartEnums: DartEnum[] = [];
 
@@ -161,13 +158,10 @@ export class DartWriter {
 		isInterface: Record<string, boolean> = {},
 		supertypes: Record<string, string[]> = {}
 	): Promise<void> {
-		console.log('[NEW] Starting new Dart writer...');
-
 		// Step 1: Transform to clean model
 		const { classes, enums } = this.transformToDartModel(cTypes, cEnums, inheritance, isInterface, supertypes);
 
 		// Step 2 & 3: Generate and write files
-		console.log('[NEW] Writing enum files...');
 		for (const dartEnum of enums) {
 			const content = this.generateEnumCode(dartEnum);
 			const fileName = `${toSnakeCase(dartEnum.name)}.dart`;
@@ -175,7 +169,6 @@ export class DartWriter {
 			fs.writeFileSync(filePath, content);
 		}
 
-		console.log('[NEW] Writing class files...');
 		for (const dartClass of classes) {
 			const content = this.generateDartCode(dartClass);
 			const fileName = `${toSnakeCase(dartClass.name)}.dart`;
@@ -184,13 +177,13 @@ export class DartWriter {
 		}
 
 		// Generate arrays.dart (crucial - this was missing!)
-		console.log('[NEW] Writing arrays.dart...');
 		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);
-
-		console.log('[NEW] New dart writer completed!');
 	}
 
 	// Class type resolution (from spec)
@@ -509,11 +502,7 @@ export class DartWriter {
 		const lines: string[] = [];
 
 		lines.push('');
-		lines.push("import 'dart:ffi';");
-
-		if (needsPackageFfi) {
-			lines.push("import 'package:ffi/ffi.dart';");
-		}
+		lines.push("import '../ffi_proxy.dart';");
 
 		lines.push("import 'spine_dart_bindings_generated.dart';");
 		lines.push("import '../spine_bindings.dart';");
@@ -714,8 +703,7 @@ ${declaration} {`;
 
 		lines.push(this.generateHeader());
 		lines.push('');
-		lines.push("import 'dart:ffi';");
-		lines.push("import 'package:ffi/ffi.dart';");
+		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';");
@@ -777,16 +765,47 @@ ${declaration} {`;
 
 		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) : super(ptr);`);
+		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');
@@ -854,6 +873,86 @@ ${declaration} {`;
 			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('}');
@@ -1139,7 +1238,7 @@ ${declaration} {`;
 		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;
 	}
 
@@ -1203,7 +1302,7 @@ ${declaration} {`;
 		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;
@@ -1501,4 +1600,83 @@ ${declaration} {`;
 		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 'package:web_ffi_fork/web_ffi.dart';`);
+		lines.push(`import 'package:web_ffi_fork/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'));
+	}
 }

+ 23 - 1
spine-flutter/codegen/src/index.ts

@@ -32,7 +32,24 @@ async function generateFFIBindings(spineCDir: string): Promise<void> {
     // Replace dart:ffi import with ffi_proxy.dart
     console.log('Replacing dart:ffi import with ffi_proxy.dart...');
     let content = fs.readFileSync(bindingsPath, 'utf8');
-    content = content.replace("import 'dart:ffi' as ffi;", "import '../../ffi_proxy.dart' as ffi;");
+    content = content.replace("import 'dart:ffi' as ffi;", "import '../ffi_proxy.dart' as ffi;");
+
+    // For web_ffi compatibility, we need to convert wrapper structs to Opaque
+    console.log('Converting wrapper structs to Opaque for web_ffi compatibility...');
+    const wrapperMatches = content.match(/final class \w+_wrapper extends ffi\.Struct \{[^}]+\}/g) || [];
+    console.log(`Found ${wrapperMatches.length} wrapper structs to convert`);
+    content = content.replace(/final class (\w+_wrapper) extends ffi\.Struct \{[^}]+\}/g,
+        'final class $1 extends ffi.Opaque {}');
+
+    // Also remove __mbstate_t and other system types
+    console.log('Removing system types like __mbstate_t...');
+    content = content.replace(/final class __mbstate_t extends ffi\.Union \{[^}]*\}/gs, '');
+    content = content.replace(/final class __\w+ extends ffi\.\w+ \{[^}]*\}/gs, '');
+
+    // Remove structs with external fields (spine_rect and spine_vector2)
+    console.log('Removing structs with external fields...');
+    content = content.replace(/final class (spine_rect|spine_vector2) extends ffi\.Struct \{[\s\S]*?\n\}/gm, '');
+
     fs.writeFileSync(bindingsPath, content);
 
     // Clean up ffigen.yaml
@@ -78,12 +95,17 @@ functions:
 structs:
   include:
     - 'spine_.*'
+  exclude:
+    - '__.*'  # Exclude all structs starting with __
+  dependency-only: opaque
 enums:
   include:
     - 'spine_.*'
 typedefs:
   include:
     - 'spine_.*'
+  exclude:
+    - '__.*'  # Exclude system typedefs like __mbstate_t
 preamble: |
   // ignore_for_file: always_specify_types, constant_identifier_names
   // ignore_for_file: camel_case_types

+ 2 - 1
spine-flutter/compile-wasm.sh

@@ -32,6 +32,7 @@ log_action "Compiling spine-cpp to WASM"
 # Build the emscripten command
 if EMCC_OUTPUT=$(em++ \
     -Isrc/spine-cpp/include \
+    -Isrc/spine-c/include \
     -O2 --closure 1 -fno-rtti -fno-exceptions \
     -s STRICT=1 \
     -s EXPORTED_RUNTIME_METHODS=wasmExports \
@@ -45,7 +46,7 @@ if EMCC_OUTPUT=$(em++ \
     --no-entry \
     --extern-pre-js pre.js \
     -s EXPORT_NAME=libspine_flutter \
-    src/spine-cpp-lite/spine-cpp-lite.cpp $(find src/spine-cpp/src -type f) \
+    src/spine-c/src/extensions.cpp $(find src/spine-c/src/generated -name "*.cpp") $(find src/spine-cpp/src -name "*.cpp") \
     -o lib/assets/libspine_flutter.js 2>&1); then
     log_ok
 else