import { CClassOrStruct, CEnum, CArrayType, InheritanceInfo, CMethod, CParameter } from '../../../spine-c/codegen/src/c-types.js'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; export interface SwiftGenerationOptions { outputDir: string; licenseHeader: string; } interface SwiftClass { name: string; cType: CClassOrStruct; isAbstract: boolean; isInterface: boolean; parentClass?: string; protocols: string[]; methods: SwiftMethod[]; properties: SwiftProperty[]; constructors: SwiftConstructor[]; } interface SwiftMethod { name: string; cMethod: CMethod; parameters: SwiftParameter[]; returnType: string; isStatic: boolean; implementation: string; } interface SwiftParameter { name: string; type: string; cParam: CParameter; } interface SwiftProperty { name: string; type: string; isReadOnly: boolean; getter?: SwiftMethod; setter?: SwiftMethod; } interface SwiftConstructor { parameters: SwiftParameter[]; implementation: string; } export class SwiftWriter { private cTypes: CClassOrStruct[]; private cEnums: CEnum[]; private cArrayTypes: CArrayType[]; private inheritance: Map; private isInterface: Map; private supertypes: Map>; private subtypes: Map>; private swiftClasses: Map = new Map(); constructor( cTypes: CClassOrStruct[], cEnums: CEnum[], cArrayTypes: CArrayType[], inheritance: any, isInterface: any, supertypes: any, subtypes: any ) { this.cTypes = cTypes; this.cEnums = cEnums; this.cArrayTypes = cArrayTypes; this.inheritance = new Map(Object.entries(inheritance || {})); this.isInterface = new Map(Object.entries(isInterface || {})); this.supertypes = new Map(Object.entries(supertypes || {}).map(([k, v]) => [k, new Set(v as string[])])); this.subtypes = new Map(Object.entries(subtypes || {}).map(([k, v]) => [k, new Set(v as string[])])); } async generate(options: SwiftGenerationOptions): Promise { console.log('Generating Swift bindings...'); // Process all C types to create Swift class models for (const cType of this.cTypes) { this.processType(cType); } // Generate individual Swift files for each type for (const [typeName, swiftClass] of this.swiftClasses) { await this.generateClassFile(swiftClass, options); } // Generate enum file await this.generateEnumsFile(options); // Generate arrays file await this.generateArraysFile(options); // Generate main exports file await this.generateExportsFile(options); console.log('Swift bindings generation complete!'); } private processType(cType: CClassOrStruct): void { const swiftName = this.getSwiftClassName(cType.name); const inheritanceInfo = this.inheritance.get(cType.name); const isAbstract = cType.cppType?.abstract || false; const isInterface = this.isInterface.get(cType.name) || false; const swiftClass: SwiftClass = { name: swiftName, cType: cType, isAbstract, isInterface, parentClass: inheritanceInfo?.extends ? this.getSwiftClassName(inheritanceInfo.extends) : undefined, protocols: inheritanceInfo?.mixins?.map(m => this.getSwiftClassName(m)) || [], methods: [], properties: [], constructors: [] }; // Process constructors for (const ctor of cType.constructors) { swiftClass.constructors.push(this.processConstructor(ctor, cType)); } // Process methods and detect properties const getterSetterMap = new Map(); for (const method of cType.methods) { // Check if it's a getter/setter pattern const getterMatch = method.name.match(/^get(.+)$/); const setterMatch = method.name.match(/^set(.+)$/); if (getterMatch && method.parameters.length === 0) { const propName = this.lowercaseFirst(getterMatch[1]); const prop = getterSetterMap.get(propName) || {}; prop.getter = method; getterSetterMap.set(propName, prop); } else if (setterMatch && method.parameters.length === 1) { const propName = this.lowercaseFirst(setterMatch[1]); const prop = getterSetterMap.get(propName) || {}; prop.setter = method; getterSetterMap.set(propName, prop); } else { // Regular method swiftClass.methods.push(this.processMethod(method, cType)); } } // Create properties from getter/setter pairs for (const [propName, { getter, setter }] of getterSetterMap) { if (getter) { swiftClass.properties.push({ name: propName, type: this.mapReturnType(getter.returnType, getter.returnTypeNullable), isReadOnly: !setter, getter: getter ? this.processMethod(getter, cType) : undefined, setter: setter ? this.processMethod(setter, cType) : undefined }); } } this.swiftClasses.set(cType.name, swiftClass); } private processConstructor(ctor: any, cType: CClassOrStruct): SwiftConstructor { const params = (ctor.parameters || []).map((p: CParameter) => this.processParameter(p)); return { parameters: params, implementation: this.generateConstructorImplementation(ctor, cType) }; } private processMethod(method: CMethod, cType: CClassOrStruct): SwiftMethod { // Filter out 'self' parameter which is always the first one for instance methods const params = method.parameters.filter(p => p.name !== 'self').map(p => this.processParameter(p)); return { name: this.getSwiftMethodName(method.name, cType.name), cMethod: method, parameters: params, returnType: this.mapReturnType(method.returnType, method.returnTypeNullable), isStatic: method.isStatic || false, implementation: this.generateMethodImplementation(method, cType) }; } private processParameter(param: CParameter): SwiftParameter { return { name: this.sanitizeParameterName(param.name), type: this.mapParameterType(param.cType, param.isNullable), cParam: param }; } private generateConstructorImplementation(ctor: any, cType: CClassOrStruct): string { // TODO: Generate actual implementation return ''; } private generateMethodImplementation(method: CMethod, cType: CClassOrStruct): string { // TODO: Generate actual implementation return ''; } private async generateClassFile(swiftClass: SwiftClass, options: SwiftGenerationOptions): Promise { const filePath = path.join(options.outputDir, `${swiftClass.name}.swift`); const content = this.generateClassContent(swiftClass, options); await fs.writeFile(filePath, content, 'utf-8'); } private generateClassContent(swiftClass: SwiftClass, options: SwiftGenerationOptions): string { const lines: string[] = []; // Add license header as comment lines.push(...options.licenseHeader.split('\n').map(line => `// ${line}`)); lines.push(''); // Imports lines.push('import Foundation'); lines.push(''); // Class declaration const inheritance = this.buildInheritanceDeclaration(swiftClass); lines.push(`@objc(Spine${swiftClass.name})`); lines.push(`@objcMembers`); lines.push(`public ${swiftClass.isAbstract ? 'class' : 'final class'} ${swiftClass.name}${inheritance} {`); // Internal wrapper property lines.push(` internal let wrappee: ${swiftClass.cType.name}`); lines.push(''); // Internal init lines.push(` internal init(_ wrappee: ${swiftClass.cType.name}) {`); lines.push(' self.wrappee = wrappee'); if (swiftClass.parentClass && !swiftClass.isInterface) { lines.push(' super.init(wrappee)'); } else { lines.push(' super.init()'); } lines.push(' }'); lines.push(''); // isEqual and hash if (!swiftClass.parentClass || swiftClass.parentClass === 'NSObject') { lines.push(' public override func isEqual(_ object: Any?) -> Bool {'); lines.push(` guard let other = object as? ${swiftClass.name} else { return false }`); lines.push(' return self.wrappee == other.wrappee'); lines.push(' }'); lines.push(''); lines.push(' public override var hash: Int {'); lines.push(' var hasher = Hasher()'); lines.push(' hasher.combine(self.wrappee)'); lines.push(' return hasher.finalize()'); lines.push(' }'); lines.push(''); } // Add public constructors if (!swiftClass.isAbstract && swiftClass.constructors.length > 0) { for (const ctor of swiftClass.constructors) { lines.push(...this.generateConstructor(ctor, swiftClass)); lines.push(''); } } // Add properties for (const prop of swiftClass.properties) { lines.push(...this.generateProperty(prop, swiftClass)); lines.push(''); } // Add methods for (const method of swiftClass.methods) { lines.push(...this.generateMethod(method, swiftClass)); lines.push(''); } // Add destructor if concrete class if (!swiftClass.isAbstract && swiftClass.cType.destructor) { lines.push(' deinit {'); lines.push(` ${swiftClass.cType.destructor.name}(wrappee)`); lines.push(' }'); } lines.push('}'); return lines.join('\n'); } private buildInheritanceDeclaration(swiftClass: SwiftClass): string { const parts: string[] = []; if (swiftClass.parentClass) { parts.push(swiftClass.parentClass); } else if (!swiftClass.isInterface) { parts.push('NSObject'); } if (swiftClass.protocols.length > 0) { parts.push(...swiftClass.protocols); } return parts.length > 0 ? `: ${parts.join(', ')}` : ''; } private async generateEnumsFile(options: SwiftGenerationOptions): Promise { const filePath = path.join(options.outputDir, 'Enums.swift'); const lines: string[] = []; // Add license header lines.push(...options.licenseHeader.split('\n').map(line => `// ${line}`)); lines.push(''); lines.push('import Foundation'); lines.push(''); // Generate each enum for (const cEnum of this.cEnums) { lines.push(`public typealias ${this.getSwiftClassName(cEnum.name)} = ${cEnum.name}`); } await fs.writeFile(filePath, lines.join('\n'), 'utf-8'); } private async generateArraysFile(options: SwiftGenerationOptions): Promise { const filePath = path.join(options.outputDir, 'Arrays.swift'); const lines: string[] = []; // Add license header lines.push(...options.licenseHeader.split('\n').map(line => `// ${line}`)); lines.push(''); lines.push('import Foundation'); lines.push(''); // TODO: Generate array wrapper classes await fs.writeFile(filePath, lines.join('\n'), 'utf-8'); } private async generateExportsFile(options: SwiftGenerationOptions): Promise { const filePath = path.join(options.outputDir, 'Spine.Generated.swift'); const lines: string[] = []; // Add license header lines.push(...options.licenseHeader.split('\n').map(line => `// ${line}`)); lines.push(''); lines.push('// This file exports all generated types'); lines.push(''); await fs.writeFile(filePath, lines.join('\n'), 'utf-8'); } private mapCTypeToSwift(cType: string | undefined): string { if (!cType) return 'Void'; const typeMap: Record = { 'void': 'Void', 'spine_bool': 'Bool', 'bool': 'Bool', 'int': 'Int32', 'int32_t': 'Int32', 'uint32_t': 'UInt32', 'int16_t': 'Int16', 'uint16_t': 'UInt16', 'int64_t': 'Int64', 'uint64_t': 'UInt64', 'float': 'Float', 'double': 'Double', 'const char *': 'String?', 'char *': 'String?', 'const utf8 *': 'String?', 'utf8 *': 'String?', 'float *': 'UnsafeMutablePointer?', 'int *': 'UnsafeMutablePointer?', 'int32_t *': 'UnsafeMutablePointer?', }; // Check if it's already in the map if (typeMap[cType]) { return typeMap[cType]; } // Handle spine types if (cType.startsWith('spine_')) { // It's a spine type - map to Swift class const swiftType = this.getSwiftClassName(cType); return swiftType; } // Handle pointer types if (cType.endsWith('*')) { // It's a pointer to something const baseType = cType.slice(0, -1).trim(); if (baseType.startsWith('spine_')) { // Pointer to spine type const swiftType = this.getSwiftClassName(baseType); return swiftType + '?'; } else { // Generic pointer return 'OpaquePointer?'; } } return cType; } private mapReturnType(cType: string | undefined, isNullable: boolean): string { if (!cType) return 'Void'; const baseType = this.mapCTypeToSwift(cType); // Handle spine type pointers if (cType.startsWith('spine_') && cType.endsWith('*')) { const typeName = cType.slice(0, -1).trim(); // Remove pointer const swiftType = this.getSwiftClassName(typeName); return isNullable ? `${swiftType}?` : swiftType; } return baseType; } private mapParameterType(cType: string | undefined, isNullable: boolean): string { return this.mapReturnType(cType, isNullable); } private getSwiftClassName(cTypeName: string): string { // Remove spine_ prefix and convert to PascalCase const name = cTypeName.replace(/^spine_/, ''); return name.split('_') .map(part => part.charAt(0).toUpperCase() + part.slice(1)) .join(''); } private getSwiftMethodName(cMethodName: string, cTypeName: string): string { // Remove the type prefix (e.g., spine_skeleton_update -> update) let name = cMethodName; if (name.startsWith(cTypeName + '_')) { name = name.substring(cTypeName.length + 1); } // Handle numbered methods (e.g., setSkin_1 -> setSkin) name = name.replace(/_\d+$/, ''); // Convert snake_case to camelCase const parts = name.split('_'); if (parts.length > 1) { name = parts[0] + parts.slice(1).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(''); } return name; } private lowercaseFirst(str: string): string { return str.charAt(0).toLowerCase() + str.slice(1); } private sanitizeParameterName(name: string): string { // Swift reserved words that need escaping const reserved = ['in', 'var', 'let', 'func', 'class', 'struct', 'enum', 'protocol', 'extension']; return reserved.includes(name) ? `\`${name}\`` : name; } private isNullableType(cType: string): boolean { // Pointers can be null return cType.includes('*'); } private generateConstructor(ctor: SwiftConstructor, swiftClass: SwiftClass): string[] { const lines: string[] = []; // Generate parameter list const params = ctor.parameters.map(p => `${p.name}: ${p.type}`).join(', '); lines.push(` public convenience init(${params}) {`); // Generate C function call const cParams = ctor.parameters.map(p => { if (p.type.endsWith('?') && p.type !== 'String?') { // It's an optional spine type return `${p.name}?.wrappee ?? nil`; } else if (p.type === 'String?') { return p.name; } else if (this.isSpineType(p.type)) { return `${p.name}.wrappee`; } else { return p.name; } }).join(', '); lines.push(` let ptr = ${swiftClass.cType.constructors[0].name}(${cParams})`); lines.push(' self.init(ptr)'); lines.push(' }'); return lines; } private generateProperty(prop: SwiftProperty, swiftClass: SwiftClass): string[] { const lines: string[] = []; if (prop.isReadOnly) { // Read-only property lines.push(` public var ${prop.name}: ${prop.type} {`); if (prop.getter) { lines.push(...this.generatePropertyGetter(prop.getter, swiftClass)); } lines.push(' }'); } else { // Read-write property lines.push(` public var ${prop.name}: ${prop.type} {`); lines.push(' get {'); if (prop.getter) { lines.push(...this.generatePropertyGetter(prop.getter, swiftClass).map(l => ' ' + l)); } lines.push(' }'); lines.push(' set {'); if (prop.setter) { lines.push(...this.generatePropertySetter(prop.setter, swiftClass).map(l => ' ' + l)); } lines.push(' }'); lines.push(' }'); } return lines; } private generatePropertyGetter(method: SwiftMethod, swiftClass: SwiftClass): string[] { const lines: string[] = []; const returnType = method.returnType; if (returnType === 'Void') { lines.push(` ${method.cMethod.name}(wrappee)`); } else if (returnType === 'String?') { lines.push(` let result = ${method.cMethod.name}(wrappee)`); lines.push(' return result != nil ? String(cString: result!) : nil'); } else if (returnType === 'Bool') { lines.push(` return ${method.cMethod.name}(wrappee) != 0`); } else if (this.isSpineType(returnType)) { const isOptional = returnType.endsWith('?'); const baseType = isOptional ? returnType.slice(0, -1) : returnType; lines.push(` let result = ${method.cMethod.name}(wrappee)`); if (isOptional) { lines.push(` return result != nil ? ${baseType}(result!) : nil`); } else { lines.push(` return ${baseType}(result)`); } } else { lines.push(` return ${method.cMethod.name}(wrappee)`); } return lines; } private generatePropertySetter(method: SwiftMethod, swiftClass: SwiftClass): string[] { const lines: string[] = []; const param = method.parameters[0]; let paramValue = 'newValue'; if (param.type === 'String?') { paramValue = 'newValue?.cString(using: .utf8)'; } else if (param.type === 'Bool') { paramValue = 'newValue ? 1 : 0'; } else if (this.isSpineType(param.type)) { if (param.type.endsWith('?')) { paramValue = 'newValue?.wrappee'; } else { paramValue = 'newValue.wrappee'; } } lines.push(` ${method.cMethod.name}(wrappee, ${paramValue})`); return lines; } private generateMethod(method: SwiftMethod, swiftClass: SwiftClass): string[] { const lines: string[] = []; // Generate parameter list const params = method.parameters.map(p => `${p.name}: ${p.type}`).join(', '); const funcDecl = method.isStatic ? 'public static func' : 'public func'; lines.push(` ${funcDecl} ${method.name}(${params})${method.returnType !== 'Void' ? ' -> ' + method.returnType : ''} {`); // Generate method body const cParams = [ !method.isStatic ? 'wrappee' : '', ...method.parameters.map(p => { if (p.type === 'String?') { return `${p.name}?.cString(using: .utf8)`; } else if (p.type === 'Bool') { return `${p.name} ? 1 : 0`; } else if (this.isSpineType(p.type)) { if (p.type.endsWith('?')) { return `${p.name}?.wrappee`; } else { return `${p.name}.wrappee`; } } else { return p.name; } }) ].filter(p => p !== '').join(', '); if (method.returnType === 'Void') { lines.push(` ${method.cMethod.name}(${cParams})`); } else if (method.returnType === 'String?') { lines.push(` let result = ${method.cMethod.name}(${cParams})`); lines.push(' return result != nil ? String(cString: result!) : nil'); } else if (method.returnType === 'Bool') { lines.push(` return ${method.cMethod.name}(${cParams}) != 0`); } else if (this.isSpineType(method.returnType)) { const isOptional = method.returnType.endsWith('?'); const baseType = isOptional ? method.returnType.slice(0, -1) : method.returnType; lines.push(` let result = ${method.cMethod.name}(${cParams})`); if (isOptional) { lines.push(` return result != nil ? ${baseType}(result!) : nil`); } else { lines.push(` return ${baseType}(result)`); } } else { lines.push(` return ${method.cMethod.name}(${cParams})`); } lines.push(' }'); return lines; } private isSpineType(type: string): boolean { const baseType = type.endsWith('?') ? type.slice(0, -1) : type; // Check if it's one of our generated Swift classes return Array.from(this.swiftClasses.values()).some(c => c.name === baseType); } }