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.
1012 lines
43 KiB
Swift
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 }
|
|
}
|
|
}
|