635 lines
23 KiB
TypeScript

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<string, InheritanceInfo>;
private isInterface: Map<string, boolean>;
private supertypes: Map<string, Set<string>>;
private subtypes: Map<string, Set<string>>;
private swiftClasses: Map<string, SwiftClass> = 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<void> {
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<string, { getter?: CMethod, setter?: CMethod }>();
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<void> {
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<void> {
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<void> {
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<void> {
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<string, string> = {
'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<Float>?',
'int *': 'UnsafeMutablePointer<Int32>?',
'int32_t *': 'UnsafeMutablePointer<Int32>?',
};
// 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);
}
}