// 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 gobject-generator. 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 .asyncCallable(let p): functions.append((p.starter.name, renderAsyncMethod(p, indent: "", isTopLevel: true).joined(separator: "\n") + "\n")) 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 } // Any owner with a bound GIO `*_async`/`*_finish` pair needs the async // bridge (`_sgtkAwaitAsyncReady`) emitted into this module's Support.swift. let hasAsyncCallables = plan.types.contains { typePlan in switch typePlan { case .class(let p): return !p.asyncMethods.isEmpty case .record(let p): return !p.asyncMethods.isEmpty case .interface(let p): return !p.asyncMethods.isEmpty case .asyncCallable: return true default: return false } } files["Support.swift"] = renderSupport(moduleName: plan.module, dependencyModules: plan.dependencyModules, hasSignals: hasSignals, hasCallbackBoxes: hasCallbacks, hasAsyncCallables: hasAsyncCallables) 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, hasAsyncCallables: 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 asyncSupport: String if hasAsyncCallables { asyncSupport = """ // MARK: - GIO async/await bridge /// Boxes the continuation awaiting a GIO `*_async` operation. @MainActor final class _AsyncReadyBox { let continuation: CheckedContinuation init(_ continuation: CheckedContinuation) { self.continuation = continuation } } /// The single `GAsyncReadyCallback` every generated `async` method hands to C. /// /// GIO invokes it on the thread-default main context - the main thread for /// these libraries - so `MainActor.assumeIsolated` is sound, exactly as in the /// signal trampolines. The `GAsyncResult` is only guaranteed to live for the /// duration of this call, and resuming a continuation merely schedules the /// awaiting job, so the result is retained here and released by the awaiting /// method once `*_finish` has run. The `nonisolated(unsafe)` shadow copies /// below are the same documented-safe pattern the signal trampolines use: /// the C parameters belong to this `nonisolated` closure's isolation domain, /// and Swift 6's region-based sending checker flags capturing them directly /// into the `@MainActor` closure as a potential data race even though both /// values are only ever touched here, once, on the main thread. private let _sgtkAsyncReadyCallback: GAsyncReadyCallback = { _, result, data in guard let result, let data else { return } nonisolated(unsafe) let capturedResult = UnsafeMutableRawPointer(result) nonisolated(unsafe) let box = Unmanaged<_AsyncReadyBox>.fromOpaque(data).takeRetainedValue() MainActor.assumeIsolated { g_object_ref(capturedResult) box.continuation.resume(returning: capturedResult) } } /// Runs a GIO `*_async` starter and suspends until its callback fires. /// /// - Parameter start: Invokes the C starter, passing through the callback and /// user-data arguments this helper supplies. /// - Returns: An owned `GAsyncResult` pointer the caller must `g_object_unref`. func _sgtkAwaitAsyncReady( _ start: (GAsyncReadyCallback, UnsafeMutableRawPointer) -> Void ) async -> UnsafeMutableRawPointer { await withCheckedContinuation { continuation in let box = _AsyncReadyBox(continuation) start(_sgtkAsyncReadyCallback, Unmanaged.passRetained(box).toOpaque()) } } """ } else { asyncSupport = "" } let depImports = dependencyModules.map { "@_spi(SGTKInternal) import \($0)\n" }.joined() return """ // Generated by gobject-generator. 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) } } /// Bridges a `[String]` to a NULL-terminated C `const char * const *`, freeing the copies after `body`. @inline(__always) func _withConstStringArray(_ strings: [String], _ body: (UnsafePointer?>?) 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 { buffer in try buffer.withMemoryRebound(to: (UnsafePointer?).self) { rebound in try body(rebound.baseAddress) } } } /// Bridges a `[String]` to a C `const char **` (const elements, mutable outer /// pointer), NULL-terminated, freeing the copies after `body`. @inline(__always) func _withConstElementStringArray(_ 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 { buffer in try buffer.withMemoryRebound(to: (UnsafePointer?).self) { rebound in try body(rebound.baseAddress) } } } /// Bridges wrapper instance pointers to a NULL-terminated C array of element /// pointers (`T **`), valid only for the duration of `body`. The element /// pointer type `E` is inferred from the C function being called, which is /// how both opaque (`OpaquePointer?`) and complete-struct /// (`UnsafeMutablePointer?`) element types are served by one helper. @inline(__always) func _withPointerArray(_ pointers: [UnsafeMutableRawPointer], _ body: (UnsafeMutablePointer?) throws -> R) rethrows -> R { var raw: [UnsafeMutableRawPointer?] = pointers raw.append(nil) return try raw.withUnsafeMutableBufferPointer { buffer in try buffer.withMemoryRebound(to: E.self) { rebound in try body(rebound.baseAddress) } } } /// Bridges a `[T]` of C-scalar values to a contiguous C buffer valid only for /// the duration of `body`. The buffer is a copy: only `const` C parameters /// may be bridged this way (the planner enforces that). @inline(__always) func _withScalarArray(_ values: [T], _ body: (UnsafeMutablePointer?) throws -> R) rethrows -> R { var copy = values return try copy.withUnsafeMutableBufferPointer { try body($0.baseAddress) } } \(glibError)\(gtypeConstants)\(closureBoxSupport)\(signalSupport)\(primitiveShims)\(asyncSupport) """ } // 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 .asyncCallable(let p): return (p.starter.name, renderAsyncMethod(p, indent: "", isTopLevel: true).joined(separator: "\n") + "\n") 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(" }") } // Boxed record constructors, methods, and associated functions are emitted // in the wrapper class after its memory-management members. 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 asyncMethod in plan.asyncMethods { lines.append(contentsOf: renderAsyncMethod(asyncMethod, indent: " ")) lines.append("") } for fn in plan.functions { lines.append(contentsOf: renderStaticFunction(fn)) 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.asyncMethods.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 asyncMethod in plan.asyncMethods { lines.append(contentsOf: renderAsyncMethod(asyncMethod, indent: " ")) 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 asyncMethod in plan.asyncMethods { lines.append(contentsOf: renderAsyncMethod(asyncMethod, indent: " ")) 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 ── /// How a signal handler's Swift return value is handed back across the C ABI. /// /// `nil` for a void signal. Switches on `marshalIn` - the mapping's /// Swift-into-C direction, which is exactly the direction a signal return /// travels - so the spellings match the parameter path in `renderCallable`. private struct SignalReturnBridge { /// The `@_cdecl` function's return type. let cType: String /// Returned when `data` is nil and the closure never runs. let zero: String /// `true` when `cType` is a non-Sendable pointer (e.g. /// `UnsafeMutableRawPointer`) that cannot cross `MainActor.assumeIsolated` /// as a return type. The trampoline uses a result-variable pattern instead. let needsResultVar: Bool /// Statements that evaluate `call` and `return` its C form. Emitted /// inside `MainActor.assumeIsolated`. When `needsResultVar`, `return X` /// lines are rewritten to `bridgeResult = X` by the trampoline. let emit: (_ call: String) -> [String] } private func signalReturnBridge(_ m: Mapping) -> SignalReturnBridge? { let isOptional = m.swiftType.hasSuffix("?") let nonSendable = m.cSwiftType.replacingOccurrences(of: "?", with: "").hasPrefix("Unsafe") switch m.marshalIn { case .boolToGboolean: return .init(cType: "Int32", zero: "0", needsResultVar: false, emit: { ["return (\($0)) ? 1 : 0"] }) case .direct: return .init(cType: m.cSwiftType, zero: "0", needsResultVar: nonSendable, emit: { ["return \($0)"] }) case .numericCast: return .init(cType: m.cSwiftType, zero: "0", needsResultVar: nonSendable, emit: { ["return numericCast(\($0))"] }) case .enumRaw, .bitfieldRaw: return .init( cType: m.cSwiftType, zero: ".init(rawValue: 0)", needsResultVar: nonSendable, emit: { ["return \(m.cSwiftType)(rawValue: numericCast((\($0)).rawValue))"] }) case .stringToC: // The signal's return is an owned `char*`; duplicate so the Swift // String's buffer is not handed to C. return .init(cType: "UnsafeMutablePointer?", zero: "nil", needsResultVar: true, emit: { ["return g_strdup(\($0))"] }) case .objectPointer(_), .interfacePointer(_): // transfer=full (`.objectWrap`/`.interfaceWrap(adopt:)`) means C takes // ownership of a reference, so add one; transfer=none // (`.objectRetain`) hands over a borrowed pointer unchanged. let transfersFull: Bool switch m.marshalOut { case .objectRetain: transfersFull = false default: transfersFull = true } let refLine = transfersFull ? ["_ = g_object_ref(r.pointer)"] : [] // UnsafeMutableRawPointer needs its own path (r.pointer, not // _instancePointer) and is never Sendable. if m.cSwiftType.replacingOccurrences(of: "?", with: "") == "UnsafeMutableRawPointer" { if isOptional { return .init(cType: m.cSwiftType, zero: "nil", needsResultVar: true, emit: { call in ["guard let r = \(call) else { return nil }"] + refLine + ["return r.pointer"] }) } return .init(cType: m.cSwiftType, zero: "nil", needsResultVar: true, emit: { call in ["let r = \(call)"] + refLine + ["return r.pointer"] }) } if isOptional { return .init(cType: m.cSwiftType, zero: "nil", needsResultVar: nonSendable, emit: { call in ["guard let r = \(call) else { return nil }"] + refLine + ["return _instancePointer(r.pointer)"] }) } return .init(cType: m.cSwiftType, zero: "nil", needsResultVar: nonSendable, emit: { call in ["let r = \(call)"] + refLine + ["return _instancePointer(r.pointer)"] }) default: return nil } } /// 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.. \($0.cType)" } ?? "") {") lines.append(" guard let data else { return \(bridge?.zero ?? "") }") lines.append(" let box = Unmanaged<_ClosureBox<\(closureType)>>.fromOpaque(data).takeUnretainedValue()") lines.append(shadowBody) if let bridge, bridge.needsResultVar { lines.append(" nonisolated(unsafe) var bridgeResult: \(bridge.cType) = \(bridge.zero)") lines.append(" MainActor.assumeIsolated {") for var line in wrapperBody.split(separator: "\n", omittingEmptySubsequences: false) { // Drop the 8-space lead for processing, then re-prefix. let body = String(line.dropFirst(line.hasPrefix(" ") ? 8 : 0)) if body.hasPrefix("return ") { line = Substring(" bridgeResult = " + body.dropFirst(7)) } else if body.contains("return nil") { // guard let r = ... else { return nil } — inside a Void // assumeIsolated, bare `return` is correct. line = Substring(" " + body.replacingOccurrences(of: "return nil", with: "return")) } else { line = Substring(" " + body) } lines.append(String(line)) } lines.append(" }") lines.append(" return bridgeResult") } else if let bridge { lines.append(" return MainActor.assumeIsolated { () -> \(bridge.cType) in") lines.append(wrapperBody) lines.append(" }") } else { 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))" // Inbound signal parameters are borrowed pointers from C. The // `consumingRefFunction` payload describes the OUTBOUND call-argument // direction only and is deliberately ignored here. 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" case .numericCast: // The raw C parameter is `cSwiftType` (e.g. the literal `gint64` // typedef), which is not always the same nominal type as the // public `swiftType` (`Int`) exposed on the closure. `numericCast` // alone can't infer its target here (this becomes a standalone // `let` binding, with no call-site context to infer from), so // construct the public type directly instead — equivalent to // `numericCast` for the always-non-optional scalar types this // marshalling case covers. let baseType = p.mapping.swiftType.hasSuffix("?") ? String(p.mapping.swiftType.dropLast()) : p.mapping.swiftType return "\(baseType)(\(rawName))" 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)" let cRet = plan.returnMapping.flatMap(signalReturnBridge)?.cType ?? "Void" // 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)) -> \(cRet)), 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 && $0.asyncRole == 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, constArray, constElementStringArray, pointerArray, scalarArray } /// Generates the closure opener for a string/array parameter: `withCString`, /// `_withOptionalCString`, `_withStringArray`, `_withConstStringArray`, /// `_withConstElementStringArray`, `_withPointerArray`, or `_withScalarArray`. 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" case .constArray: return "_withConstStringArray(\(swiftName)) { \(cName) in" case .constElementStringArray: return "_withConstElementStringArray(\(swiftName)) { \(cName) in" case .pointerArray: return "_withPointerArray(\(swiftName).map { $0.pointer }) { \(cName) in" case .scalarArray: return "_withScalarArray(\(swiftName)) { \(cName) in" } } /// The C-buffer wrapping kind for an array `MarshalIn`, or `nil` when the /// parameter is not an array. private func arrayWrapKind(_ marshalIn: MarshalIn) -> CWrapKind? { switch marshalIn { case .stringArrayToC: return .array case .stringConstArrayToC: return .constArray case .stringConstElementArrayToC: return .constElementStringArray case .objectArrayToC: return .pointerArray case .scalarArrayToC: return .scalarArray default: return nil } } /// 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 let kind = arrayWrapKind(param.mapping.marshalIn) { let cName = "cArray\(stringParams.count)" stringParams.append((cName: cName, swiftName: param.swiftName, kind: kind)) cArgExprs.append(cName) } else if param.asyncRole == .callback { cArgExprs.append("_asyncCallback") } else if param.asyncRole == .userData { cArgExprs.append("_asyncUserData") } 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) } /// Renders one GIO `*_async`/`*_finish` pair as a single Swift `async` /// method. Modeled on `renderMethod`/`renderStaticFunction`/`renderCallable` /// - the trailing `GAsyncReadyCallback`/`user_data` pair is bridged through /// `_sgtkAwaitAsyncReady`, and the result is unpacked via the `*_finish` /// sibling, already planned on the same owner. /// /// - Parameters: /// - plan: The starter/finish pair. /// - indent: `""` for a top-level free function, `" "` for a class/ /// record method or an interface protocol-extension default. /// - isTopLevel: `true` suppresses the `static` keyword — free functions /// are never `static` in Swift even though `CallablePlan.isStatic` is /// `true` for them (see `planFunction`). private func renderAsyncMethod(_ plan: AsyncCallablePlan, indent: String, isTopLevel: Bool = false) -> [String] { var lines: [String] = [] if let doc = plan.starter.doc { lines.append(contentsOf: renderDocComment(doc).map { indent.isEmpty ? $0 : "\(indent)\($0)" }) } let staticKeyword = (!isTopLevel && plan.starter.isStatic) ? "static " : "" let throwsKeyword = plan.finish.throwsError ? " throws" : "" let retType = callableReturnType(plan.finish) ?? "" let hidden = signatureHasRawPointer(plan.starter) || signatureHasRawPointer(plan.finish) lines.append("\(indent)\(spiPrefix(hidden))public \(staticKeyword)func \(plan.starter.name)(\(swiftSignature(plan.starter))) async\(throwsKeyword)\(retType) {") let bodyIndent = indent + " " lines.append("\(bodyIndent)let _asyncResult = await _sgtkAwaitAsyncReady { _asyncCallback, _asyncUserData in") lines.append(contentsOf: renderCallBody(plan.starter, indent: bodyIndent + " ")) lines.append("\(bodyIndent)}") lines.append("\(bodyIndent)defer { g_object_unref(_asyncResult) }") // Rule 3 (the recognizer) guarantees exactly one such parameter exists. let finishResultParam = plan.finish.parameters.first { !$0.isInstanceParameter && !$0.isOutParameter && $0.synthesizedLengthOf == nil } let ownerPrefix = plan.finishOwner.map { "\($0)." } ?? "" let tryKeyword = plan.finish.throwsError ? "try " : "" let returnKeyword = callableReturnType(plan.finish) != nil ? "return " : "" let resultArg = finishResultParam.map { "\($0.swiftName): AsyncResultRef(retaining: _asyncResult)" } ?? "" lines.append("\(bodyIndent)\(returnKeyword)\(tryKeyword)\(ownerPrefix)\(plan.finish.name)(\(resultArg))") lines.append("\(indent)}") return lines } // 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, .stringConstArrayToC, .stringConstElementArrayToC, .objectArrayToC, .scalarArrayToC: return param.swiftName case .objectPointer(let consumingRefFunction), .interfacePointer(let consumingRefFunction): return pointerArg(param, consumingRefFunction: consumingRefFunction) case .boxedPointer(let consumingCopyFunction, let copyReturnsVoid): return pointerArg(param, consumingCopyFunction: consumingCopyFunction, consumingCopyReturnsVoid: copyReturnsVoid) 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, interface, or boxed parameter, /// handling optional (nullable) types via `.map { ... }` so the C function /// receives `nil` when the Swift wrapper is `nil`. /// /// Transfer-full object inputs receive an extra reference. Transfer-full /// boxed inputs receive a copied value; void-returning copy/ref functions are /// called for their side effect and then pass the original pointer. private func pointerArg(_ param: ParameterPlan, consumingRefFunction: String? = nil, consumingCopyFunction: String? = nil, consumingCopyReturnsVoid: Bool = false) -> String { func arg(_ pointerExpr: String) -> String { if let ref = consumingRefFunction { return "_instancePointer(_rawPointer(\(ref)(_instancePointer(\(pointerExpr)))))" } if let copy = consumingCopyFunction { if consumingCopyReturnsVoid { return "{ let pointer = _instancePointer(\(pointerExpr)); \(copy)(pointer); return pointer }()" } return "_instancePointer(_rawPointer(\(copy)(_instancePointer(\(pointerExpr)))))" } return "_instancePointer(\(pointerExpr))" } if param.mapping.swiftType.hasSuffix("?") { return "\(param.swiftName).map { \(arg("$0.pointer")) }" } return arg("\(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 if suffix == "int64" || suffix == "uint64" { // `g_value_get_int64`/`_uint64` return the literal `gint64`/`guint64` // typedef, which glib defines as `long long` on some platforms and // plain `long` on others (see `TypeMapper.int64Mapping`) — bridge // through `numericCast` so this compiles regardless of which one // the Clang importer resolved it to. resultExpr = "numericCast(g_value_get_\(suffix)(&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 if suffix == "int64" || suffix == "uint64" { // See the getter's matching branch: `g_value_set_int64`/`_uint64` // take the literal `gint64`/`guint64` typedef, which is not always // the same nominal Swift type as the public `Int`/`UInt` property. setCall = "g_value_set_\(suffix)(&gvalue, numericCast(newValue))" } 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 }