1
0
Fork 0

Refactor CodeGen.swift into CodeGen+Signal.swift and CodeGen+Scaffolding.swift

This commit is contained in:
Brendan Szymanski 2026-07-01 17:27:42 -04:00
parent 84a536d314
commit 27edea19af
5 changed files with 361 additions and 295 deletions

View file

@ -0,0 +1,124 @@
import Foundation
extension CodeGenerator {
// MARK: - Package Scaffolding Generation
/// Generates the package scaffolding files (Package.swift, module map, umbrella header)
/// for a self-contained SwiftPM wrapper package.
///
/// Returns a dictionary mapping relative file paths (e.g. `"Package.swift"`,
/// `"Sources/CGtk/module.modulemap"`) to their textual content. The library
/// name in `config.library` determines the package name, the C interop target
/// name (prefixed with `C`). Link names and pkg-config names are derived from
/// the repository's namespace `shared-library` attribute, the umbrella header
/// from the repository's `<c:include>` element, and external library link flags
/// from `<include>` elements and `config.externalLibraries`.
///
/// - Parameters:
/// - config: The generation configuration (uses `library` and
/// `externalLibraries` fields).
/// - repository: The parsed GIR repository containing namespace metadata,
/// `<c:include>`, `<include>`, and `<package>` values.
/// - Returns: A dictionary of relative path file content.
public static func generatePackageScaffolding(config: GenerationConfig, repository: Repository) -> [String: String] {
var files: [String: String] = [:]
let cName = "C\(config.library)"
let lowerName = config.library.lowercased()
// Extract namespace data from the repository
let ns = repository.namespaces.first { $0.name == config.library }
let sharedLibrary = ns?.cSharedLibrary ?? ""
// Parse shared-library to derive all link names
// shared-library format: "libgtk-4.so.1" or "libgio-2.0.so.0,libglib-2.0.so.0"
let libraries = sharedLibrary.split(separator: ",").map(String.init)
let allLinkNames = libraries.compactMap { extractLinkName(from: $0) }
let lowerLibName = config.library.lowercased()
// Pick the best primary link: prefer the one matching the library name
let linkName = allLinkNames.first { $0.lowercased().contains(lowerLibName) }
?? allLinkNames.first
?? lowerName
// Use <package> name as pkgConfig when available (it matches the system .pc file),
// otherwise fall back to the derived link name
let pkgConfigName = repository.packageName.isEmpty ? linkName : repository.packageName
// Derive umbrella header from repository <c:include>
let cHeader = repository.cHeaderPath
let umbrellaHeader = cHeader.isEmpty
? "#include <\(lowerName)/\(lowerName).h>"
: "#include <\(cHeader)>"
// Build external link names from GIR dependencies + config
var externalLinks: [String] = []
for linkName in repository.includedLibraryLinks {
if !externalLinks.contains(linkName) {
externalLinks.append(linkName)
}
}
for ext in config.externalLibraries {
let el = ext.lowercased()
if !externalLinks.contains(el) {
externalLinks.append(el)
}
}
// Package.swift
let packageSwift = """
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "\(config.library)",
platforms: [.macOS(.v14)],
products: [
.library(
name: "\(config.library)",
targets: ["\(config.library)"]
)
],
targets: [
.target(
name: "\(config.library)",
dependencies: ["\(cName)"],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency=complete"),
.defaultIsolation(MainActor.self),
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
]
),
.systemLibrary(
name: "\(cName)",
path: "Sources/\(cName)",
pkgConfig: "\(pkgConfigName)"
),
]
)
"""
files["Package.swift"] = packageSwift
// Module map: use the generated umbrella header which includes the system header
let linkLines = (allLinkNames + externalLinks).map { " link \"\($0)\"" }.joined(separator: "\n")
let moduleMap = """
module \(cName) [system] {
header "\(cName).h"
\(linkLines)
}
"""
files["Sources/\(cName)/module.modulemap"] = moduleMap
files["Sources/\(cName)/\(cName).h"] = umbrellaHeader + "\n"
return files
}
/// Extracts link name from a shared-library string like "libgtk-4.so.1" -> "gtk-4".
private static func extractLinkName(from sharedLib: String) -> String? {
guard sharedLib.hasPrefix("lib") else { return nil }
let withoutLib = String(sharedLib.dropFirst(3))
guard let soRange = withoutLib.range(of: ".so") else { return withoutLib }
return String(withoutLib[..<soRange.lowerBound])
}
}

View file

