1
0
Fork 0
gobject-generator/Sources/SwiftGtkGenCore/PlanRenderer.swift
Brendan Szymanski dac435ec89 Add Tier 4 Gdk/Gsk bindings and E3 review remediation
Generates Gdk and Gsk wrappers (330/417 and 140/183 callables), with runtime smoke tests linked against real libgtk-4. Also fixes a latent cross-module dropped-type reference bug, corrects the init(takingOwnership:) doc for no-free records, restores a precise filename safety check, and adds unit test coverage for four previously compile-gate-only branches.
2026-07-20 12:37:54 -04:00

1668 lines
70 KiB
Swift

// 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 { "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 3 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`).
/// 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 > 3 && 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
public init(consuming error: UnsafeMutablePointer<GError>) {
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 G_TYPE_INVALID: UInt = 0
public let G_TYPE_NONE: UInt = 4
public let G_TYPE_INTERFACE: UInt = 8
public let G_TYPE_CHAR: UInt = 12
public let G_TYPE_BOOLEAN: UInt = 20
public let G_TYPE_INT: UInt = 24
public let G_TYPE_UINT: UInt = 28
public let G_TYPE_LONG: UInt = 32
public let G_TYPE_ULONG: UInt = 36
public let G_TYPE_INT64: UInt = 40
public let G_TYPE_UINT64: UInt = 44
public let G_TYPE_ENUM: UInt = 48
public let G_TYPE_FLAGS: UInt = 52
public let G_TYPE_FLOAT: UInt = 56
public let G_TYPE_DOUBLE: UInt = 60
public let G_TYPE_STRING: UInt = 64
public let G_TYPE_POINTER: UInt = 68
public let G_TYPE_BOXED: UInt = 72
public let G_TYPE_PARAM: UInt = 76
public let G_TYPE_OBJECT: UInt = 80
public let G_TYPE_GTYPE: UInt = 88
public let G_TYPE_VARIANT: UInt = 96
"""
} 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<T> {
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
public let instance: UnsafeMutableRawPointer
private var isDisconnected: Bool = false
public init(id: UInt, instance: UnsafeMutableRawPointer) { self.id = id; self.instance = instance }
public mutating func disconnect() {
guard !isDisconnected else { return }
isDisconnected = true
_sgtk_signal_handler_disconnect(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.
public nonisolated func _sgtk_destroy_notify_impl(
_ data: UnsafeMutableRawPointer?,
_ closure: UnsafeMutableRawPointer?
) {
guard let data else { return }
_ = Unmanaged<AnyObject>.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")
public nonisolated func _sgtk_signal_connect_data(
_ instance: UnsafeMutableRawPointer, _ detailedSignal: UnsafePointer<CChar>,
_ cHandler: UnsafeRawPointer, _ data: UnsafeMutableRawPointer?,
_ destroyData: UnsafeRawPointer?, _ connectFlags: UInt32
) -> UInt
@_silgen_name("g_signal_handler_disconnect")
public nonisolated func _sgtk_signal_handler_disconnect(
_ instance: UnsafeMutableRawPointer, _ handlerId: UInt
)
"""
} else {
primitiveShims = ""
}
let depImports = dependencyModules.map { "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<T>`, selected when the C function's parameter is a
/// complete struct pointer.
@inline(__always)
func _instancePointer<T>(_ pointer: UnsafeMutableRawPointer) -> UnsafeMutablePointer<T> {
pointer.assumingMemoryBound(to: T.self)
}
/// Returns a `UnsafeMutableRawPointer` from a typed const C pointer.
@inline(__always)
func _rawPointer<T>(_ p: UnsafePointer<T>) -> 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<T>(_ p: UnsafeMutablePointer<T>) -> UnsafeMutableRawPointer {
UnsafeMutableRawPointer(mutating: p)
}
/// Returns a `UnsafeMutableRawPointer` from an `OpaquePointer`.
@inline(__always)
func _rawPointer(_ p: OpaquePointer) -> UnsafeMutableRawPointer {
UnsafeMutableRawPointer(p)
}
\(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 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 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"
}
// 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("public typealias \(plan.name) = \(plan.cSwiftType)")
lines.append("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("public final class \(plan.name) {")
lines.append(" 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(" 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.
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(" public init(retaining pointer: UnsafeMutableRawPointer) {")
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.joined(separator: ", ")
}
lines.append("public protocol \(plan.name)\(protocolInherits) {")
lines.append(" 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("public final class \(plan.name)Ref: \(classPrereq), \(plan.name) {")
lines.append("}")
} else {
lines.append("public final class \(plan.name)Ref: \(plan.name) {")
lines.append(" public let pointer: UnsafeMutableRawPointer")
lines.append("")
lines.append(" public init(retaining pointer: UnsafeMutableRawPointer) {")
lines.append(" g_object_ref(pointer)")
lines.append(" self.pointer = pointer")
lines.append(" }")
lines.append("")
lines.append(" 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("\(access) class \(plan.name)\(parentDecl) {")
let isRoot = plan.parent == nil
// Storage only for root classes
if isRoot {
lines.append(" 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 ? "" : "public override "
// takingOwnership init
if isRoot {
let body = plan.descendsFromInitiallyUnowned
? ["g_object_ref_sink(pointer)", "self.pointer = pointer"]
: ["self.pointer = pointer"]
lines.append(" 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(" public init(retaining pointer: UnsafeMutableRawPointer) {")
lines.append(" self.pointer = pointer")
lines.append(" \(plan.refFunc)(\(refArg))")
lines.append(" }")
} else {
lines.append(" public override init(retaining pointer: UnsafeMutableRawPointer) {")
lines.append(" \(plan.refFunc)(\(refArg))")
lines.append(" super.init(takingOwnership: pointer)")
lines.append(" }")
}
lines.append("")
}
// deinit skipped until Phase C (ownership semantics).
// A naive g_object_unref here would double-free if the C layer
// already released its reference. Proper ref-counting requires
// tracking transfer ownership from every C call path.
// 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..<allParams.count).map { "w\($0)" }.joined(separator: ", ")
let shadowBody = shadowLines.map { " \($0)" }.joined(separator: "\n")
let wrapperBody = (wrapperLines + ["box.closure(\(wrapperRefs))"]).map { " \($0)" }.joined(separator: "\n")
lines.append("@_cdecl(\"\(plan.trampolineCName)\")")
lines.append("nonisolated func \(plan.trampolineCName)(\(cDecl)) {")
lines.append(" guard let data else { return }")
lines.append(" let box = Unmanaged<_ClosureBox<\(closureType)>>.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<Name>` 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(" _sgtk_destroy_notify_impl(data, nil)")
lines.append(" }")
lines.append(" let ptr = self.pointer")
lines.append(" \(detailBody)")
lines.append(" return signalName.withCString { cName in")
lines.append(" let id = _sgtk_signal_connect_data(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 }.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: ", ")
}
/// 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)]) {
var stringParams: [(cName: String, swiftName: String)] = []
var cArgExprs: [String] = []
var outIndex = 0
// Build map: closureIndex -> callback data ptr name, so separate user-data
// params get replaced with the box pointer.
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 param.mapping.marshalIn == .stringToC {
let cName = "cString\(stringParams.count)"
stringParams.append((cName: cName, swiftName: param.swiftName))
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<CChar>? as the pointee, not
// UnsafeMutablePointer<CChar>? (that's only correct when the
// callee hands over a mutable/owned `char**`).
return "UnsafePointer<CChar>?"
}
return "UnsafeMutablePointer<CChar>?"
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<Int32>` 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<Int8>?"
}
}
/// 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 "nil"
}
}
/// Generates the expression that converts an out-param's C value to Swift.
/// `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, _):
let note = free ? " /* TODO: transfer-full out-string not freed */" : ""
if param.mapping.swiftType.hasSuffix("?") {
return "\(varName).map { String(cString: $0) }\(note)"
}
return "String(cString: \(varName)!)\(note)"
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) /* unsupported out-param marshalOut */"
}
}
/// 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<GError>? = 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)\(sp.swiftName).withCString { \(sp.cName) in")
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 = "\(sp.swiftName).withCString { \(sp.cName) in \(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)\(sp.swiftName).withCString { \(sp.cName) in")
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<GError>? = 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 \(sp.swiftName).withCString { \(sp.cName) in")
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("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" : ""
lines.append(" 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
}
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(" 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:)`,
/// which sinks a floating reference for `InitiallyUnowned` descendants.
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(" 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<GError>? = nil")
lines.append("\(indent)let __ptr = \(cCall)")
lines.append(contentsOf: errorCheck)
lines.append("\(indent)self.init(takingOwnership: _rawPointer(__ptr!))")
} else {
lines.append("\(indent)var error: UnsafeMutablePointer<GError>? = nil")
var scope = indent
for i in stringParams.indices {
let sp = stringParams[i]
let prefix = i == 0 ? "let __result = " : ""
lines.append("\(scope)\(prefix)\(sp.swiftName).withCString { \(sp.cName) in")
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)self.init(takingOwnership: _rawPointer(__result!))")
}
} else {
let expr = renderCallExpression(plan)
lines.append(" self.init(takingOwnership: _rawPointer(\(expr)))")
}
lines.append(" }")
return lines
}
/// 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 .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(let reason):
return "/* unsupported: \(reason) */"
}
}
/// 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, _):
if mapping.swiftType.hasSuffix("?") {
let freeStr = free ? "/* TODO: g_free */" : ""
return "\(cCall).map { String(cString: $0) \(freeStr) }"
}
if free {
return "String(cString: \(cCall)) /* TODO: g_free */"
}
return "String(cString: \(cCall))"
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<T>` (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 + " /* unsupported marshalOut */"
}
}
// 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 getterBody = propertyGetterBody(plan.getter, plan: plan)
if let setter = plan.setter {
let setterBody = propertySetterBody(setter, plan: plan)
lines.append(" 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(" 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):
return gvalueGetterBody(swiftType: plan.swiftType, girName: plan.girName,
typeMacro: typeMacro, suffix: suffix)
}
}
/// 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) -> [String] {
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 = "\(swiftType)(retaining: g_value_get_boxed(&gvalue))"
} else {
resultExpr = "g_value_get_\(suffix)(&gvalue)"
}
return [
"var gvalue = GValue()",
"g_value_init(&gvalue, \(typeMacro))",
"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] {
if suffix == "string" {
return [
"newValue.withCString { cstr in",
" var gvalue = GValue()",
" g_value_init(&gvalue, \(typeMacro))",
" 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, \(typeMacro))",
setCall,
"g_object_set_property(_instancePointer(pointer), \"\(girName)\", &gvalue)",
"g_value_unset(&gvalue)",
]
}
private func renderDocComment(_ text: String) -> [String] {
text.components(separatedBy: "\n").map { "/// \($0)" }
}
/// 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
}