// PlanRenderer.swift // Dumb string emitter: takes a finished `ModulePlan` and renders it to // Swift source text. The renderer never sees `GIRType`, `IRModel`, or the // registry — only the plan types from `BindingPlan.swift`. // // Rule 2 of the three rules: the renderer must never see `GIRType`. import Foundation // MARK: - Module Renderer /// Renders a module plan to a dictionary of filename → Swift source content. /// /// File layout follows Swift conventions, decoupled from symbol names: /// - Each *type* (class, record, enum, bitfield, interface, alias) gets its /// own file named after the type (`"Align.swift"` — GIR type names are /// already PascalCase). /// - All module-level free functions merge into one `Functions.swift`, and /// all constants into one `Constants.swift`, each with `// MARK:` sections /// grouped by leading name word. One file per lowerCamelCase symbol /// produced hundreds of non-PascalCase filenames /// (`boxedFree.swift`, `PARAM_MASK.swift`) and made modules unnavigable. /// /// Every emitted filename is checked against `isValidGeneratedFileName`; /// a violation is a generator bug and traps immediately rather than landing /// in a generated package. /// /// The renderer does NOT produce scaffolding files (Package.swift, module /// maps, umbrella headers) — those come from `CodeGen+Scaffolding.swift`. /// /// - Parameter plan: The completed module plan. /// - Returns: A dictionary of relative file path → source content. public func renderModule(_ plan: ModulePlan) -> [String: String] { var files: [String: String] = [:] let depImports = plan.dependencyModules.map { "@_spi(SGTKInternal) import \($0)\n" }.joined() let header = """ // Generated by SwiftGtkGen. DO NOT EDIT. import C\(plan.module) \(depImports) """ var constants: [(name: String, body: String)] = [] var functions: [(name: String, body: String)] = [] var callbacks: [(name: String, body: String)] = [] for typePlan in plan.types { switch typePlan { case .constant(let p): constants.append((p.name, renderConstant(p))) case .callable(let p): functions.append((p.name, renderCallable(p))) case .callback(let p): callbacks.append((p.name, renderCallbackType(p))) default: let (baseName, body) = renderTypePlan(typePlan) files["\(baseName).swift"] = header + body + "\n" } } if !constants.isEmpty { files["Constants.swift"] = header + mergedFileBody(constants) } if !functions.isEmpty { files["Functions.swift"] = header + mergedFileBody(functions) } if !callbacks.isEmpty { files["Callbacks.swift"] = header + mergedFileBody(callbacks) } // Determine if any class/interface has signals — the signal runtime // (ClosureBox, SignalHandle, destroy trampoline) is only needed // when signals or callback-param callables are present. let hasSignals = plan.types.contains { typePlan in switch typePlan { case .class(let p): return !p.signals.isEmpty case .interface(let p): return !p.signals.isEmpty default: return false } } let hasCallbacks = plan.types.contains { typePlan in if case .callable(let p) = typePlan { return p.parameters.contains { if case .callbackBox(_, _) = $0.mapping.marshalIn { true } else { false } } } return false } files["Support.swift"] = renderSupport(moduleName: plan.module, dependencyModules: plan.dependencyModules, hasSignals: hasSignals, hasCallbackBoxes: hasCallbacks) for filename in files.keys { precondition(isValidGeneratedFileName(filename), "generated filename '\(filename)' violates the PascalCase convention") } return files } /// Tests whether a generated filename follows the PascalCase convention — starts /// with an uppercase ASCII letter, contains only ASCII letters/digits, and /// never has a run of more than 4 consecutive uppercase letters immediately /// preceded by a lowercase letter. That last rule rejects unconverted C /// spellings (`MarshalBOOLEAN`) while still accepting legitimate acronym /// runs, since those never follow a lowercase letter mid-name (`RGBA`, /// `GLAPI`, `DNDEvent`, `IOChannel`, `FileIOStream`, `PluginAPIFlags`, /// `AuthNTLM`). /// Per-symbol filenames are only ever derived from authoritative GIR type /// names (constants/functions/callbacks are merged into fixed-name files). /// /// - Parameter filename: A relative filename ending in `.swift`. /// - Returns: `true` when the name is conventional. public func isValidGeneratedFileName(_ filename: String) -> Bool { guard filename.hasSuffix(".swift") else { return false } let base = filename.dropLast(".swift".count) guard let first = base.first, first.isUppercase else { return false } guard base.allSatisfy({ ($0.isLetter && $0.isASCII) || $0.isNumber }) else { return false } var run = 0 var precededByLower = false for ch in base { if ch.isUppercase && ch.isASCII { run += 1 if run > 4 && precededByLower { return false } } else { run = 0 precededByLower = ch.isLowercase && ch.isASCII } } return true } /// Joins pre-rendered symbol bodies into one file body, sorted by symbol name /// with a `// MARK: -` section heading whenever the leading name word changes /// (`ascii…`, `unichar…`), so merged files stay navigable in an editor's /// symbol outline. private func mergedFileBody(_ symbols: [(name: String, body: String)]) -> String { let sorted = symbols.sorted { $0.name.lowercased() < $1.name.lowercased() } var sections: [String] = [] var currentGroup = "" for symbol in sorted { let group = leadingNameWord(symbol.name) if group != currentGroup { currentGroup = group sections.append("// MARK: - \(group.prefix(1).uppercased() + group.dropFirst())\n") } sections.append(symbol.body) } return sections.joined(separator: "\n") } /// Extracts the leading lowercase word of a symbol name for MARK grouping: /// `"unicharToUtf8"` → `"unichar"`, `"`import`"` (backtick-escaped) → `"import"`. private func leadingNameWord(_ name: String) -> String { let trimmed = name.drop(while: { $0 == "`" || $0 == "_" }) let word = trimmed.prefix(while: { $0.isLowercase || $0.isNumber }) return word.isEmpty ? String(trimmed) : String(word) } private func renderSupport(moduleName: String, dependencyModules: [String] = [], hasSignals: Bool = false, hasCallbackBoxes: Bool = false) -> String { let glibError: String if moduleName == "GLib" { glibError = """ /// A wrapper around the C `GError` that conforms to Swift's `Error` protocol. public struct GLibError: Swift.Error { public let domain: UInt32 public let code: Int32 public let message: String @_spi(SGTKInternal) public init(consuming error: UnsafeMutablePointer) { self.domain = error.pointee.domain self.code = error.pointee.code self.message = String(cString: error.pointee.message) g_error_free(error) } } """ } else { glibError = "" } let gtypeConstants: String if moduleName == "GObject" { gtypeConstants = """ // MARK: - Fundamental GType Constants public let gTypeInvalid: UInt = 0 public let gTypeNone: UInt = 4 public let gTypeInterface: UInt = 8 public let gTypeChar: UInt = 12 public let gTypeBoolean: UInt = 20 public let gTypeInt: UInt = 24 public let gTypeUint: UInt = 28 public let gTypeLong: UInt = 32 public let gTypeUlong: UInt = 36 public let gTypeInt64: UInt = 40 public let gTypeUint64: UInt = 44 public let gTypeEnum: UInt = 48 public let gTypeFlags: UInt = 52 public let gTypeFloat: UInt = 56 public let gTypeDouble: UInt = 60 public let gTypeString: UInt = 64 public let gTypePointer: UInt = 68 public let gTypeBoxed: UInt = 72 public let gTypeParam: UInt = 76 public let gTypeObject: UInt = 80 public let gTypeGtype: UInt = 88 public let gTypeVariant: UInt = 96 // MARK: - Collision-free aliases for cross-module qualification /// `GObject` (this Swift module) shares its spelling with the raw C /// struct `GObject` that every generated module's C target /// transitively vends from glib-object.h. Writing the qualifier /// `GObject.Object` from another module therefore resolves `GObject` /// to the locally visible C struct, not this module — `Object` is /// then not one of its members and the reference fails to /// typecheck. These bare, collision-free aliases are what /// `TypeRegistry.swiftTypeName` emits instead whenever a /// cross-module reference to one of these types would otherwise /// need qualifying (e.g. from `Gst`, which declares its own /// distinct `Object`/`ValueArray` classes). public typealias GLibObject = Object public typealias GLibValueArray = ValueArray """ } else { gtypeConstants = "" } let closureBoxSupport: String if hasCallbackBoxes || hasSignals { closureBoxSupport = """ // MARK: - Closure box for callback/signal dispatch /// Boxes a Swift closure for C callback trampoline dispatch. /// `@MainActor` because the stored closure is always accessed from /// `MainActor.assumeIsolated` in the trampoline or from the /// `@MainActor` connect method. @MainActor final class _ClosureBox { let closure: T init(_ c: T) { closure = c } } """ } else { closureBoxSupport = "" } let signalSupport: String if hasSignals { signalSupport = """ /// A handle returned by `connect` methods, wrapping a GObject signal /// handler ID. Disconnecting marks the handle as disconnected to /// prevent double-disconnect. /// - Note: Uses `mutating func disconnect()` + `isDisconnected` flag /// as a fallback per Phase D contingency; `~Copyable` would also work. public struct SignalHandle { public let id: UInt @_spi(SGTKInternal) public let instance: UnsafeMutableRawPointer private var isDisconnected: Bool = false @_spi(SGTKInternal) public init(id: UInt, instance: UnsafeMutableRawPointer) { self.id = id; self.instance = instance } public mutating func disconnect() { guard !isDisconnected else { return } isDisconnected = true _sgtkSignalHandlerDisconnect(instance, numericCast(id)) } } /// Implements the `GClosureNotify` C callback signature /// (two args: data pointer + GClosure pointer). /// /// Never emitted with `@_cdecl` — per-call-site wrapping via a /// `@convention(c)` literal closure avoids duplicate-symbol link /// errors when multiple modules with signals link together. @_spi(SGTKInternal) public nonisolated func _sgtkDestroyNotifyImpl( _ data: UnsafeMutableRawPointer?, _ closure: UnsafeMutableRawPointer? ) { guard let data else { return } _ = Unmanaged.fromOpaque(data).takeRetainedValue() } """ } else { signalSupport = "" } let primitiveShims: String if moduleName == "GObject" && hasSignals { primitiveShims = """ // MARK: - manual primitives (Phase E: replace with planned bindings) @_silgen_name("g_signal_connect_data") @_spi(SGTKInternal) public nonisolated func _sgtkSignalConnectData( _ instance: UnsafeMutableRawPointer, _ detailedSignal: UnsafePointer, _ cHandler: UnsafeRawPointer, _ data: UnsafeMutableRawPointer?, _ destroyData: UnsafeRawPointer?, _ connectFlags: UInt32 ) -> UInt @_silgen_name("g_signal_handler_disconnect") @_spi(SGTKInternal) public nonisolated func _sgtkSignalHandlerDisconnect( _ instance: UnsafeMutableRawPointer, _ handlerId: UInt ) """ } else { primitiveShims = "" } let depImports = dependencyModules.map { "@_spi(SGTKInternal) import \($0)\n" }.joined() return """ // Generated by SwiftGtkGen. DO NOT EDIT. import C\(moduleName) \(depImports) /// Reinterprets a wrapper's raw instance pointer as an `OpaquePointer`, /// selected when the C function's parameter is an opaque struct pointer. @inline(__always) func _instancePointer(_ pointer: UnsafeMutableRawPointer) -> OpaquePointer { OpaquePointer(pointer) } /// Reinterprets a wrapper's raw instance pointer as a typed /// `UnsafeMutablePointer`, selected when the C function's parameter is a /// complete struct pointer. @inline(__always) func _instancePointer(_ pointer: UnsafeMutableRawPointer) -> UnsafeMutablePointer { pointer.assumingMemoryBound(to: T.self) } /// Returns a `UnsafeMutableRawPointer` from a typed const C pointer. @inline(__always) func _rawPointer(_ p: UnsafePointer) -> UnsafeMutableRawPointer { UnsafeMutableRawPointer(mutating: p) } /// Returns a `UnsafeMutableRawPointer` from an untyped const raw pointer. @inline(__always) func _rawPointer(_ p: UnsafeRawPointer) -> UnsafeMutableRawPointer { UnsafeMutableRawPointer(mutating: p) } /// Returns a `UnsafeMutableRawPointer` from a typed mutable C pointer. @inline(__always) func _rawPointer(_ p: UnsafeMutablePointer) -> UnsafeMutableRawPointer { UnsafeMutableRawPointer(mutating: p) } /// Returns a `UnsafeMutableRawPointer` from an `OpaquePointer`. @inline(__always) func _rawPointer(_ p: OpaquePointer) -> UnsafeMutableRawPointer { UnsafeMutableRawPointer(p) } /// Copies a transfer-full C string into a Swift `String` and frees the source. @inline(__always) func _takeString(_ p: UnsafeMutablePointer!) -> String { defer { g_free(p) } return String(cString: p) } /// Copies a nullable transfer-full C string, freeing the source; `nil` in → `nil` out. @inline(__always) func _takeStringIfPresent(_ p: UnsafeMutablePointer?) -> String? { guard let p else { return nil } defer { g_free(p) } return String(cString: p) } /// Bridges an optional `String?` to a C `const char *`, passing `nil` when absent. @inline(__always) func _withOptionalCString(_ s: String?, _ body: (UnsafePointer?) throws -> R) rethrows -> R { guard let s else { return try body(nil) } return try s.withCString(body) } /// Bridges a `[String]` to a NULL-terminated C `char **`, freeing the copies after `body`. @inline(__always) func _withStringArray(_ strings: [String], _ body: (UnsafeMutablePointer?>?) throws -> R) rethrows -> R { var cStrings: [UnsafeMutablePointer?] = strings.map { g_strdup($0) } cStrings.append(nil) defer { for p in cStrings { g_free(p) } } return try cStrings.withUnsafeMutableBufferPointer { try body($0.baseAddress) } } \(glibError)\(gtypeConstants)\(closureBoxSupport)\(signalSupport)\(primitiveShims) """ } // MARK: - Type-level rendering /// Renders a single `TypePlan` into a base filename and Swift source body. /// /// - Parameter typePlan: The type to render. /// - Returns: A tuple of base filename (without extension) and Swift source text. private func renderTypePlan(_ typePlan: TypePlan) -> (String, String) { switch typePlan { case .enumeration(let p): return (p.name, renderEnum(p)) case .bitfield(let p): return (p.name, renderBitfield(p)) case .constant(let p): return (p.name, renderConstant(p)) case .alias(let p): return (p.name, renderAlias(p)) case .class(let p): return (p.name, renderClass(p)) case .interface(let p): return (p.name, renderInterface(p)) case .record(let p): return (p.name, renderRecord(p)) case .callable(let p): return (p.name, renderCallable(p)) case .callback(let p): return (p.name, renderCallbackType(p)) } } // ── Enumeration ── private func renderEnum(_ plan: EnumPlan) -> String { var lines: [String] = [] if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc)) } lines.append("public nonisolated enum \(plan.name): Int, Sendable {") for c in plan.cases { lines.append(" case \(c.name) = \(c.rawValue)") } if !plan.aliases.isEmpty { lines.append("") for alias in plan.aliases { lines.append(" public static var \(alias.name): \(plan.name) { .\(alias.targetCaseName) }") } } lines.append("}") return lines.joined(separator: "\n") + "\n" } // ── Bitfield ── private func renderBitfield(_ plan: BitfieldPlan) -> String { var lines: [String] = [] if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc)) } lines.append("public nonisolated struct \(plan.name): OptionSet, Sendable {") lines.append(" public let rawValue: UInt32") lines.append(" public init(rawValue: UInt32) { self.rawValue = rawValue }") for member in plan.members { if member.rawValue == "0" { lines.append(" public static let \(member.name): \(plan.name) = []") } else { let value = swiftBitfieldLiteral(member.rawValue) lines.append(" public static let \(member.name) = \(plan.name)(rawValue: \(value))") } } lines.append("}") return lines.joined(separator: "\n") + "\n" } // ── Constant ── private func renderConstant(_ plan: ConstantPlan) -> String { var lines: [String] = [] if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc)) } // Keep the C spelling greppable after the lowerCamelCase rename. lines.append("/// Binds the GIR constant `\(plan.girName)`.") // String constants need quotes; numeric/literal values pass through let valueExpr: String if plan.swiftType == "String" { valueExpr = "\"\(plan.value)\"" } else { valueExpr = plan.value } lines.append("public nonisolated let \(plan.name): \(plan.swiftType) = \(valueExpr)") return lines.joined(separator: "\n") + "\n" } // ── Alias ── private func renderAlias(_ plan: AliasPlan) -> String { var lines: [String] = [] if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc)) } lines.append("public typealias \(plan.name) = \(plan.swiftType)") return lines.joined(separator: "\n") + "\n" } // MARK: - Raw pointer detection (no-unsafe-pointer public API policy) /// Raw-pointer type markers that must never appear in a plain-`public` /// declaration's signature, property type, or typealias — anything matching /// is demoted to `@_spi(SGTKInternal)` instead of being skipped (Phase E4). private let rawPointerMarkers = [ "UnsafeMutableRawPointer", "UnsafeRawPointer", "UnsafeMutablePointer", "UnsafePointer", "OpaquePointer", "@convention(c)", ] /// `true` when a Swift-facing type string exposes a raw pointer or C /// function-pointer type. private func swiftTypeHasRawPointer(_ swiftType: String) -> Bool { rawPointerMarkers.contains { swiftType.contains($0) } } /// `true` when any parameter or the return type of a callable's Swift-facing /// signature (including out-param-derived return types) exposes a raw /// pointer — such callables must be rendered `@_spi(SGTKInternal) public` /// rather than plain `public`. private func signatureHasRawPointer(_ plan: CallablePlan) -> Bool { let paramHit = plan.parameters.contains { param in !param.isInstanceParameter && swiftTypeHasRawPointer(param.mapping.swiftType) } let returnHit = plan.returnMapping.map { swiftTypeHasRawPointer($0.swiftType) } ?? false return paramHit || returnHit } /// `true` when a property's Swift-facing type exposes a raw pointer. private func propertyHasRawPointer(_ plan: PropertyPlan) -> Bool { swiftTypeHasRawPointer(plan.swiftType) } /// Renders the `@_spi(SGTKInternal) ` prefix when `hidden` is true, else /// the empty string — used to keep call sites terse. private func spiPrefix(_ hidden: Bool) -> String { hidden ? "@_spi(SGTKInternal) " : "" } // ── Callback typealias ── /// Renders a namespace-level callback type as both a C-compatible /// `@convention(c)` typealias and a Swift-friendly `@escaping` typealias. /// /// Example output: /// ```swift /// /// Documentation /// public typealias GClosureNotify = @convention(c) (UnsafeMutableRawPointer?) -> Void /// public typealias GClosureNotifySwift = (UnsafeMutableRawPointer?) -> Void /// ``` private func renderCallbackType(_ plan: CallbackTypePlan) -> String { var lines: [String] = [] if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc)) } lines.append("@_spi(SGTKInternal) public typealias \(plan.name) = \(plan.cSwiftType)") lines.append("\(spiPrefix(swiftTypeHasRawPointer(plan.swiftType)))public typealias \(plan.name)Swift = \(plan.swiftType)") return lines.joined(separator: "\n") + "\n" } // ── Record (boxed) ── private func renderRecord(_ plan: RecordPlan) -> String { var lines: [String] = [] if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc)) } lines.append("@MainActor public final class \(plan.name) {") lines.append(" @_spi(SGTKInternal) public let pointer: UnsafeMutableRawPointer") lines.append("") if plan.freeFunction != nil { lines.append(" /// Adopts an owned boxed pointer; the wrapper takes responsibility") lines.append(" /// for freeing it. Use for `transfer-ownership=\"full\"` returns.") } else { lines.append(" /// Stores a boxed pointer. This wrapper has no known free function, so it") lines.append(" /// never frees the pointee — safe for borrowed (transfer-none) values such") lines.append(" /// as signal parameters.") } lines.append(" @_spi(SGTKInternal) public init(takingOwnership pointer: UnsafeMutableRawPointer) {") lines.append(" self.pointer = pointer") lines.append(" }") // init(retaining:) — copies a borrowed pointer so the wrapper owns its own // instance. Only emitted when a copy/ref function is known. Most C // ref/copy functions follow the `T *fn(T *)` convention and return the // (possibly new) pointer; a plain refcount bump like // `gst_atomic_queue_ref` returns `void` instead, so the argument // pointer itself is retained rather than the call's result. if let copy = plan.copyFunction { lines.append("") lines.append(" /// Copies a borrowed boxed pointer so the wrapper owns an independent") lines.append(" /// instance. Use for `transfer-ownership=\"none\"` returns.") lines.append(" @_spi(SGTKInternal) public init(retaining pointer: UnsafeMutableRawPointer) {") if plan.copyReturnsVoid { lines.append(" \(copy)(_instancePointer(pointer))") lines.append(" self.pointer = pointer") } else { lines.append(" self.pointer = _rawPointer(\(copy)(_instancePointer(pointer)))") } lines.append(" }") } // deinit — frees the owned pointer. `isolated` runs the deinit on the // type's default actor (MainActor for this package), which is required to // touch the non-Sendable `pointer` from a deinitialiser under strict // concurrency. Only emitted when a free/unref function is known; without // one the wrapper leaks rather than risk freeing with the wrong function. if let free = plan.freeFunction { lines.append("") lines.append(" isolated deinit {") lines.append(" \(free)(_instancePointer(pointer))") lines.append(" }") } lines.append("}") return lines.joined(separator: "\n") + "\n" } // ── Interface ── private func renderInterface(_ plan: InterfacePlan) -> String { var lines: [String] = [] // Signal trampolines at file scope (same pattern as classes) for sig in plan.signals { lines.append(contentsOf: renderSignalTrampoline(sig)) lines.append("") } if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc)) } var protocolInherits = "" if !plan.prereqs.isEmpty { protocolInherits = ": " + plan.prereqs.map { "@MainActor \($0)" }.joined(separator: ", ") } lines.append("public protocol \(plan.name)\(protocolInherits) {") lines.append(" @_spi(SGTKInternal) var pointer: UnsafeMutableRawPointer { get }") lines.append("}") // Method/property bodies live in a protocol extension as default // implementations (not bare requirements): every conformer gets a // working implementation via `self.pointer` for free, and no // requirement can go unwitnessed (a class's own member of the same // name still wins by static dispatch specificity where it exists). if !plan.methods.isEmpty || !plan.properties.isEmpty || !plan.signals.isEmpty { lines.append("") lines.append("extension \(plan.name) {") for method in plan.methods { lines.append(contentsOf: renderMethod(method)) lines.append("") } for prop in plan.properties { lines.append(contentsOf: renderProperty(prop)) lines.append("") } for sig in plan.signals { lines.append(contentsOf: renderSignalConnect(sig, className: plan.name)) lines.append("") } lines.append("}") } // Concrete wrapper: interface protocols have no initializers, so a // value of `any Foo` cannot be constructed from a raw C pointer. Only // emitted for GObject-derived interfaces, which is every case the // mapper currently produces (`TypeMapper`'s `.interface` case guards // on `registry.isGObject`). if plan.isGObject { lines.append("") lines.append("/// Concrete, ref-counted storage for an `any \(plan.name)` value obtained") lines.append("/// from a C call — interface protocols have no initializers of their own.") if let classPrereq = plan.classPrereq { // The protocol's own inheritance clause constrains conformers to // `Self: \(classPrereq)` (a class-typed prerequisite), so the Ref // must literally subclass it — inheriting its pointer storage, // inits, and deinit rather than declaring its own. lines.append("@MainActor public final class \(plan.name)Ref: \(classPrereq), @MainActor \(plan.name) {") lines.append("}") } else { lines.append("@MainActor public final class \(plan.name)Ref: @MainActor \(plan.name) {") lines.append(" @_spi(SGTKInternal) public let pointer: UnsafeMutableRawPointer") lines.append("") lines.append(" @_spi(SGTKInternal) public init(retaining pointer: UnsafeMutableRawPointer) {") lines.append(" g_object_ref(pointer)") lines.append(" self.pointer = pointer") lines.append(" }") lines.append("") lines.append(" @_spi(SGTKInternal) public init(takingOwnership pointer: UnsafeMutableRawPointer) {") lines.append(" self.pointer = pointer") lines.append(" }") lines.append("") lines.append(" isolated deinit {") lines.append(" g_object_unref(pointer)") lines.append(" }") lines.append("}") } } return lines.joined(separator: "\n") + "\n" } // ── Class ── private func renderClass(_ plan: ClassPlan) -> String { var lines: [String] = [] if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc)) } // ── Signal trampolines (file-level @_cdecl) ── for sig in plan.signals { lines.append(contentsOf: renderSignalTrampoline(sig)) lines.append("") } let access = plan.isOpen ? "open" : "public" let parentDecl: String if let parent = plan.parent { let ifaces = plan.interfaces.isEmpty ? "" : ", \(plan.interfaces.map { "@MainActor \($0)" }.joined(separator: ", "))" parentDecl = ": \(parent)\(ifaces)" } else if !plan.interfaces.isEmpty { parentDecl = ": \(plan.interfaces.map { "@MainActor \($0)" }.joined(separator: ", "))" } else { parentDecl = "" } lines.append("@MainActor \(access) class \(plan.name)\(parentDecl) {") let isRoot = plan.parent == nil // Storage — only for root classes if isRoot { lines.append(" @_spi(SGTKInternal) public let pointer: UnsafeMutableRawPointer") lines.append("") } // Root classes ALWAYS get inits (even abstract — subclasses need them for chaining). // Non-root abstract classes skip inits (parent handles them). let needsInits = isRoot || !plan.isAbstract if needsInits { let initModifier = isRoot ? "" : "@_spi(SGTKInternal) public " // takingOwnership init if isRoot { let body = plan.descendsFromInitiallyUnowned ? ["g_object_ref_sink(pointer)", "self.pointer = pointer"] : ["self.pointer = pointer"] lines.append(" @_spi(SGTKInternal) public required init(takingOwnership pointer: UnsafeMutableRawPointer) {") for line in body { lines.append(" \(line)") } lines.append(" }") } else { let body = plan.descendsFromInitiallyUnowned ? ["g_object_ref_sink(pointer)", "super.init(takingOwnership: pointer)"] : ["super.init(takingOwnership: pointer)"] lines.append(" \(initModifier)required init(takingOwnership pointer: UnsafeMutableRawPointer) {") for line in body { lines.append(" \(line)") } lines.append(" }") } lines.append("") // Retaining init. `g_object_ref`/`g_object_ref_sink` take a plain // `gpointer` and accept `pointer` as-is; a class rooting its own // fundamental hierarchy (e.g. `GParamSpec`) declares a ref function // that takes its typed C struct pointer, requiring `_instancePointer`. let refArg = plan.refFunc == "g_object_ref" ? "pointer" : "_instancePointer(pointer)" if isRoot { lines.append(" @_spi(SGTKInternal) public init(retaining pointer: UnsafeMutableRawPointer) {") lines.append(" self.pointer = pointer") lines.append(" \(plan.refFunc)(\(refArg))") lines.append(" }") } else { lines.append(" @_spi(SGTKInternal) public override init(retaining pointer: UnsafeMutableRawPointer) {") lines.append(" \(plan.refFunc)(\(refArg))") lines.append(" super.init(takingOwnership: pointer)") lines.append(" }") } lines.append("") } // deinit — releases the ref taken/adopted in the inits above. `isolated` // runs on the package default actor (MainActor), required to touch the // non-Sendable `pointer` under strict concurrency. Emitted only on the // root (which owns `pointer`); subclasses inherit it. if isRoot { let unrefArg = plan.unrefFunc == "g_object_unref" ? "pointer" : "_instancePointer(pointer)" lines.append(" isolated deinit {") lines.append(" \(plan.unrefFunc)(\(unrefArg))") lines.append(" }") lines.append("") } // ── Members: constructors, methods, static functions ── for ctor in plan.constructors { lines.append(contentsOf: renderConstructor(ctor)) lines.append("") } for method in plan.methods { lines.append(contentsOf: renderMethod(method)) lines.append("") } for fn in plan.functions { lines.append(contentsOf: renderStaticFunction(fn)) lines.append("") } // ── Properties ── for prop in plan.properties { lines.append(contentsOf: renderProperty(prop)) lines.append("") } // ── Signals ── for sig in plan.signals { lines.append(contentsOf: renderSignalConnect(sig, className: plan.name)) lines.append("") } lines.append("}") return lines.joined(separator: "\n") + "\n" } // ── Signal rendering ── /// Renders a `@_cdecl nonisolated` trampoline for a GObject signal. /// /// The `@_cdecl` ABI is required because C calls this function via a raw /// function pointer; Swift's native calling convention would SIGILL. /// The body wraps wrapper construction and the closure call in /// `MainActor.assumeIsolated` per AGENTS Top Risk #1 — traps if C /// ever fires the signal off the main thread. private func renderSignalTrampoline(_ plan: SignalPlan) -> [String] { var lines: [String] = [] let instanceParams = plan.parameters.filter { $0.isInstanceParameter } let realParams = plan.parameters.filter { !$0.isInstanceParameter } let allParams = instanceParams + realParams // User-facing closure type stored in ClosureBox (typed wrappers) let closureParamTypes = allParams.map { $0.mapping.swiftType } let closureRet = plan.returnMapping?.swiftType ?? "Void" let closureParams = closureParamTypes.isEmpty ? "" : closureParamTypes.joined(separator: ", ") let closureType = "(\(closureParams)) -> \(closureRet)" // C parameter declaration for the @_cdecl function var cParamStrs: [String] = [] cParamStrs.append("_ instance: UnsafeMutableRawPointer") for (idx, p) in realParams.enumerated() { let cType = p.mapping.cSwiftType.replacingOccurrences(of: "?", with: "") cParamStrs.append("_ p\(idx + 1): \(cType)") } cParamStrs.append("_ data: UnsafeMutableRawPointer?") let cDecl = cParamStrs.joined(separator: ", ") // Wrapper construction for each parameter inside MainActor.assumeIsolated. // The raw C parameters (`instance`, `p1`, `p2`, ...) belong to this // `nonisolated` trampoline's isolation domain. Swift 6's region-based // sending checker flags capturing them directly into the `@MainActor` // closure below as a potential data race, even though they are trivial // pointer values with no live aliasing concern here (C never touches // them again once the trampoline is invoked). `nonisolated(unsafe)` // shadow copies sidestep the checker for this documented-safe case. var shadowLines: [String] = [] var wrapperLines: [String] = [] for (i, p) in allParams.enumerated() { let rawName = i == 0 ? "instance" : "p\(i)" let shadowName = "captured\(rawName.prefix(1).uppercased())\(rawName.dropFirst())" shadowLines.append("nonisolated(unsafe) let \(shadowName) = \(rawName)") wrapperLines.append("let w\(i) = \(renderWrapperExpr(for: p, rawName: shadowName, ownerIsInterface: plan.ownerIsInterface))") } let wrapperRefs = (0..>.fromOpaque(data).takeUnretainedValue()") lines.append(shadowBody) lines.append(" MainActor.assumeIsolated {") lines.append(wrapperBody) lines.append(" }") lines.append("}") return lines } /// Renders a single wrapper-expression for a signal parameter: the Swift /// expression that converts a raw C argument (managed by the trampoline) /// into a typed Swift wrapper. Extracted from renderSignalTrampoline. private func renderWrapperExpr(for p: ParameterPlan, rawName: String, ownerIsInterface: Bool = false) -> String { if p.isInstanceParameter { if ownerIsInterface { return "\(p.mapping.swiftType)Ref(retaining: \(rawName))" } return "\(p.mapping.swiftType)(retaining: \(rawName))" } switch p.mapping.marshalIn { case .boxedPointer: let isOptional = p.mapping.swiftType.hasSuffix("?") let baseType = isOptional ? String(p.mapping.swiftType.dropLast()) : p.mapping.swiftType // A signal parameter is a borrowed pointer (transfer none); wrapping // it must copy/ref so the wrapper owns an independent instance. Boxed // records without a GIR copy-function (e.g. GdkToplevelSize) have no // `init(retaining:)` (only emitted when one is known — see // renderRecord); adopt the borrowed pointer via `takingOwnership:` // instead, matching the only initializer such records expose. if case .boxedWrap(_, let copyFn) = p.mapping.marshalOut, copyFn == nil { return "\(baseType)(takingOwnership: \(rawName))" } return "\(baseType)(retaining: \(rawName))" case .objectPointer: let isOptional = p.mapping.swiftType.hasSuffix("?") let baseType = isOptional ? String(p.mapping.swiftType.dropLast()) : p.mapping.swiftType return "\(baseType)(retaining: \(rawName))" case .interfacePointer: let isOptional = p.mapping.swiftType.hasSuffix("?") let baseType = isOptional ? String(p.mapping.swiftType.dropLast()) : p.mapping.swiftType return "\(baseType)Ref(retaining: \(rawName))" case .enumRaw: return "\(p.mapping.swiftType)(rawValue: numericCast(\(rawName).rawValue))!" case .bitfieldRaw: return "\(p.mapping.swiftType)(rawValue: numericCast(\(rawName).rawValue))" case .stringToC: return "String(cString: \(rawName))" case .boolToGboolean: return "\(rawName) != 0" default: return rawName } } /// Renders the `connect` method. Boxes the user's typed handler /// directly — wrapper construction from raw C args is handled by the /// trampoline under `MainActor.assumeIsolated`. private func renderSignalConnect(_ plan: SignalPlan, className: String) -> [String] { var lines: [String] = [] let instanceParams = plan.parameters.filter { $0.isInstanceParameter } let realParams = plan.parameters.filter { !$0.isInstanceParameter } // User-facing closure type (typed wrappers) — same as the trampoline's closureType let closureParamTypes = instanceParams.map { $0.mapping.swiftType } + realParams.map { $0.mapping.swiftType } let closureRet = plan.returnMapping?.swiftType ?? "Void" let closureParams = closureParamTypes.isEmpty ? "" : closureParamTypes.joined(separator: ", ") let closureType = "(\(closureParams)) -> \(closureRet)" // Build @convention(c) type for the trampoline's unsafeBitCast let cTypes = ["UnsafeMutableRawPointer"] + realParams.map { p in p.mapping.cSwiftType.replacingOccurrences(of: "?", with: "") } + ["UnsafeMutableRawPointer?"] let cTypeStr = cTypes.joined(separator: ", ") let connectName = "connect" + plan.swiftName.prefix(1).uppercased() + plan.swiftName.dropFirst() let detailParam = plan.isDetailed ? "detail: String?, " : "" let detailBody: String if plan.isDetailed { detailBody = "let signalName: String = detail.map { \"\(plan.girName)::\\($0)\" } ?? \"\(plan.girName)\"" } else { detailBody = "let signalName = \"\(plan.girName)\"" } if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc).map { " \($0)" }) } lines.append(" public func \(connectName)(\(detailParam)_ handler: @escaping \(closureType)) -> SignalHandle {") lines.append(" let box = _ClosureBox(handler)") lines.append(" let dataPtr = Unmanaged.passRetained(box).toOpaque()") lines.append(" let destroyFn: @convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> Void = { data, _ in") lines.append(" _sgtkDestroyNotifyImpl(data, nil)") lines.append(" }") lines.append(" let ptr = self.pointer") lines.append(" \(detailBody)") lines.append(" return signalName.withCString { cName in") lines.append(" let id = _sgtkSignalConnectData(ptr, cName, unsafeBitCast(\(plan.trampolineCName) as (@convention(c) (\(cTypeStr)) -> Void), to: UnsafeRawPointer.self), dataPtr, unsafeBitCast(destroyFn as (@convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> Void), to: UnsafeRawPointer.self), 0)") lines.append(" return SignalHandle(id: id, instance: ptr)") lines.append(" }") lines.append(" }") return lines } private func swiftSignature(_ plan: CallablePlan) -> String { plan.parameters.filter { !$0.isInstanceParameter && !$0.isOutParameter && $0.synthesizedLengthOf == nil }.map { param in let typeStr: String if param.mapping.category == .callback { typeStr = "@escaping \(param.mapping.swiftType)" } else { typeStr = param.mapping.swiftType } return "\(param.swiftName): \(typeStr)" }.joined(separator: ", ") } /// How a string/array parameter is wrapped for the C call. private enum CWrapKind { case plain, optional, array } /// Generates the closure opener for a string/array parameter: `withCString`, /// `_withOptionalCString`, or `_withStringArray`. private func cWrapOpener(_ cName: String, _ swiftName: String, _ kind: CWrapKind) -> String { switch kind { case .plain: return "\(swiftName).withCString { \(cName) in" case .optional: return "_withOptionalCString(\(swiftName)) { \(cName) in" case .array: return "_withStringArray(\(swiftName)) { \(cName) in" } } /// Builds the C call: the C-function-call string with each argument marshalled, /// plus the string parameters that must be wrapped in `withCString`. The /// instance parameter — if any — is passed as `self.pointer`. /// /// Callback-box parameters: the data pointer replaces the closure arg at the /// callback's cArgIndex, and also replaces any separate user-data slot /// identified by `closureIndex`. private func cArguments(_ plan: CallablePlan, error: Bool = false) -> (cCall: String, stringParams: [(cName: String, swiftName: String, kind: CWrapKind)]) { var stringParams: [(cName: String, swiftName: String, kind: CWrapKind)] = [] var cArgExprs: [String] = [] var outIndex = 0 var closureDataMap: [Int: String] = [:] for p in plan.parameters { if case .callbackBox(_, _) = p.mapping.marshalIn, let ci = p.closureIndex { let base = p.swiftName.replacingOccurrences(of: "`", with: "") closureDataMap[ci] = "\(base)Data" } } for param in plan.parameters { if param.isOutParameter { cArgExprs.append("&out\(outIndex)") outIndex += 1 } else if param.isInstanceParameter { cArgExprs.append("_instancePointer(self.pointer)") } else if let arrName = param.synthesizedLengthOf { cArgExprs.append("numericCast(\(arrName).count)") } else if param.mapping.marshalIn == .stringToC { let cName = "cString\(stringParams.count)" let kind: CWrapKind = param.mapping.swiftType.hasSuffix("?") ? .optional : .plain stringParams.append((cName: cName, swiftName: param.swiftName, kind: kind)) cArgExprs.append(cName) } else if param.mapping.marshalIn == .stringArrayToC { let cName = "cArray\(stringParams.count)" stringParams.append((cName: cName, swiftName: param.swiftName, kind: .array)) cArgExprs.append(cName) } else if let dataPtrName = closureDataMap[param.cArgIndex], !isCallbackBox(param) { cArgExprs.append(dataPtrName) } else { cArgExprs.append(marshalCallArg(param)) } } if error { cArgExprs.append("&error") } return ("\(plan.cIdentifier)(\(cArgExprs.joined(separator: ", ")))", stringParams) } /// Emits setup statements for callback-box parameters: box the closure, private func isCallbackBox(_ param: ParameterPlan) -> Bool { if case .callbackBox(_, _) = param.mapping.marshalIn { return true } return false } private func callbackBoxSetup(_ plan: CallablePlan, indent: String) -> [String] { var lines: [String] = [] for p in plan.parameters { if case .callbackBox(_, _) = p.mapping.marshalIn { let base = p.swiftName.replacingOccurrences(of: "`", with: "") lines.append("\(indent)let \(base)Box = _ClosureBox(\(p.swiftName))") lines.append("\(indent)let \(base)Data = Unmanaged.passRetained(\(base)Box).toOpaque()") } } return lines } /// Emits release statements for scope==.call callback-box parameters. private func callbackBoxRelease(_ plan: CallablePlan, indent: String) -> [String] { var lines: [String] = [] for p in plan.parameters { if case .callbackBox(let scope, _) = p.mapping.marshalIn, scope == .call { let base = p.swiftName.replacingOccurrences(of: "`", with: "") lines.append("\(indent)_ = Unmanaged<_ClosureBox<\(p.mapping.cSwiftType)>>.fromOpaque(\(base)Data).takeRetainedValue()") } } return lines } /// The C type for an out-param's local variable declaration (the pointee type /// passed as `&local` to the C function). private func outParamLocalType(_ param: ParameterPlan) -> String { switch param.mapping.marshalIn { case .stringToC: if case .stringCopy(_, let constPointee) = param.mapping.marshalOut, constPointee { // c:type is `const char**`: the Clang importer expects // UnsafePointer? as the pointee, not // UnsafeMutablePointer? (that's only correct when the // callee hands over a mutable/owned `char**`). return "UnsafePointer?" } return "UnsafeMutablePointer?" case .boolToGboolean: return "Int32" case .direct, .numericCast: // GIR `nullable="1"` on an out-param describes whether the ARGUMENT // itself is omittable (caller may pass NULL to skip it) — this // generator always allocates a local and passes `&local`, so that // never applies. For scalar pointees the type-mapper's blanket // `optionalised(nullable:)` still wraps `Int32` → `Int32?`, which // doesn't match the non-optional `UnsafeMutablePointer` the // Clang importer expects. Raw/object/boxed pointees are exempt: // their cSwiftType is `UnsafeMutableRawPointer?`, optional from the // base mapping itself (a real nullable pointee), not from this wrap. let t = param.mapping.cSwiftType return t == "UnsafeMutableRawPointer?" ? t : (t.hasSuffix("?") ? String(t.dropLast()) : t) case .enumRaw, .bitfieldRaw: let t = param.mapping.cSwiftType return t.hasSuffix("?") ? String(t.dropLast()) : t default: return "UnsafeMutablePointer?" } } /// The initial value for an out-param local variable. `.direct` marshalIn /// covers BOTH numeric scalars (`gint*`, `gsize*`) and raw pointers /// (`gpointer*`, `GVariant**` imported as `UnsafeMutableRawPointer?`) — the /// zero literal that numeric out-params need is not a valid pointer /// initializer, so the local type (not the marshal category) decides. private func outParamInitValue(_ param: ParameterPlan) -> String { switch param.mapping.marshalIn { case .stringToC: return "nil" case .boolToGboolean, .direct, .numericCast: let localType = outParamLocalType(param) return localType.hasSuffix("?") ? "nil" : "0" case .enumRaw, .bitfieldRaw: return ".init(rawValue: 0)" default: return "UnsafeMutablePointer?" } } /// `varName` is the local variable after the C call filled it in. private func outParamValueExpr(_ param: ParameterPlan, varName: String) -> String { switch param.mapping.marshalOut { case .direct: return varName case .numericCast: return "numericCast(\(varName))" case .gbooleanToBool: return "\(varName) != 0" case .stringCopy(let free, _): if param.mapping.swiftType.hasSuffix("?") { return free ? "_takeStringIfPresent(\(varName))" : "\(varName).map { String(cString: $0) }" } return free ? "_takeString(\(varName))" : "String(cString: \(varName)!)" case .enumFromRaw(let swiftType): return "\(swiftType)(rawValue: numericCast(\(varName).rawValue))!" case .bitfieldFromRaw(let swiftType): return "\(swiftType)(rawValue: numericCast(\(varName).rawValue))" case .objectWrap, .objectRetain, .boxedWrap, .interfaceWrap, .unsupported: return varName } } /// Computes the Swift return type when a callable has out-params. /// Returns `nil` for callables without out-params (normal return applies). private func outParamReturnType(_ plan: CallablePlan) -> String? { let outParams = plan.parameters.filter(\.isOutParameter) guard !outParams.isEmpty else { return nil } if let ret = plan.returnMapping { let retStr = "return: \(ret.swiftType)" let outStrs = outParams.map { "\($0.swiftName): \($0.mapping.swiftType)" } return "(\(retStr), \(outStrs.joined(separator: ", ")))" } else if outParams.count == 1 { // Single out-param with no Swift return: return the value type directly. return outParams[0].mapping.swiftType } else { let outStrs = outParams.map { "\($0.swiftName): \($0.mapping.swiftType)" } return "(\(outStrs.joined(separator: ", ")))" } } /// Renders a multi-statement function body that handles out-params (and /// optionally GError throws). This generalizes `renderThrowingBody` when the /// callable also has `direction="out"` parameters. private func renderMultiStatementBody(_ plan: CallablePlan, indent: String) -> [String] { let hasReturn = plan.returnMapping != nil let (cCall, stringParams) = cArguments(plan, error: plan.throwsError) // Out-param locals: declare, pass `&`, extract after call let outParams = plan.parameters.filter(\.isOutParameter) var localDecls = callbackBoxSetup(plan, indent: indent) var outValues: [String] = [] for (idx, param) in outParams.enumerated() { localDecls.append("\(indent)var out\(idx): \(outParamLocalType(param)) = \(outParamInitValue(param))") outValues.append(outParamValueExpr(param, varName: "out\(idx)")) } // Error local if plan.throwsError { localDecls.append("\(indent)var error: UnsafeMutablePointer? = nil") } let cCallStmt = hasReturn ? "\(indent)let result = \(cCall)" : "\(indent)_ = \(cCall)" // Post-call: error check then return var postCall: [String] = [] if plan.throwsError { postCall.append("\(indent)if let e = error {") postCall.append("\(indent) throw GLibError(consuming: e)") postCall.append("\(indent)}") } postCall.append(contentsOf: callbackBoxRelease(plan, indent: indent)) // C pointer returns (object, string, boxed) are imported as optionals; // force-unwrap when the Swift type is non-optional (safe: error-check // or out-param-validity ensures non-nil on success). let needsUnwrap: Bool if hasReturn { switch plan.returnMapping!.marshalOut { case .objectWrap, .objectRetain, .boxedWrap, .interfaceWrap, .stringCopy: needsUnwrap = !plan.returnMapping!.swiftType.hasSuffix("?") default: needsUnwrap = false } } else { needsUnwrap = false } let returnArg = needsUnwrap ? "result!" : "result" let returnExpr: String if hasReturn && !outValues.isEmpty { let rv = marshalReturn(returnArg, mapping: plan.returnMapping!) let all = ([rv] + outValues).joined(separator: ", ") returnExpr = "return (\(all))" } else if !outValues.isEmpty { returnExpr = outValues.count == 1 ? "return \(outValues[0])" : "return (\(outValues.joined(separator: ", ")))" } else if hasReturn { returnExpr = "return \(marshalReturn(returnArg, mapping: plan.returnMapping!))" } else { returnExpr = "return" } postCall.append("\(indent)\(returnExpr)") if stringParams.isEmpty { return localDecls + [cCallStmt] + postCall } // Wrap in `withCString` closures when there are string params var lines: [String] = [] var scope = indent for sp in stringParams { let openerPrefix = plan.throwsError ? "return try " : "return " lines.append("\(scope)\(openerPrefix)\(cWrapOpener(sp.cName, sp.swiftName, sp.kind))") scope += " " } for ld in localDecls { let stripped = ld.hasPrefix(indent) ? String(ld.dropFirst(indent.count)) : ld lines.append("\(scope)\(stripped)") } let strippedCall = cCallStmt.hasPrefix(indent) ? String(cCallStmt.dropFirst(indent.count)) : cCallStmt lines.append("\(scope)\(strippedCall)") for pc in postCall { let stripped = pc.hasPrefix(indent) ? String(pc.dropFirst(indent.count)) : pc lines.append("\(scope)\(stripped)") } for _ in stringParams { scope = String(scope.dropLast(4)) lines.append("\(scope)}") } return lines } /// Builds a single Swift expression that evaluates to the callable's raw C /// return value, bridging string parameters through nested `withCString` /// closures. Used where a statement form is impossible (e.g. feeding a /// constructor's `self.init`). Prefer `renderCallBody` for functions/methods — /// the multi-line statement form keeps the type-checker happy for callables /// with several string parameters. private func renderCallExpression(_ plan: CallablePlan) -> String { let (cCall, stringParams) = cArguments(plan) var expr = cCall for sp in stringParams.reversed() { expr = "\(cWrapOpener(sp.cName, sp.swiftName, sp.kind)) \(expr) }" } return expr } /// Renders the `{ … }` body lines of a function or method (indented by /// `indent`). String parameters are wrapped in one `withCString` closure per /// string. Callback-box params add setup before the C call and release /// after (for scope==.call). private func renderCallBody(_ plan: CallablePlan, indent: String) -> [String] { let hasCallbacks = plan.parameters.contains { if case .callbackBox(_, _) = $0.mapping.marshalIn { true } else { false } } let (cCall, stringParams) = cArguments(plan) let hasReturn = plan.returnMapping != nil // When callbacks are present, emit setup / call / release / return // instead of a single return-expression (release must run after C call). let callStmt: String let returnExprAfter: String? if hasCallbacks && hasReturn { callStmt = "\(indent)let _result = \(cCall)" returnExprAfter = "\(indent)return \(marshalReturn("_result", mapping: plan.returnMapping!))" } else if hasReturn { callStmt = "\(indent)return \(marshalReturn(cCall, mapping: plan.returnMapping!))" returnExprAfter = nil } else { callStmt = "\(indent)\(cCall)" returnExprAfter = nil } // Simple path: no callbacks, no string params if !hasCallbacks && stringParams.isEmpty { return [callStmt] } let setupStmts = callbackBoxSetup(plan, indent: indent) let releaseStmts = callbackBoxRelease(plan, indent: indent) if stringParams.isEmpty && hasCallbacks { var lines = setupStmts lines.append(callStmt) lines.append(contentsOf: releaseStmts) if let ret = returnExprAfter { lines.append(ret) } return lines } // String params present — wrap in withCString closures var lines: [String] = [] var scope = indent let openerPrefix = hasReturn ? "return " : "" for sp in stringParams { lines.append("\(scope)\(openerPrefix)\(cWrapOpener(sp.cName, sp.swiftName, sp.kind))") scope += " " } for stmt in setupStmts { let stripped = stmt.hasPrefix(indent) ? String(stmt.dropFirst(indent.count)) : stmt lines.append("\(scope)\(stripped)") } let strippedCall = callStmt.hasPrefix(indent) ? String(callStmt.dropFirst(indent.count)) : callStmt lines.append("\(scope)\(strippedCall)") for stmt in releaseStmts { let stripped = stmt.hasPrefix(indent) ? String(stmt.dropFirst(indent.count)) : stmt lines.append("\(scope)\(stripped)") } if let ret = returnExprAfter { let stripped = ret.hasPrefix(indent) ? String(ret.dropFirst(indent.count)) : ret lines.append("\(scope)\(stripped)") } for _ in stringParams { scope = String(scope.dropLast(4)) lines.append("\(scope)}") } return lines } private func renderThrowingBody(_ plan: CallablePlan, indent: String) -> [String] { let (cCall, stringParams) = cArguments(plan, error: true) let hasReturn = plan.returnMapping != nil let errorDecl = "\(indent)var error: UnsafeMutablePointer? = nil" let errorType = "GLibError" let cCallStmt = hasReturn ? "\(indent)let result = \(cCall)" : "\(indent)_ = \(cCall)" // When the C return is a pointer type (nullable) but the Swift type is // non-optional, force-unwrap — the error check ensures non-nil on success. // Non-pointer returns (Bool, Int, enum) are never optional in C imports. let needsUnwrap: Bool if hasReturn { switch plan.returnMapping!.marshalOut { case .objectWrap, .objectRetain, .boxedWrap, .interfaceWrap, .stringCopy: needsUnwrap = !plan.returnMapping!.swiftType.hasSuffix("?") default: needsUnwrap = false } } else { needsUnwrap = false } let returnArg = needsUnwrap ? "result!" : "result" let errorCheck = [ "\(indent)if let e = error {", "\(indent) throw \(errorType)(consuming: e)", "\(indent)}", ] let returnStmt = hasReturn ? ["\(indent)return \(marshalReturn(returnArg, mapping: plan.returnMapping!))"] : [] let setupStmts = callbackBoxSetup(plan, indent: indent) let releaseStmts = callbackBoxRelease(plan, indent: indent) if stringParams.isEmpty { return setupStmts + [errorDecl, cCallStmt] + errorCheck + releaseStmts + returnStmt } var lines: [String] = [] var scope = indent for sp in stringParams { lines.append("\(scope)return try \(cWrapOpener(sp.cName, sp.swiftName, sp.kind))") scope += " " } for stmt in setupStmts { let stripped = stmt.hasPrefix(indent) ? String(stmt.dropFirst(indent.count)) : stmt lines.append("\(scope)\(stripped)") } lines.append("\(scope)\(errorDecl)") lines.append("\(scope)\(cCallStmt)") lines += errorCheck.map { $0.hasPrefix(indent) ? scope + String($0.dropFirst(indent.count)) : $0 } for stmt in releaseStmts { let stripped = stmt.hasPrefix(indent) ? String(stmt.dropFirst(indent.count)) : stmt lines.append("\(scope)\(stripped)") } lines += returnStmt.map { $0.hasPrefix(indent) ? scope + String($0.dropFirst(indent.count)) : $0 } for _ in stringParams { scope = String(scope.dropLast(4)) lines.append("\(scope)}") } return lines } /// The effective Swift return type for a callable, including out-param returns. private func callableReturnType(_ plan: CallablePlan) -> String? { if let outReturn = outParamReturnType(plan) { return " -> \(outReturn)" } return plan.returnMapping.map { " -> \($0.swiftType)" } } private func hasOutParams(_ plan: CallablePlan) -> Bool { plan.parameters.contains(where: \.isOutParameter) } // MARK: - Callable renderers private func renderCallable(_ plan: CallablePlan) -> String { var lines: [String] = [] if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc)) } let retType = callableReturnType(plan) ?? "" let throwsKeyword = plan.throwsError ? " throws" : "" lines.append("\(spiPrefix(signatureHasRawPointer(plan)))public func \(plan.name)(\(swiftSignature(plan)))\(throwsKeyword)\(retType) {") if hasOutParams(plan) { lines.append(contentsOf: renderMultiStatementBody(plan, indent: " ")) } else if plan.throwsError { lines.append(contentsOf: renderThrowingBody(plan, indent: " ")) } else { lines.append(contentsOf: renderCallBody(plan, indent: " ")) } lines.append("}") return lines.joined(separator: "\n") + "\n" } private func renderMethod(_ plan: CallablePlan) -> [String] { var lines: [String] = [] if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc).map { " \($0)" }) } let retType = callableReturnType(plan) ?? "" let throwsKeyword = plan.throwsError ? " throws" : "" let overrideKeyword = plan.isOverride ? "override " : "" lines.append(" \(spiPrefix(signatureHasRawPointer(plan)))public \(overrideKeyword)func \(plan.name)(\(swiftSignature(plan)))\(throwsKeyword)\(retType) {") if hasOutParams(plan) { lines.append(contentsOf: renderMultiStatementBody(plan, indent: " ")) } else if plan.throwsError { lines.append(contentsOf: renderThrowingBody(plan, indent: " ")) } else { lines.append(contentsOf: renderCallBody(plan, indent: " ")) } lines.append(" }") return lines } private func renderStaticFunction(_ plan: CallablePlan) -> [String] { var lines: [String] = [] if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc).map { " \($0)" }) } let retType = callableReturnType(plan) ?? "" let throwsKeyword = plan.throwsError ? " throws" : "" lines.append(" \(spiPrefix(signatureHasRawPointer(plan)))public static func \(plan.name)(\(swiftSignature(plan)))\(throwsKeyword)\(retType) {") if hasOutParams(plan) { lines.append(contentsOf: renderMultiStatementBody(plan, indent: " ")) } else if plan.throwsError { lines.append(contentsOf: renderThrowingBody(plan, indent: " ")) } else { lines.append(contentsOf: renderCallBody(plan, indent: " ")) } lines.append(" }") return lines } /// Renders a constructor as a `convenience init`. The C constructor's returned /// instance pointer is adopted through the designated `init(takingOwnership:)` /// or `init(retaining:)`, depending on the GIR `transfer-ownership` annotation /// on the constructor's return value. private func renderConstructor(_ plan: CallablePlan) -> [String] { var lines: [String] = [] if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc).map { " \($0)" }) } let throwsKeyword = plan.throwsError ? " throws" : "" lines.append(" \(spiPrefix(signatureHasRawPointer(plan)))public convenience init(\(swiftSignature(plan)))\(throwsKeyword) {") if plan.throwsError { let indent = " " let (cCall, stringParams) = cArguments(plan, error: true) let errorCheck = [ "\(indent)if let e = error {", "\(indent) throw GLibError(consuming: e)", "\(indent)}", ] if stringParams.isEmpty { lines.append("\(indent)var error: UnsafeMutablePointer? = nil") lines.append("\(indent)let _ptr = \(cCall)") lines.append(contentsOf: errorCheck) lines.append("\(indent)\(ownInitCall(plan, "_rawPointer(_ptr!)"))") } else { lines.append("\(indent)var error: UnsafeMutablePointer? = nil") var scope = indent for i in stringParams.indices { let sp = stringParams[i] let prefix = i == 0 ? "let _result = " : "" lines.append("\(scope)\(prefix)\(cWrapOpener(sp.cName, sp.swiftName, sp.kind))") scope += " " } // `self.init` (a delegating initializer call) cannot be nested // inside a closure — even a non-escaping one. So the C call's // result is threaded back OUT through each `withCString` // closure's implicit return (a nested closure body must be a // single, un-bound expression for this to propagate — an inner // `let` declaration would make the outer closure infer `Void`), // and `self.init` runs at the outer scope once every closure // has returned. lines.append("\(scope)\(cCall)") for _ in stringParams { scope = String(scope.dropLast(4)) lines.append("\(scope)}") } lines += errorCheck lines.append("\(indent)\(ownInitCall(plan, "_rawPointer(_result!)"))") } } else { let expr = renderCallExpression(plan) lines.append(" \(ownInitCall(plan, "_rawPointer(\(expr))"))") } lines.append(" }") return lines } /// Selects `init(takingOwnership:)` or `init(retaining:)` based on the /// constructor's ownership annotation from GIR. private func ownInitCall(_ plan: CallablePlan, _ pointerExpr: String) -> String { // ownershipInit is nil for non-constructor callables; default to takingOwnership. switch plan.ownershipInit { case .takingOwnership?, .sinkingRef?: return "self.init(takingOwnership: \(pointerExpr))" case .retaining?: return "self.init(retaining: \(pointerExpr))" case nil: return "self.init(takingOwnership: \(pointerExpr))" } } /// Generates the argument expression for a callable parameter. private func marshalCallArg(_ param: ParameterPlan) -> String { switch param.mapping.marshalIn { case .direct: return param.swiftName case .numericCast: // Let the compiler infer the C-imported integer type at the call site; // this bridges width/signedness mismatches (e.g. gsize imported as Int) // portably without hard-coding the platform's C type. return "numericCast(\(param.swiftName))" case .enumRaw: // Convert our Swift enum to the C enum type as imported by Swift. // e.g., ConnectFlags → GConnectFlags(rawValue: numericCast(flags.rawValue)) let cType = param.mapping.cSwiftType return "\(cType)(rawValue: numericCast(\(param.swiftName).rawValue))" case .bitfieldRaw: let cType = param.mapping.cSwiftType return "\(cType)(rawValue: numericCast(\(param.swiftName).rawValue))" case .boolToGboolean: return "\(param.swiftName) ? 1 : 0" case .stringToC: return param.swiftName case .stringArrayToC: return param.swiftName case .objectPointer, .interfacePointer: return pointerArg(param) case .boxedPointer: return pointerArg(param) case .callbackBox(let scope, _): // The callback itself is passed as the C arg (Swift closures with // @convention(c) convert to C function pointers automatically). // The opaque data pointer goes to the user-data slot via closureDataMap. return param.swiftName case .unsupported: return "nil" } } /// Generates the C argument for an object or boxed parameter, handling /// optional (nullable) types via `.map { _instancePointer($0.pointer) }` /// so the C function receives `nil` when the Swift wrapper is `nil`. private func pointerArg(_ param: ParameterPlan) -> String { if param.mapping.swiftType.hasSuffix("?") { return "\(param.swiftName).map { _instancePointer($0.pointer) }" } return "_instancePointer(\(param.swiftName).pointer)" } /// Generates the return expression for a callable. private func marshalReturn(_ cCall: String, mapping: Mapping) -> String { switch mapping.marshalOut { case .direct: return cCall case .numericCast: // Target type is the declared Swift return type; the compiler infers // it, bridging any C width/signedness mismatch portably. return "numericCast(\(cCall))" case .gbooleanToBool: return "\(cCall) != 0" case .stringCopy(let free, _): let optional = mapping.swiftType.hasSuffix("?") switch (free, optional) { case (true, false): return "_takeString(\(cCall))" case (true, true): return "_takeStringIfPresent(\(cCall))" case (false, false): return "String(cString: \(cCall))" case (false, true): return "\(cCall).map { String(cString: $0) }" } case .objectWrap: let isOptional = mapping.swiftType.hasSuffix("?") let baseType = isOptional ? String(mapping.swiftType.dropLast()) : mapping.swiftType if isOptional { return "\(cCall).map { \(baseType)(takingOwnership: _rawPointer($0)) }" } return "\(baseType)(takingOwnership: _rawPointer(\(cCall)))" case .objectRetain: // transfer-ownership="none": the C call keeps its own reference, so // the wrapper must take a new one instead of adopting the borrowed // pointer outright (see TypeMapper.swift's `.object` case). let isOptional = mapping.swiftType.hasSuffix("?") let baseType = isOptional ? String(mapping.swiftType.dropLast()) : mapping.swiftType if isOptional { return "\(cCall).map { \(baseType)(retaining: _rawPointer($0)) }" } return "\(baseType)(retaining: _rawPointer(\(cCall)))" case .interfaceWrap(let adopt): let isOptional = mapping.swiftType.hasSuffix("?") let baseType = isOptional ? String(mapping.swiftType.dropLast()) : mapping.swiftType let ctor = adopt ? "takingOwnership" : "retaining" if isOptional { return "\(cCall).map { \(baseType)Ref(\(ctor): _rawPointer($0)) }" } return "\(baseType)Ref(\(ctor): _rawPointer(\(cCall)))" case .boxedWrap(let copy, let copyFunction): let isOptional = mapping.swiftType.hasSuffix("?") let baseType = isOptional ? String(mapping.swiftType.dropLast()) : mapping.swiftType if copy, copyFunction != nil { // transfer=none: the C getter may return a `const` pointer, which // a mutable copy/ref function cannot accept directly. Route // through the record's `init(retaining:)` (emitted whenever a // copy function exists — see `renderRecord`), which itself calls // the copy function on the mutable-cast instance pointer. // `_rawPointer` accepts both `UnsafePointer` (const) and // `OpaquePointer` overloads, so this never mismatches constness. if isOptional { return "\(cCall).map { \(baseType)(retaining: _rawPointer($0)) }" } return "\(baseType)(retaining: _rawPointer(\(cCall)))" } let arg = isOptional ? "$0" : cCall let wrapExpr = "\(baseType)(takingOwnership: _rawPointer(\(arg)))" if isOptional { return "\(cCall).map { \(wrapExpr) }" } return "\(baseType)(takingOwnership: _rawPointer(\(arg)))" case .enumFromRaw(let swiftType): // C function returns a C enum type; extract its rawValue to init our Swift enum. // e.g., ConnectFlags(rawValue: numericCast((g_something(...)).rawValue)) return "\(swiftType)(rawValue: numericCast((\(cCall)).rawValue))!" case .bitfieldFromRaw(let swiftType): // C function returns a C flags value; rebuild our OptionSet from its raw bits. return "\(swiftType)(rawValue: numericCast((\(cCall)).rawValue))" case .unsupported: return cCall } } // MARK: - Doc comment helper /// Renders a GObject property as a Swift computed property. Each accessor is /// either a one-line delegation to a generated method or a GValue-machinery /// body, per its ``PropertyAccessorPlan``. private func renderProperty(_ plan: PropertyPlan) -> [String] { var lines: [String] = [] if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc).map { " \($0)" }) } let prefix = spiPrefix(propertyHasRawPointer(plan)) let getterBody = propertyGetterBody(plan.getter, plan: plan) if let setter = plan.setter { let setterBody = propertySetterBody(setter, plan: plan) lines.append(" \(prefix)public var \(plan.swiftName): \(plan.swiftType) {") lines.append(" get {") for line in getterBody { lines.append(" \(line)") } lines.append(" }") lines.append(" set {") for line in setterBody { lines.append(" \(line)") } lines.append(" }") lines.append(" }") } else { lines.append(" \(prefix)public var \(plan.swiftName): \(plan.swiftType) {") for line in getterBody { lines.append(" \(line)") } lines.append(" }") } return lines } /// Builds the getter body for a property accessor. private func propertyGetterBody(_ accessor: PropertyAccessorPlan, plan: PropertyPlan) -> [String] { switch accessor { case .delegate(let method, _): return ["\(method)()"] case .gvalue(let typeMacro, let suffix, let hasCopyFunction): return gvalueGetterBody(swiftType: plan.swiftType, girName: plan.girName, typeMacro: typeMacro, suffix: suffix, hasCopyFunction: hasCopyFunction) } } /// Builds the setter body for a property accessor. private func propertySetterBody(_ accessor: PropertyAccessorPlan, plan: PropertyPlan) -> [String] { switch accessor { case .delegate(let method, let label): if let label { return ["\(method)(\(label): newValue)"] } return ["\(method)(newValue)"] case .gvalue(let typeMacro, let suffix, _): return gvalueSetterBody(swiftType: plan.swiftType, girName: plan.girName, typeMacro: typeMacro, suffix: suffix) } } /// Builds a GValue-machinery getter body: init a GValue, read the property into /// it, extract the Swift value (with the per-category bridge), and clean up. private func gvalueGetterBody(swiftType: String, girName: String, typeMacro: String, suffix: String, hasCopyFunction: Bool = false) -> [String] { let typeMacroSwift = camelCased(typeMacro) let resultExpr: String if suffix == "boolean" { resultExpr = "g_value_get_boolean(&gvalue) != 0" } else if suffix == "enum" { // `g_value_get_enum` returns a `gint` (Int32); generated enums use an // `Int` raw value, so bridge through `numericCast` (matching the enum // marshalling idiom used elsewhere in the renderer). resultExpr = "\(swiftType)(rawValue: numericCast(g_value_get_enum(&gvalue)))!" } else if suffix == "flags" { resultExpr = "\(swiftType)(rawValue: numericCast(g_value_get_flags(&gvalue)))" } else if suffix == "string" { let isOptional = swiftType.hasSuffix("?") if isOptional { resultExpr = "g_value_get_string(&gvalue).map { String(cString: $0) }" } else { resultExpr = "String(cString: g_value_get_string(&gvalue))" } } else if suffix == "object" { let isOptional = swiftType.hasSuffix("?") let baseType = isOptional ? String(swiftType.dropLast()) : swiftType if isOptional { resultExpr = "g_value_get_object(&gvalue).map { \(baseType)(retaining: $0) }" } else { resultExpr = "\(baseType)(retaining: g_value_get_object(&gvalue))" } } else if suffix == "boxed" { resultExpr = hasCopyFunction ? "\(swiftType)(retaining: g_value_get_boxed(&gvalue))" : "\(swiftType)(takingOwnership: g_value_get_boxed(&gvalue))" } else { resultExpr = "g_value_get_\(suffix)(&gvalue)" } return [ "var gvalue = GValue()", "g_value_init(&gvalue, \(typeMacroSwift))", "g_object_get_property(_instancePointer(pointer), \"\(girName)\", &gvalue)", "let result = \(resultExpr)", "g_value_unset(&gvalue)", "return result", ] } /// Builds a GValue-machinery setter body: pack `newValue` into a GValue and /// write it back through `g_object_set_property`. private func gvalueSetterBody(swiftType: String, girName: String, typeMacro: String, suffix: String) -> [String] { let typeMacroSwift = camelCased(typeMacro) if suffix == "string" { return [ "newValue.withCString { cstr in", " var gvalue = GValue()", " g_value_init(&gvalue, \(typeMacroSwift))", " g_value_set_string(&gvalue, cstr)", " g_object_set_property(_instancePointer(pointer), \"\(girName)\", &gvalue)", " g_value_unset(&gvalue)", "}", ] } let setCall: String if suffix == "boolean" { setCall = "g_value_set_boolean(&gvalue, newValue ? 1 : 0)" } else if suffix == "enum" || suffix == "flags" { setCall = "g_value_set_\(suffix)(&gvalue, numericCast(newValue.rawValue))" } else if suffix == "object" { setCall = swiftType.hasSuffix("?") ? "g_value_set_object(&gvalue, newValue?.pointer)" : "g_value_set_object(&gvalue, newValue.pointer)" } else if suffix == "boxed" { setCall = "g_value_set_boxed(&gvalue, newValue.pointer)" } else { setCall = "g_value_set_\(suffix)(&gvalue, newValue)" } return [ "var gvalue = GValue()", "g_value_init(&gvalue, \(typeMacroSwift))", setCall, "g_object_set_property(_instancePointer(pointer), \"\(girName)\", &gvalue)", "g_value_unset(&gvalue)", ] } private func renderDocComment(_ text: String) -> [String] { convertGtkDocToDocC(text).components(separatedBy: "\n").map { "/// \($0)" } } /// Converts raw gtk-doc markup to DocC-compatible Markdown: /// - `|[` fences → code blocks /// - `#Type`, `%CONST`, `@param` → `` `backtick` `` references /// - `%TRUE`, `%FALSE`, `%NULL` → `` `true` ``, `` `false` ``, `` `nil` `` private func convertGtkDocToDocC(_ text: String) -> String { var s = text s = s.replacing(/\|\[()?/, with: "```") s = s.replacing(/\]\|/, with: "```") s = s.replacingOccurrences(of: "%TRUE", with: "`true`") s = s.replacingOccurrences(of: "%FALSE", with: "`false`") s = s.replacingOccurrences(of: "%NULL", with: "`nil`") s = s.replacing(/[#%@]([A-Za-z_][A-Za-z0-9_]*)/) { "`\($0.1)`" } return s } /// Converts a raw GIR bitfield value string to a Swift `UInt32` literal. /// /// Negative values (masks with the high bit set) are emitted as /// `UInt32(bitPattern: Int32(…))` so they don't overflow the unsigned type. /// Positive values are passed through verbatim. private func swiftBitfieldLiteral(_ rawValue: String) -> String { if rawValue.hasPrefix("-") { return "UInt32(bitPattern: Int32(\(rawValue)))" } return rawValue }