@ -0,0 +1,118 @@
import Foundation
extension CodeGenerator {
// MARK: - Signal Connection Generation
/// Generates a signal connection method using `g_signal_connect_data`.
///
/// The method takes a Swift closure and bridges it to a C function pointer
/// via `Unmanaged.passRetained`. The retain/release is managed by
/// `GClosureNotify` (the destroy handler passed to `g_signal_connect_data`).
///
/// For signals with parameters, the C callback receives them as
/// `UnsafeMutableRawPointer?` values which are extracted into Swift types.
///
/// - Parameters:
/// - signal: The GIR signal to generate.
/// - inhibit: If `true`, the handler returns `Bool` to control signal propagation.
/// - Returns: A signal connection method declaration as a string.
public static func generateSignalConnection(signal: Signal, inhibit: Bool = false) -> String {
let signalName = swiftifySignalName(signal.name)
let handlerReturnType = inhibit ? "Bool" : "Void"
let handlerArgs = signal.parameters.map { p -> String in
let swiftType = typeToSwift(p.type)
return "_: \(swiftType)"
}.joined(separator: ", ")
let detailClause = signal.isDetailed ? ", \"\(signal.name)\"" : ""
if signal.parameters.isEmpty {
let callbackReturn = inhibit ? " -> gboolean" : ""
let handlerCall = inhibit ? "let result = stored(); return result ? 1 : 0" : "stored()"
return """
public func connect\(signalName)(_ handler: @escaping () -> \(handlerReturnType)) -> Int {
let boxed = Unmanaged.passRetained(handler as AnyObject).toOpaque()
let callback: @convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?)\(callbackReturn) = { (_, data) in
let stored = Unmanaged<AnyObject>.fromOpaque(data!).takeUnretainedValue() as! () -> \(handlerReturnType)
\(handlerCall)
}
let destroy: @convention(c) (UnsafeMutableRawPointer?) -> Void = { Unmanaged<AnyObject>.fromOpaque($0!).release() }
return Int(g_signal_connect_data(pointer, "\(signal.name)", callback, boxed, destroy, 0))
}
"""
} else {
let rawPointerParams = signal.parameters.map { _ in "UnsafeMutableRawPointer?" }.joined(separator: ", ")
let rawPointerNames = signal.parameters.enumerated().map { "p\($0.offset)" }.joined(separator: ", ")
let extractionExprs = signal.parameters.enumerated().map { i, p in
cSignalParameterExtraction(index: i, type: p.type)
}.joined(separator: ", ")
let callbackReturn = inhibit ? " -> gboolean" : ""
let handlerCall = inhibit
? "let result = stored(\(extractionExprs)); return result ? 1 : 0"
: "stored(\(extractionExprs))"
return """
public func connect\(signalName)(_ handler: @escaping (\(handlerArgs)) -> \(handlerReturnType)) -> Int {
let boxed = Unmanaged.passRetained(handler as AnyObject).toOpaque()
let callback: @convention(c) (\(rawPointerParams), UnsafeMutableRawPointer?)\(callbackReturn) = { (\(rawPointerNames), data) in
let stored = Unmanaged<AnyObject>.fromOpaque(data!).takeUnretainedValue() as! (\(handlerArgs)) -> \(handlerReturnType)
\(handlerCall)
}
let destroy: @convention(c) (UnsafeMutableRawPointer?) -> Void = { Unmanaged<AnyObject>.fromOpaque($0!).release() }
return Int(g_signal_connect_data(pointer, "\(signal.name)"\(detailClause), callback, boxed, destroy, 0))
}
"""
}
}
/// Generates a Swift expression to extract a signal parameter value from an
/// `UnsafeMutableRawPointer?` received in the C callback trampoline.
///
/// - Parameters:
/// - index: The zero-based index of the signal parameter.
/// - type: The GIR type of the parameter.
/// - Returns: A Swift expression string that converts `p{index}` to the target type.
private static func cSignalParameterExtraction(index: Int, type: GIRType) -> String {
let p = "p\(index)"
switch type {
case .boolean:
return "Int(bitPattern: \(p)) != 0"
case .int8:
return "Int8(bitPattern: UInt8(bitPattern: Int8(truncatingIfNeeded: Int(bitPattern: \(p)))))"
case .int16:
return "Int16(bitPattern: UInt16(bitPattern: Int16(truncatingIfNeeded: Int(bitPattern: \(p)))))"
case .int32:
return "Int32(bitPattern: Int32(truncatingIfNeeded: Int(bitPattern: \(p))))"
case .int64:
return "\(p)!.load(as: Int64.self)"
case .uint8:
return "UInt8(bitPattern: UInt8(truncatingIfNeeded: Int(bitPattern: \(p))))"
case .uint16:
return "UInt16(bitPattern: UInt16(truncatingIfNeeded: Int(bitPattern: \(p))))"
case .uint32:
return "UInt32(bitPattern: UInt32(truncatingIfNeeded: Int(bitPattern: \(p))))"
case .uint64:
return "\(p)!.load(as: UInt64.self)"
case .float:
return "\(p)!.load(as: Float.self)"
case .double:
return "\(p)!.load(as: Double.self)"
case .string, .filename:
return "String(cString: \(p)!.assumingMemoryBound(to: CChar.self))"
case .typeRef(let name, _):
return "\(name)(pointer: \(p)!)"
case .pointer:
return "\(p)!"
case .optional(let inner):
if case .typeRef(let name, _) = inner {
return "\(p).map { \(name)(pointer: $0) }"
}
return cSignalParameterExtraction(index: index, type: inner)
default:
return "\(p)!"
}
}
}

