1
0
Fork 0
gobject-generator/Sources/SwiftGtkGenCore/CodeGen.swift
Brendan Szymanski 1330f2bd25 Initial commit: SwiftGtkGen GIR-to-Swift binding generator
Extracted from gtk-swift monorepo. Previous history
contains 38 commits across the full Phase 1 development
including GIR XML parsing, IR model, analysis engine,
code generation, CLI, plugins, and documentation.
2026-07-01 03:27:41 -04:00

1012 lines
43 KiB
Swift

import Foundation
/// Errors that can occur during Swift source code generation.
public enum CodeGenError: Error, Equatable {
/// Generation failed with a descriptive message.
case generationFailed(String)
}
/// Generates Swift source code from a parsed GIR repository and analysis results.
///
/// Takes the intermediate representation produced by the XML parser, filtered
/// through an ``Analyzer``/``AnalysisResult``, and emits Swift source text
/// containing class wrappers (with GObject reference counting), enumerations,
/// and option sets for bitfields. Generated output targets a single Swift file
/// per invocation.
public struct CodeGenerator {
/// The generation configuration controlling output behavior.
public let config: GenerationConfig
/// Creates a new code generator with the given configuration.
/// - Parameter config: The generation configuration.
public init(config: GenerationConfig) {
self.config = config
}
/// Generates Swift source code for the specified repository.
///
/// Iterates over all namespaces, classes, enumerations, and bitfields in
/// the repository, producing Swift declarations only for types included
/// in the analysis result. Generated classes include pointer-based storage
/// with `g_object_ref_sink`/`g_object_unref` lifetime management, computed
/// property accessors, method stubs, and signal connection methods.
///
/// - Parameters:
/// - repository: The parsed GIR repository to generate code from.
/// - analysis: The analysis results specifying which types to generate
/// and any per-type overrides.
/// - Returns: A string containing the generated Swift source code.
/// - Throws: `CodeGenError` if generation fails.
public func generate(repository: Repository, analysis: AnalysisResult) throws -> String {
var output = """
// Generated by SwiftGtkGen. DO NOT EDIT.
// Source: \(config.library) \(config.version)
import C\(config.library)
import Foundation
"""
for ns in repository.namespaces where ns.name == config.library {
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)
}
for enm in ns.enumerations {
let fullName = "\(ns.name).\(enm.name)"
if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue }
output += generateEnum(enm)
}
for bf in ns.bitfields {
let fullName = "\(ns.name).\(bf.name)"
if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue }
output += generateBitfield(bf)
}
for iface in ns.interfaces {
let fullName = "\(ns.name).\(iface.name)"
if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue }
output += generateInterface(iface)
}
for cb in ns.callbacks {
let fullName = "\(ns.name).\(cb.name)"
if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue }
output += generateCallback(cb)
}
for fn in ns.functions {
let fullName = "\(ns.name).\(fn.name)"
if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue }
output += generateGlobalFunction(fn, namespace: ns.name)
}
}
return output
}
/// Generates Swift source code split into one file per type.
///
/// Returns a dictionary mapping filenames (e.g. `"Widget.swift"`) to their
/// generated Swift source code. Each file includes a standard header with
/// `import Foundation` and the generated C interop import, followed by the
/// declaration for that single type. Only types present in the analysis
/// result are included.
///
/// - Parameters:
/// - repository: The parsed GIR repository to generate code from.
/// - analysis: The analysis results specifying which types to generate.
/// - Returns: A dictionary of filename to generated source code.
/// - Throws: `CodeGenError` if generation fails.
public func generateFiles(repository: Repository, analysis: AnalysisResult) throws -> [String: String] {
var bodies: [String: String] = [:]
for ns in repository.namespaces where ns.name == config.library {
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)
}
for enm in ns.enumerations {
let fullName = "\(ns.name).\(enm.name)"
if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue }
bodies["\(enm.name).swift"] = generateEnum(enm)
}
for bf in ns.bitfields {
let fullName = "\(ns.name).\(bf.name)"
if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue }
bodies["\(bf.name).swift"] = generateBitfield(bf)
}
for iface in ns.interfaces {
let fullName = "\(ns.name).\(iface.name)"
if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue }
bodies["\(iface.name).swift"] = generateInterface(iface)
}
for cb in ns.callbacks {
let fullName = "\(ns.name).\(cb.name)"
if !shouldGenerateAll && !analysis.generatedTypes.contains(fullName) { continue }
bodies["\(cb.name).swift"] = generateCallback(cb)
}
for fn in ns.functions {
let fullName = "\(ns.name).\(fn.name)"
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)
}
}
}
let header = """
// Generated by SwiftGtkGen. DO NOT EDIT.
// Source: \(config.library) \(config.version)
import C\(config.library)
import Foundation
""" + "\n"
var files: [String: String] = [:]
for (name, body) in bodies {
files[name] = header + body
}
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.
///
/// Produces a `final class` or `open class` (depending on overrides) with:
/// - An `UnsafeMutableRawPointer` backing field and `init(pointer:)` that
/// calls `g_object_ref_sink` / `g_object_unref` in `deinit`
/// - Computed properties for readable/writable GObject properties
/// - Method stubs for each GIR method
/// - Signal connection methods for each GIR signal
///
/// - Parameters:
/// - cls: The GIR class to generate.
/// - 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 {
var swift = ""
let override = analysis.classOverrides["\(namespace).\(cls.name)"]
swift += Self.formatDocComment(cls.doc)
let isFinal = override?.finalType ?? true
let classKeyword = isFinal ? "final class" : "open class"
let parentClause = cls.parent.map { ": \($0)" } ?? ""
swift += "public \(classKeyword) \(cls.name)\(parentClause) {\n"
swift += """
let pointer: UnsafeMutableRawPointer
public init(pointer: UnsafeMutableRawPointer) {
g_object_ref_sink(pointer)
self.pointer = pointer
}
deinit {
g_object_unref(pointer)
}
"""
for ctor in cls.constructors {
swift += Self.formatDocComment(ctor.doc, indentation: 4)
swift += Self.generateConstructor(constructor: ctor, className: cls.name)
}
for prop in cls.properties {
swift += Self.formatDocComment(prop.doc, indentation: 4)
swift += Self.generatePropertyAccessor(property: prop)
}
// Class-level functions (static methods)
for fn in cls.functions {
swift += Self.formatDocComment(fn.doc, indentation: 4)
let params = Self.nonVarargParameters(fn.parameters)
let paramList = params.map { p -> String in
let type = Self.typeToSwift(p.type)
return "\(Self.swiftifyParameterName(p.name)): \(type)"
}.joined(separator: ", ")
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)
}.joined(separator: ", ")
let cCall = "\(fn.cIdentifier)(\(args))"
let wrappedReturn = Self.wrapCReturnValue(callExpression: cCall, returnType: fn.returnType)
swift += """
public static func \(Self.swiftifyMethodName(fn.name))(\(paramList))\(returnTypeStr) {
\(returnStmt)\(wrappedReturn)
}
"""
}
for method in cls.methods {
swift += Self.formatDocComment(method.doc, indentation: 4)
let params = Self.nonVarargParameters(method.parameters.filter { !$0.isInstanceParameter })
let paramList = params.map { p -> String in
"\(Self.swiftifyParameterName(p.name)): \(Self.typeToSwift(p.type))"
}.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")
swift += """
public func \(Self.swiftifyMethodName(method.name))(\(paramList))\(returnTypeStr) {
\(returnStmt)\(cCall)
}
"""
}
for signal in cls.signals {
swift += Self.formatDocComment(signal.doc, indentation: 4)
swift += Self.generateSignalConnection(signal: signal)
}
while swift.hasSuffix("\n") {
swift = String(swift.dropLast())
}
swift += "\n}\n"
return swift
}
// MARK: - Enum Generation
/// Generates a Swift `enum` with `Int` raw values from a GIR enumeration.
///
/// Each GIR member is mapped to a camelCase Swift case. The enum adopts
/// `Int` as its raw value type, matching the underlying C enum layout.
///
/// - Parameter enm: The GIR enumeration to generate.
/// - Returns: A Swift enum declaration as a string.
private func generateEnum(_ enm: Enumeration) -> String {
var swift = ""
swift += Self.formatDocComment(enm.doc)
swift += "public enum \(enm.name): Int {\n"
for member in enm.members {
let caseName = Self.swiftifyEnumCaseName(member.name)
let formattedValue = Self.formatNumericLiteral(member.value)
swift += " case \(caseName) = \(formattedValue)\n"
}
while swift.hasSuffix("\n") { swift = String(swift.dropLast()) }
swift += "\n}\n"
return swift
}
// MARK: - Bitfield Generation
/// Generates a Swift `OptionSet` conformance from a GIR bitfield definition.
///
/// Produces a struct with an `Int` raw value and static constants using
/// bit-shifted literals (`1 << N`), matching the C bitmask semantics.
///
/// - Parameter bf: The GIR bitfield to generate.
/// - Returns: A Swift `OptionSet` struct declaration as a string.
private func generateBitfield(_ bf: Bitfield) -> String {
var swift = ""
swift += Self.formatDocComment(bf.doc)
swift += "public struct \(bf.name): OptionSet {\n"
swift += " public let rawValue: Int\n"
swift += " public init(rawValue: Int) { self.rawValue = rawValue }\n\n"
for member in bf.members {
let caseName = Self.swiftifyEnumCaseName(member.name)
let formattedValue = Self.formatNumericLiteral(member.value)
swift += " public static let \(caseName) = \(bf.name)(rawValue: 1 << \(formattedValue))\n"
}
while swift.hasSuffix("\n") { swift = String(swift.dropLast()) }
swift += "\n}\n"
return swift
}
// MARK: - Interface Generation
/// Generates a Swift protocol declaration from a GIR interface definition.
///
/// Produces a `protocol` with method requirements (excluding the instance
/// parameter) and computed property requirements for readable/writable
/// properties. Signals are not included since protocol-based signal
/// connection is not directly expressible.
///
/// - Parameter iface: The GIR interface to generate.
/// - Returns: A Swift protocol declaration as a string.
private func generateInterface(_ iface: Interface) -> String {
var swift = ""
swift += Self.formatDocComment(iface.doc)
swift += "public protocol \(iface.name) {\n"
for method in iface.methods {
let params = Self.nonVarargParameters(method.parameters.filter { !$0.isInstanceParameter })
let paramList = params.map { p -> String in
"\(Self.swiftifyParameterName(p.name)): \(Self.typeToSwift(p.type))"
}.joined(separator: ", ")
let returnTypeStr = method.returnType == .void ? "" : " -> \(Self.typeToSwift(method.returnType))"
swift += " func \(Self.swiftifyMethodName(method.name))(\(paramList))\(returnTypeStr)\n"
}
for prop in iface.properties {
let propName = Self.swiftifyPropertyName(prop.name)
let swiftType = Self.typeToSwift(prop.type)
if prop.isReadable && prop.isWritable {
swift += " var \(propName): \(swiftType) { get set }\n"
} else if prop.isReadable {
swift += " var \(propName): \(swiftType) { get }\n"
}
}
while swift.hasSuffix("\n") { swift = String(swift.dropLast()) }
swift += "\n}\n"
return swift
}
// MARK: - Callback Generation
/// Generates a Swift `typealias` with `@convention(c)` from a GIR callback
/// definition.
///
/// Produces a public typealias suitable for use as a C function pointer in
/// the generated GObject wrapper APIs. Each callback parameter is mapped
/// to its Swift type, and the return type is preserved.
///
/// - Parameter cb: The GIR callback to generate.
/// - Returns: A Swift typealias declaration as a string.
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)"
}.joined(separator: ", ")
let returnType = cb.returnType == .void ? "Void" : Self.typeToSwift(cb.returnType)
result += "public typealias \(cb.name) = @convention(c) (\(paramList)) -> \(returnType)\n"
return result
}
// MARK: - Global Function Generation
/// Generates a Swift free function from a GIR global function definition.
///
/// Produces a `public func` that wraps the underlying C function call,
/// including parameter passing (extracting `.pointer` from GObject types)
/// and return value conversion (e.g. `String(cString:)` for strings,
/// `!= 0` for booleans).
///
/// - Parameters:
/// - 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 {
let params = Self.nonVarargParameters(fn.parameters)
let paramList = params.map { p -> String in
"\(Self.swiftifyParameterName(p.name)): \(Self.typeToSwift(p.type))"
}.joined(separator: ", ")
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)
}.joined(separator: ", ")
let cCall = "\(fn.cIdentifier)(\(args))"
let wrappedReturn = Self.wrapCReturnValue(callExpression: cCall, returnType: fn.returnType)
var result = Self.formatDocComment(fn.doc)
result += "public func \(Self.swiftifyMethodName(fn.name))(\(paramList))\(returnTypeStr) {\n"
result += " \(returnStmt)\(wrappedReturn)\n"
result += "}\n"
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
/// using the `GValue` API (`g_object_get_property`/`g_object_set_property`).
///
/// Construct-only properties return an empty string since they are handled
/// via constructor parameters rather than runtime setters.
///
/// - Parameter property: The GIR property to generate an accessor for.
/// - Returns: A Swift computed property declaration string, or empty if
/// the property is construct-only.
public static func generatePropertyAccessor(property: Property) -> String {
guard !property.isConstructOnly else { return "" }
let propName = swiftifyPropertyName(property.name)
let swiftType = typeToSwift(property.type)
let cTypeName = gValueTypeName(for: property.type)
let getterFunc = gValueGetterFunc(for: property.type)
let setterFunc = gValueSetterFunc(for: property.type)
let girPropName = property.name
var swift = ""
if property.isReadable && property.isWritable {
swift += """
public var \(propName): \(swiftType) {
get {
var value = GValue()
g_value_init(&value, \(cTypeName))
g_object_get_property(pointer, "\(girPropName)", &value)
let result = \(getterFunc)(&value)
g_value_unset(&value)
return result
}
set {
var value = GValue()
g_value_init(&value, \(cTypeName))
\(setterFunc)(&value, newValue)
g_object_set_property(pointer, "\(girPropName)", &value)
g_value_unset(&value)
}
}
"""
} else if property.isReadable {
swift += """
public var \(propName): \(swiftType) {
var value = GValue()
g_value_init(&value, \(cTypeName))
g_object_get_property(pointer, "\(girPropName)", &value)
let result = \(getterFunc)(&value)
g_value_unset(&value)
return result
}
"""
} else if property.isWritable {
swift += """
public var \(propName): \(swiftType) {
set {
var value = GValue()
g_value_init(&value, \(cTypeName))
\(setterFunc)(&value, newValue)
g_object_set_property(pointer, "\(girPropName)", &value)
g_value_unset(&value)
}
}
"""
}
return swift
}
/// Returns the GType name constant for the given GIR type.
private static func gValueTypeName(for type: GIRType) -> String {
switch type {
case .boolean: return "G_TYPE_BOOLEAN"
case .int8: return "G_TYPE_INT8"
case .int16: return "G_TYPE_INT16"
case .int32: return "G_TYPE_INT"
case .int64: return "G_TYPE_INT64"
case .uint8: return "G_TYPE_UINT8"
case .uint16: return "G_TYPE_UINT16"
case .uint32: return "G_TYPE_UINT"
case .uint64: return "G_TYPE_UINT64"
case .float: return "G_TYPE_FLOAT"
case .double: return "G_TYPE_DOUBLE"
case .string, .filename: return "G_TYPE_STRING"
case .typeRef: return "G_TYPE_OBJECT"
case .pointer: return "G_TYPE_POINTER"
case .array: return "G_TYPE_ARRAY"
case .cArray: return "G_TYPE_ARRAY"
case .void: return "G_TYPE_NONE"
case .optional: return "G_TYPE_NONE"
}
}
/// Returns the GValue getter function name for the given GIR type.
private static func gValueGetterFunc(for type: GIRType) -> String {
switch type {
case .boolean: return "g_value_get_boolean"
case .int8: return "g_value_get_schar"
case .int16: return "g_value_get_int16"
case .int32: return "g_value_get_int"
case .int64: return "g_value_get_int64"
case .uint8: return "g_value_get_uchar"
case .uint16: return "g_value_get_uint16"
case .uint32: return "g_value_get_uint"
case .uint64: return "g_value_get_uint64"
case .float: return "g_value_get_float"
case .double: return "g_value_get_double"
case .string, .filename: return "g_value_get_string"
case .typeRef: return "g_value_get_object"
case .pointer: return "g_value_get_pointer"
case .array: return "g_value_get_boxed"
case .cArray: return "g_value_get_boxed"
default: return "g_value_get_pointer"
}
}
/// Returns the GValue setter function name for the given GIR type.
private static func gValueSetterFunc(for type: GIRType) -> String {
switch type {
case .boolean: return "g_value_set_boolean"
case .int8: return "g_value_set_schar"
case .int16: return "g_value_set_int16"
case .int32: return "g_value_set_int"
case .int64: return "g_value_set_int64"
case .uint8: return "g_value_set_uchar"
case .uint16: return "g_value_set_uint16"
case .uint32: return "g_value_set_uint"
case .uint64: return "g_value_set_uint64"
case .float: return "g_value_set_float"
case .double: return "g_value_set_double"
case .string, .filename: return "g_value_set_string"
case .typeRef: return "g_value_set_object"
case .pointer: return "g_value_set_pointer"
default: return "g_value_set_pointer"
}
}
// 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 zero parameters, a complete bridging trampoline is
/// generated. For signals with parameters, the generated code uses
/// `g_signal_connect_data` with a nil callback, noting manual implementation.
///
/// - Parameter signal: The GIR signal to generate.
/// - Returns: A signal connection method declaration as a string.
public static func generateSignalConnection(signal: Signal) -> String {
let signalName = swiftifySignalName(signal.name)
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 {
return """
public func connect\(signalName)(_ handler: @escaping () -> Void) -> Int {
let boxed = Unmanaged.passRetained(handler as AnyObject).toOpaque()
let callback: @convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> Void = { (_, data) in
let stored = Unmanaged<AnyObject>.fromOpaque(data!).takeUnretainedValue() as! () -> Void
stored()
}
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 {
return """
public func connect\(signalName)(_ handler: @escaping (\(handlerArgs)) -> Void) -> Int {
// Manual: Signal connection with parameters needs manual implementation
return Int(g_signal_connect_data(pointer, "\(signal.name)"\(detailClause), nil, nil, nil, 0))
}
"""
}
}
// MARK: - Constructor Generation
/// Generates a Swift convenience initializer for a GObject constructor.
///
/// Calls the C constructor function (e.g. `gtk_button_new_with_label(label)`)
/// and handles the floating reference pattern by calling `g_object_ref_sink`
/// on the returned pointer before passing it to `self.init(pointer:)`.
/// This is correct for all `GtkWidget` subclasses and is harmless for
/// non-floating objects (where `g_object_ref_sink` simply refs once).
///
/// - Parameters:
/// - 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 {
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)
}.joined(separator: ", ")
return """
public convenience init(\(paramList)) {
let ptr = \(constructor.cIdentifier)(\(args))
g_object_ref_sink(ptr)
self.init(pointer: ptr!)
}
"""
}
// MARK: - C Function Call Generation
public static func generateCFunctionCall(method: Method, instancePointer: String) -> 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)
}.joined(separator: ", ")
let cFuncCall = "\(method.cIdentifier)(\(args))"
return wrapCReturnValue(callExpression: cFuncCall, returnType: method.returnType)
}
/// 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).
///
/// - Parameters:
/// - name: The Swift parameter name.
/// - type: The GIR type of the parameter.
/// - Returns: A Swift expression string for the C function argument.
private static func cParameterExpression(name: String, type: GIRType) -> String {
switch type {
case .typeRef:
return "\(name).pointer"
case .optional(let inner):
if case .typeRef = inner {
return "\(name)?.pointer"
}
return name
default:
return name
}
}
/// Wraps a C function call expression to convert its return value to the
/// corresponding Swift type.
///
/// - `.void` returns the bare call expression (no `return` statement).
/// - `.boolean` compares the result to `0` via `cFunc() != 0`.
/// - `.string` / `.filename` wraps with `String(cString:)`.
/// - `.typeRef` wraps in the Swift wrapper type's initializer.
/// - `.optional(.typeRef)` uses `Optional.map` to wrap non-nil results.
/// - All other types pass through directly.
///
/// - Parameters:
/// - callExpression: The raw C function call expression.
/// - returnType: The GIR return type.
/// - Returns: A Swift expression string with appropriate type wrapping.
private static func wrapCReturnValue(callExpression: String, returnType: GIRType) -> String {
switch returnType {
case .void:
return callExpression
case .boolean:
return "(\(callExpression) != 0)"
case .string, .filename:
return "String(cString: \(callExpression))"
case .typeRef(let name, _):
return "\(name)(pointer: \(callExpression))"
case .optional(let inner):
if case .typeRef(let name, _) = inner {
return "\(callExpression).map { \(name)(pointer: $0) }"
}
if case .string = inner {
return "\(callExpression).map { String(cString: $0) }"
}
if case .filename = inner {
return "\(callExpression).map { String(cString: $0) }"
}
return callExpression
default:
return callExpression
}
}
// MARK: - Documentation Comment Formatting
/// Formats a documentation string as DocC-compatible `///` comments.
/// - Parameter doc: The raw documentation text from the GIR file.
/// - Returns: A string of `///`-prefixed lines, or empty string if nil/empty.
private static func formatDocComment(_ doc: String?, indentation: Int = 0) -> String {
guard let doc = doc, !doc.isEmpty else { return "" }
let indent = String(repeating: " ", count: indentation)
let lines = doc.split(separator: "\n", omittingEmptySubsequences: false)
var result = ""
for line in lines {
let trimmed = line.trimmingCharacters(in: .whitespaces)
if trimmed.isEmpty {
result += "\(indent)///\n"
} else {
result += "\(indent)/// \(trimmed)\n"
}
}
return result
}
// MARK: - Formatting Helpers
/// Formats a numeric literal string with underscore separators for readability.
/// For example, `"4294967295"` becomes `"4_294_967_295"`.
private static func formatNumericLiteral(_ value: String) -> String {
guard value.count > 4, let _ = Int(value) else { return value }
var result = ""
var remaining = value
while remaining.count > 3 {
let chunk = remaining.suffix(3)
remaining = String(remaining.dropLast(3))
result = "_\(chunk)" + result
}
return remaining + result
}
// MARK: - Naming Helpers
/// Maps a GIR type to its corresponding Swift type name string.
///
/// Handles primitive types (`boolean` `Bool`, `int32` `Int32`, etc.),
/// type references (looked up by name), and compound types (arrays,
/// C-style arrays, optionals). Pointers and filenames map to
/// `UnsafeMutableRawPointer` and `String` respectively.
///
/// - Parameter type: The GIR type to map.
/// - Returns: The Swift type name as a string.
private static func typeToSwift(_ type: GIRType) -> String {
switch type {
case .void: return "Void"
case .boolean: return "Bool"
case .int8: return "Int8"
case .int16: return "Int16"
case .int32: return "Int32"
case .int64: return "Int64"
case .uint8: return "UInt8"
case .uint16: return "UInt16"
case .uint32: return "UInt32"
case .uint64: return "UInt64"
case .float: return "Float"
case .double: return "Double"
case .string: return "String"
case .filename: return "String"
case .pointer: return "UnsafeMutableRawPointer"
case .typeRef(let name, _): return name
case .array(let inner): return "[\(typeToSwift(inner))]"
case .cArray(let inner): return "UnsafeBufferPointer<\(typeToSwift(inner))>"
case .optional(let inner): return "\(typeToSwift(inner))?"
}
}
/// Converts a snake_case GIR name to a camelCase Swift property name.
///
/// Splits on underscores and lowercases the first part while capitalizing
/// each subsequent part. For example, `"current_page"` becomes
/// `"currentPage"`.
///
/// - Parameter name: The snake_case GIR property name.
/// - Returns: A camelCase Swift property name.
private static func swiftifyPropertyName(_ name: String) -> String {
name.split { $0 == "_" || $0 == "-" }.enumerated().map { i, part in
i == 0 ? String(part).lowercased() : String(part).capitalized
}.joined()
}
/// The set of Swift reserved keywords that cannot be used as identifiers
/// without backtick escaping.
private static let reservedKeywords: Set<String> = [
"self", "type", "class", "default", "in", "for", "repeat", "while",
"switch", "case", "break", "continue", "return", "if", "else",
"guard", "defer", "do", "try", "throw", "catch", "import", "let",
"var", "func", "static", "struct", "enum", "protocol", "extension",
"init", "deinit", "subscript", "where", "operator", "Protocol",
"rethrows", "associatedtype", "precedencegroup",
"true", "false", "nil", "Self", "Type",
"private", "fileprivate", "internal", "public", "open",
"is", "as", "async", "await", "nonisolated", "throws",
]
/// Converts a GIR parameter name to a valid Swift identifier.
///
/// Applies camelCase conversion (via ``swiftifyPropertyName(_:)``) and
/// backtick-escapes the result if it collides with a Swift reserved
/// keyword such as `self`, `class`, `default`, or `return`.
///
/// - Parameter name: The raw GIR parameter name.
/// - Returns: A valid Swift parameter label, backtick-escaped if needed.
private static func swiftifyParameterName(_ name: String) -> String {
let swiftName = swiftifyPropertyName(name)
return reservedKeywords.contains(swiftName) ? "`\(swiftName)`" : swiftName
}
private static func swiftifyMethodName(_ name: String) -> String {
let swiftName = Self.swiftifyPropertyName(name)
return reservedKeywords.contains(swiftName) ? "`\(swiftName)`" : swiftName
}
/// Converts a GIR signal name (kebab-case) to a PascalCase Swift identifier.
///
/// Splits on hyphens and capitalizes each component. For example,
/// `"page-changed"` becomes `"PageChanged"`, suitable for use in
/// `connectPageChanged(_:)`.
///
/// - Parameter name: The kebab-case GIR signal name.
/// - Returns: A PascalCase Swift method name component.
private static func swiftifySignalName(_ name: String) -> String {
name.split(separator: "-").map { $0.capitalized }.joined()
}
/// Converts a GIR enum case name (SCREAMING_SNAKE_CASE) to a camelCase Swift case.
///
/// Lowercases the entire string, splits on underscores, and lowercases the
/// first word while capitalizing subsequent words. For example,
/// `"GTK_WIDGET_HELP"` becomes `"gtkWidgetHelp"`. Handles reserved keyword
/// collisions (backtick-escapes) and identifiers starting with digits (prefixes
/// with underscore).
///
/// - Parameter name: The GIR enum case name, typically in SCREAMING_SNAKE_CASE.
/// - Returns: A valid camelCase Swift enum case name.
private static func swiftifyEnumCaseName(_ name: String) -> String {
let lower = name.lowercased()
let parts = lower.split { $0 == "_" || $0 == "-" }
var result = parts.enumerated().map { i, p in
i == 0 ? String(p) : String(p).capitalized
}.joined()
// Prefix with underscore if starts with a digit
if result.first?.isNumber == true {
result = "_" + result
}
// Backtick-escape Swift keywords
if reservedKeywords.contains(result) {
result = "`\(result)`"
}
return result
}
/// Converts a snake_case or kebab-case name to PascalCase.
///
/// Splits on underscores and hyphens and capitalizes each component.
/// For example, `"gtk_init"` becomes `"GtkInit"`.
private static func pascalCaseName(_ name: String) -> String {
name.split { $0 == "_" || $0 == "-" }.map { $0.capitalized }.joined()
}
/// Filters out variadic C parameters (those named `"..."` or empty) from a parameter list.
private static func nonVarargParameters(_ params: [Parameter]) -> [Parameter] {
params.filter { $0.name != "..." && !$0.name.isEmpty }
}
}