View file

@ -48,12 +48,14 @@ public struct CodeGenerator {
"""
for ns in repository.namespaces where ns.name == config.library {
let classNames = Set(ns.classes.map { $0.name })
let shouldGenerateAll = analysis.generatedTypes.isEmpty
for cls in ns.classes {
let fullName = "\(ns.name).\(cls.name)"
if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue }
output += generateClass(cls, namespace: ns.name, analysis: analysis)
output += generateClass(cls, namespace: ns.name, analysis: analysis, classTypeNames: classNames)
}
for enm in ns.enumerations {
let fullName = "\(ns.name).\(enm.name)"
@ -78,12 +80,12 @@ public struct CodeGenerator {
for fn in ns.functions {
let fullName = "\(ns.name).\(fn.name)"
if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue }
output += generateGlobalFunction(fn, namespace: ns.name)
output += generateGlobalFunction(fn, namespace: ns.name, classTypeNames: classNames)
}
for rec in ns.records {
let fullName = "\(ns.name).\(rec.name)"
if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue }
output += generateRecord(rec, namespace: ns.name, analysis: analysis)
output += generateRecord(rec, namespace: ns.name, analysis: analysis, classTypeNames: classNames)
}
for cst in ns.constants {
let fullName = "\(ns.name).\(cst.name)"
@ -117,12 +119,14 @@ public struct CodeGenerator {
var bodies: [String: String] = [:]
for ns in repository.namespaces where ns.name == config.library {
let classNames = Set(ns.classes.map { $0.name })
let shouldGenerateAll = analysis.generatedTypes.isEmpty
for cls in ns.classes {
let fullName = "\(ns.name).\(cls.name)"
if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue }
bodies["\(cls.name).swift"] = generateClass(cls, namespace: ns.name, analysis: analysis)
bodies["\(cls.name).swift"] = generateClass(cls, namespace: ns.name, analysis: analysis, classTypeNames: classNames)
}
for enm in ns.enumerations {
let fullName = "\(ns.name).\(enm.name)"
@ -149,13 +153,13 @@ public struct CodeGenerator {
if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue }
let fnFilename = "\(Self.pascalCaseName(fn.name)).swift"
if !bodies.keys.contains(fnFilename) {
bodies[fnFilename] = generateGlobalFunction(fn, namespace: ns.name)
bodies[fnFilename] = generateGlobalFunction(fn, namespace: ns.name, classTypeNames: classNames)
}
}
for rec in ns.records {
let fullName = "\(ns.name).\(rec.name)"
if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue }
bodies["\(rec.name).swift"] = generateRecord(rec, namespace: ns.name, analysis: analysis)
bodies["\(rec.name).swift"] = generateRecord(rec, namespace: ns.name, analysis: analysis, classTypeNames: classNames)
}
for cst in ns.constants {
let fullName = "\(ns.name).\(cst.name)"
@ -192,117 +196,6 @@ public struct CodeGenerator {
return files
}
/// Generates the package scaffolding files (Package.swift, module map, umbrella header)
/// for a self-contained SwiftPM wrapper package.
///
/// Returns a dictionary mapping relative file paths (e.g. `"Package.swift"`,
/// `"Sources/CGtk/module.modulemap"`) to their textual content. The library
/// name in `config.library` determines the package name, the C interop target
/// name (prefixed with `C`). Link names and pkg-config names are derived from
/// the repository's namespace `shared-library` attribute, the umbrella header
/// from the repository's `<c:include>` element, and external library link flags
/// from `<include>` elements and `config.externalLibraries`.
///
/// - Parameters:
/// - config: The generation configuration (uses `library` and
/// `externalLibraries` fields).
/// - repository: The parsed GIR repository containing namespace metadata,
/// `<c:include>`, `<include>`, and `<package>` values.
/// - Returns: A dictionary of relative path file content.
public static func generatePackageScaffolding(config: GenerationConfig, repository: Repository) -> [String: String] {
var files: [String: String] = [:]
let cName = "C\(config.library)"
let lowerName = config.library.lowercased()
// Extract namespace data from the repository
let ns = repository.namespaces.first { $0.name == config.library }
let sharedLibrary = ns?.cSharedLibrary ?? ""
// Parse shared-library to derive all link names
// shared-library format: "libgtk-4.so.1" or "libgio-2.0.so.0,libglib-2.0.so.0"
let libraries = sharedLibrary.split(separator: ",").map(String.init)
let allLinkNames = libraries.compactMap { extractLinkName(from: $0) }
let lowerLibName = config.library.lowercased()
// Pick the best primary link: prefer the one matching the library name
let linkName = allLinkNames.first { $0.lowercased().contains(lowerLibName) }
?? allLinkNames.first
?? lowerName
let pkgConfigName = linkName
// Derive umbrella header from repository <c:include>
let cHeader = repository.cHeaderPath
let umbrellaHeader = cHeader.isEmpty
? "#include <\(lowerName)/\(lowerName).h>"
: "#include <\(cHeader)>"
// Build external link names from GIR dependencies + config
var externalLinks: [String] = []
for linkName in repository.includedLibraryLinks {
if !externalLinks.contains(linkName) {
externalLinks.append(linkName)
}
}
for ext in config.externalLibraries {
let el = ext.lowercased()
if !externalLinks.contains(el) {
externalLinks.append(el)
}
}
// Package.swift
let packageSwift = """
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "\(config.library)",
products: [
.library(
name: "\(config.library)",
targets: ["\(config.library)"]
)
],
targets: [
.target(
name: "\(config.library)",
dependencies: ["\(cName)"]
),
.systemLibrary(
name: "\(cName)",
path: "Sources/\(cName)",
pkgConfig: "\(pkgConfigName)"
),
]
)
"""
files["Package.swift"] = packageSwift
// Module map: include all shared libraries as links
let linkLines = (allLinkNames + externalLinks).map { " link \"\($0)\"" }.joined(separator: "\n")
let moduleMap = """
module \(cName) [system] {
umbrella "\(lowerName)"
\(linkLines)
}
"""
files["Sources/\(cName)/module.modulemap"] = moduleMap
files["Sources/\(cName)/\(cName).h"] = umbrellaHeader + "\n"
return files
}
/// Extracts link name from a shared-library string like "libgtk-4.so.1" -> "gtk-4".
private static func extractLinkName(from sharedLib: String) -> String? {
guard sharedLib.hasPrefix("lib") else { return nil }
let withoutLib = String(sharedLib.dropFirst(3))
guard let soRange = withoutLib.range(of: ".so") else { return withoutLib }
return String(withoutLib[..<soRange.lowerBound])
}
// MARK: - Class Generation
/// Generates a Swift class declaration from a GIR class definition.
@ -319,7 +212,7 @@ public struct CodeGenerator {
/// - namespace: The GIR namespace the class belongs to (e.g. `"Gtk"`).
/// - analysis: The analysis result containing per-type overrides.
/// - Returns: A Swift class declaration as a string.
private func generateClass(_ cls: Class, namespace: String, analysis: AnalysisResult) -> String {
private func generateClass(_ cls: Class, namespace: String, analysis: AnalysisResult, classTypeNames: Set<String> = []) -> String {
var swift = ""
let override = analysis.classOverrides["\(namespace).\(cls.name)"]
let concurrency = override?.concurrency ?? .none
@ -349,7 +242,7 @@ public struct CodeGenerator {
for ctor in cls.constructors {
swift += Self.formatDocComment(ctor.doc, indentation: 4)
swift += Self.generateConstructor(constructor: ctor, className: cls.name)
swift += Self.generateConstructor(constructor: ctor, className: cls.name, classTypeNames: classTypeNames)
}
for prop in cls.properties {
@ -374,7 +267,7 @@ public struct CodeGenerator {
let returnTypeStr = fn.returnType == .void ? "" : " -> \(Self.typeToSwift(fn.returnType))"
let returnStmt = fn.returnType == .void ? "" : "return "
let args = params.map { p -> String in
Self.cParameterExpression(name: Self.swiftifyParameterName(p.name), type: p.type)
Self.cParameterExpression(name: Self.swiftifyParameterName(p.name), type: p.type, classTypeNames: classTypeNames)
}.joined(separator: ", ")
let cCall = "\(fn.cIdentifier)(\(args))"
let wrappedReturn = Self.wrapCReturnValue(callExpression: cCall, returnType: fn.returnType)
@ -419,7 +312,7 @@ public struct CodeGenerator {
"\(Self.swiftifyParameterName(p.name)): \(Self.typeToSwift(p.type))"
}.joined(separator: ", ")
let args = params.map { p -> String in
Self.cParameterExpression(name: Self.swiftifyParameterName(p.name), type: p.type)
Self.cParameterExpression(name: Self.swiftifyParameterName(p.name), type: p.type, classTypeNames: classTypeNames)
}.joined(separator: ", ")
swift += """
\(methodVisibility) convenience init(\(methodName): String? = nil, \(paramList)) {
@ -436,7 +329,7 @@ public struct CodeGenerator {
}.joined(separator: ", ")
let returnTypeStr = method.returnType == .void ? "" : " -> \(Self.typeToSwift(method.returnType))"
let returnStmt = method.returnType == .void ? "" : "return "
let cCall = Self.generateCFunctionCall(method: method, instancePointer: "pointer")
let cCall = Self.generateCFunctionCall(method: method, instancePointer: "pointer", classTypeNames: classTypeNames)
swift += """
\(methodVisibility) func \(methodName)(\(paramList))\(returnTypeStr) {
@ -516,7 +409,7 @@ public struct CodeGenerator {
private func generateEnum(_ enm: Enumeration) -> String {
var swift = ""
swift += Self.formatDocComment(enm.doc)
swift += "public enum \(enm.name): Int {\n"
swift += "public enum \(enm.name): Int, Sendable {\n"
for member in enm.members {
let caseName = Self.swiftifyEnumCaseName(member.name)
let formattedValue = Self.formatNumericLiteral(member.value)
@ -539,7 +432,7 @@ public struct CodeGenerator {
private func generateBitfield(_ bf: Bitfield) -> String {
var swift = ""
swift += Self.formatDocComment(bf.doc)
swift += "public struct \(bf.name): OptionSet {\n"
swift += "public struct \(bf.name): OptionSet, Sendable {\n"
swift += " public let rawValue: Int\n"
swift += " public init(rawValue: Int) { self.rawValue = rawValue }\n\n"
for member in bf.members {
@ -606,14 +499,31 @@ public struct CodeGenerator {
private func generateCallback(_ cb: Callback) -> String {
var result = Self.formatDocComment(cb.doc)
let paramList = cb.parameters.map { p -> String in
let type = Self.typeToSwift(p.type)
return "\(Self.swiftifyParameterName(p.name)): \(type)"
let cType = Self.callbackParamType(p.type)
return "_ \(Self.swiftifyParameterName(p.name)): \(cType)"
}.joined(separator: ", ")
let returnType = cb.returnType == .void ? "Void" : Self.typeToSwift(cb.returnType)
result += "public typealias \(cb.name) = @convention(c) (\(paramList)) -> \(returnType)\n"
return result
}
/// Maps a GIR type to the corresponding Swift type for `@convention(c)` callback parameters.
/// GObject type references become `UnsafeMutableRawPointer` since C callbacks
/// pass raw pointers. All other types pass through `typeToSwift` unchanged.
private static func callbackParamType(_ type: GIRType) -> String {
switch type {
case .typeRef:
return "UnsafeMutableRawPointer"
case .optional(let inner):
if case .typeRef = inner {
return "UnsafeMutableRawPointer?"
}
return typeToSwift(type)
default:
return typeToSwift(type)
}
}
// MARK: - Global Function Generation
/// Generates a Swift free function from a GIR global function definition.
@ -627,7 +537,7 @@ public struct CodeGenerator {
/// - fn: The GIR global function to generate.
/// - namespace: The GIR namespace the function belongs to.
/// - Returns: A Swift function declaration as a string.
private func generateGlobalFunction(_ fn: GlobalFunction, namespace: String) -> String {
private func generateGlobalFunction(_ fn: GlobalFunction, namespace: String, classTypeNames: Set<String> = []) -> String {
let params = Self.nonVarargParameters(fn.parameters)
let paramList = params.map { p -> String in
"\(Self.swiftifyParameterName(p.name)): \(Self.typeToSwift(p.type))"
@ -636,7 +546,7 @@ public struct CodeGenerator {
let returnStmt = fn.returnType == .void ? "" : "return "
let args = params.map { p -> String in
Self.cParameterExpression(name: Self.swiftifyParameterName(p.name), type: p.type)
Self.cParameterExpression(name: Self.swiftifyParameterName(p.name), type: p.type, classTypeNames: classTypeNames)
}.joined(separator: ", ")
let cCall = "\(fn.cIdentifier)(\(args))"
let wrappedReturn = Self.wrapCReturnValue(callExpression: cCall, returnType: fn.returnType)
@ -648,21 +558,6 @@ public struct CodeGenerator {
return result
}
// MARK: - C Function Call Generation
/// Generates a Swift expression that calls the C function corresponding
/// to a GIR method, including return-type wrapping.
///
/// Builds the argument list from the method's parameters (replacing the
/// instance parameter with the given pointer expression), then wraps
/// the call result according to the return type (e.g. `String(cString:)`
/// for strings, `!= 0` for booleans).
///
/// - Parameters:
/// - method: The GIR method to generate a call for.
/// - instancePointer: The Swift expression for the instance pointer
/// (typically `"pointer"`).
/// - Returns: A Swift expression string for the C function call.
// MARK: - Property Accessor Generation
/// Generates a Swift computed property declaration for a GObject property
@ -808,121 +703,6 @@ public struct CodeGenerator {
}
}
// MARK: - Signal Connection Generation
/// Generates a signal connection method using `g_signal_connect_data`.
///
/// The method takes a Swift closure and bridges it to a C function pointer
/// via `Unmanaged.passRetained`. The retain/release is managed by
/// `GClosureNotify` (the destroy handler passed to `g_signal_connect_data`).
///
/// For signals with parameters, the C callback receives them as
/// `UnsafeMutableRawPointer?` values which are extracted into Swift types.
///
/// - Parameters:
/// - signal: The GIR signal to generate.
/// - inhibit: If `true`, the handler returns `Bool` to control signal propagation.
/// - Returns: A signal connection method declaration as a string.
public static func generateSignalConnection(signal: Signal, inhibit: Bool = false) -> String {
let signalName = swiftifySignalName(signal.name)
let handlerReturnType = inhibit ? "Bool" : "Void"
let handlerArgs = signal.parameters.map { p -> String in
let swiftType = typeToSwift(p.type)
return "_: \(swiftType)"
}.joined(separator: ", ")
let detailClause = signal.isDetailed ? ", \"\(signal.name)\"" : ""
if signal.parameters.isEmpty {
let callbackReturn = inhibit ? " -> gboolean" : ""
let handlerCall = inhibit ? "let result = stored(); return result ? 1 : 0" : "stored()"
return """
public func connect\(signalName)(_ handler: @escaping () -> \(handlerReturnType)) -> Int {
let boxed = Unmanaged.passRetained(handler as AnyObject).toOpaque()
let callback: @convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?)\(callbackReturn) = { (_, data) in
let stored = Unmanaged<AnyObject>.fromOpaque(data!).takeUnretainedValue() as! () -> \(handlerReturnType)
\(handlerCall)
}
let destroy: @convention(c) (UnsafeMutableRawPointer?) -> Void = { Unmanaged<AnyObject>.fromOpaque($0!).release() }
return Int(g_signal_connect_data(pointer, "\(signal.name)", callback, boxed, destroy, 0))
}
"""
} else {
let rawPointerParams = signal.parameters.map { _ in "UnsafeMutableRawPointer?" }.joined(separator: ", ")
let rawPointerNames = signal.parameters.enumerated().map { "p\($0.offset)" }.joined(separator: ", ")
let extractionExprs = signal.parameters.enumerated().map { i, p in
cSignalParameterExtraction(index: i, type: p.type)
}.joined(separator: ", ")
let callbackReturn = inhibit ? " -> gboolean" : ""
let handlerCall = inhibit
? "let result = stored(\(extractionExprs)); return result ? 1 : 0"
: "stored(\(extractionExprs))"
return """
public func connect\(signalName)(_ handler: @escaping (\(handlerArgs)) -> \(handlerReturnType)) -> Int {
let boxed = Unmanaged.passRetained(handler as AnyObject).toOpaque()
let callback: @convention(c) (\(rawPointerParams), UnsafeMutableRawPointer?)\(callbackReturn) = { (\(rawPointerNames), data) in
let stored = Unmanaged<AnyObject>.fromOpaque(data!).takeUnretainedValue() as! (\(handlerArgs)) -> \(handlerReturnType)
\(handlerCall)
}
let destroy: @convention(c) (UnsafeMutableRawPointer?) -> Void = { Unmanaged<AnyObject>.fromOpaque($0!).release() }
return Int(g_signal_connect_data(pointer, "\(signal.name)"\(detailClause), callback, boxed, destroy, 0))
}
"""
}
}
/// Generates a Swift expression to extract a signal parameter value from an
/// `UnsafeMutableRawPointer?` received in the C callback trampoline.
///
/// - Parameters:
/// - index: The zero-based index of the signal parameter.
/// - type: The GIR type of the parameter.
/// - Returns: A Swift expression string that converts `p{index}` to the target type.
private static func cSignalParameterExtraction(index: Int, type: GIRType) -> String {
let p = "p\(index)"
switch type {
case .boolean:
return "Int(bitPattern: \(p)) != 0"
case .int8:
return "Int8(bitPattern: UInt8(bitPattern: Int8(truncatingIfNeeded: Int(bitPattern: \(p)))))"
case .int16:
return "Int16(bitPattern: UInt16(bitPattern: Int16(truncatingIfNeeded: Int(bitPattern: \(p)))))"
case .int32:
return "Int32(bitPattern: Int32(truncatingIfNeeded: Int(bitPattern: \(p))))"
case .int64:
return "\(p)!.load(as: Int64.self)"
case .uint8:
return "UInt8(bitPattern: UInt8(truncatingIfNeeded: Int(bitPattern: \(p))))"
case .uint16:
return "UInt16(bitPattern: UInt16(truncatingIfNeeded: Int(bitPattern: \(p))))"
case .uint32:
return "UInt32(bitPattern: UInt32(truncatingIfNeeded: Int(bitPattern: \(p))))"
case .uint64:
return "\(p)!.load(as: UInt64.self)"
case .float:
return "\(p)!.load(as: Float.self)"
case .double:
return "\(p)!.load(as: Double.self)"
case .string, .filename:
return "String(cString: \(p)!.assumingMemoryBound(to: CChar.self))"
case .typeRef(let name, _):
return "\(name)(pointer: \(p)!)"
case .pointer:
return "\(p)!"
case .optional(let inner):
if case .typeRef(let name, _) = inner {
return "\(p).map { \(name)(pointer: $0) }"
}
return cSignalParameterExtraction(index: index, type: inner)
default:
return "\(p)!"
}
}
// MARK: - Constructor Generation
/// Generates a Swift convenience initializer for a GObject constructor.
@ -937,13 +717,13 @@ public struct CodeGenerator {
/// - constructor: The GIR constructor definition.
/// - className: The Swift class name.
/// - Returns: A convenience initializer declaration as a string.
public static func generateConstructor(constructor: Constructor, className: String) -> String {
public static func generateConstructor(constructor: Constructor, className: String, classTypeNames: Set<String>? = nil) -> String {
let params = Self.nonVarargParameters(constructor.parameters)
let paramList = params.map { p -> String in
"\(Self.swiftifyParameterName(p.name)): \(Self.typeToSwift(p.type))"
}.joined(separator: ", ")
let args = params.map { p -> String in
Self.cParameterExpression(name: Self.swiftifyParameterName(p.name), type: p.type)
Self.cParameterExpression(name: Self.swiftifyParameterName(p.name), type: p.type, classTypeNames: classTypeNames)
}.joined(separator: ", ")
return """
@ -959,11 +739,11 @@ public struct CodeGenerator {
// MARK: - C Function Call Generation
public static func generateCFunctionCall(method: Method, instancePointer: String) -> String {
public static func generateCFunctionCall(method: Method, instancePointer: String, classTypeNames: Set<String>? = nil) -> String {
let sortedParams = Self.nonVarargParameters(method.parameters)
let args = sortedParams.map { p -> String in
if p.isInstanceParameter { return instancePointer }
return cParameterExpression(name: Self.swiftifyParameterName(p.name), type: p.type)
return cParameterExpression(name: Self.swiftifyParameterName(p.name), type: p.type, classTypeNames: classTypeNames)
}.joined(separator: ", ")
let cFuncCall = "\(method.cIdentifier)(\(args))"
@ -973,20 +753,39 @@ public struct CodeGenerator {
/// Returns the Swift argument expression for a parameter of the given type.
///
/// Extracts `.pointer` from GObject wrapper types and handles optional
/// GObject pointers with optional chaining. All other types pass through
/// as-is (Swift C interop handles the conversion natively).
/// GObject pointers with optional chaining. All other types (enums,
/// bitfields, records) pass through as-is since they are value types
/// without a `.pointer` property.
///
/// - Parameters:
/// - name: The Swift parameter name.
/// - type: The GIR type of the parameter.
/// - classTypeNames: The set of class type names that have a `.pointer`
/// property. Types NOT in this set are passed directly.
/// - Returns: A Swift expression string for the C function argument.
private static func cParameterExpression(name: String, type: GIRType) -> String {
private static func cParameterExpression(name: String, type: GIRType, classTypeNames: Set<String>? = nil) -> String {
let usesPointer: Bool
switch type {
case .typeRef(let refName, _):
// When classTypeNames is nil, default to .pointer (backward compatible).
// When set, only use .pointer for types in the set.
usesPointer = classTypeNames.map { $0.contains(refName) } ?? true
case .optional(let inner):
if case .typeRef(let refName, _) = inner {
usesPointer = classTypeNames.map { $0.contains(refName) } ?? true
} else {
usesPointer = false
}
default:
usesPointer = false
}
switch type {
case .typeRef:
return "\(name).pointer"
return usesPointer ? "\(name).pointer" : name
case .optional(let inner):
if case .typeRef = inner {
return "\(name)?.pointer"
return usesPointer ? "\(name)?.pointer" : name
}
return name
default:
@ -1082,7 +881,7 @@ public struct CodeGenerator {
///
/// - Parameter type: The GIR type to map.
/// - Returns: The Swift type name as a string.
private static func typeToSwift(_ type: GIRType) -> String {
static func typeToSwift(_ type: GIRType) -> String {
switch type {
case .void: return "Void"
case .boolean: return "Bool"
@ -1160,7 +959,7 @@ public struct CodeGenerator {
///
/// - Parameter name: The kebab-case GIR signal name.
/// - Returns: A PascalCase Swift method name component.
private static func swiftifySignalName(_ name: String) -> String {
static func swiftifySignalName(_ name: String) -> String {
name.split(separator: "-").map { $0.capitalized }.joined()
}
@ -1219,7 +1018,7 @@ public struct CodeGenerator {
/// - namespace: The GIR namespace the record belongs to.
/// - analysis: The analysis result containing per-type overrides.
/// - Returns: A Swift struct declaration as a string.
private func generateRecord(_ rec: Record, namespace: String, analysis: AnalysisResult) -> String {
private func generateRecord(_ rec: Record, namespace: String, analysis: AnalysisResult, classTypeNames: Set<String> = []) -> String {
var swift = ""
swift += Self.formatDocComment(rec.doc)
@ -1227,7 +1026,7 @@ public struct CodeGenerator {
let concurrency = override?.concurrency ?? .none
if concurrency == .mainActor { swift += "@MainActor " }
swift += "public struct \(rec.name) {\n"
swift += "public struct \(rec.name): Sendable {\n"
for field in rec.fields {
swift += Self.formatDocComment(field.doc, indentation: 4)
@ -1243,7 +1042,7 @@ public struct CodeGenerator {
let returnTypeStr = method.returnType == .void ? "" : " -> \(Self.typeToSwift(method.returnType))"
let returnStmt = method.returnType == .void ? "" : "return "
let funcName = Self.swiftifyMethodName(method.name)
let cCall = Self.generateCFunctionCall(method: method, instancePointer: "&self")
let cCall = Self.generateCFunctionCall(method: method, instancePointer: "&self", classTypeNames: classTypeNames)
swift += " public mutating func \(funcName)(\(paramList))\(returnTypeStr) {\n"
swift += " \(returnStmt)\(cCall)\n"
swift += " }\n\n"
@ -1260,7 +1059,16 @@ public struct CodeGenerator {
private func generateConstant(_ cst: Constant) -> String {
var swift = Self.formatDocComment(cst.doc)
let typeName = Self.typeToSwift(cst.type)
swift += "public let \(Self.swiftifyPropertyName(cst.name)): \(typeName) = \(cst.value)\n"
let name = Self.swiftifyPropertyName(cst.name)
let escapedName = Self.reservedKeywords.contains(name) ? "`\(name)`" : name
let formattedValue: String
switch cst.type {
case .string, .filename:
formattedValue = "\"\(cst.value)\""
default:
formattedValue = cst.value
}
swift += "public let \(escapedName): \(typeName) = \(formattedValue)\n"
return swift
}