1
0
Fork 0
gobject-generator/Sources/GObjectGeneratorCore/PlanRenderer.swift

2171 lines
96 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 { "@_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<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 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<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
@_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<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")
@_spi(SGTKInternal) public nonisolated func _sgtkSignalConnectData(
_ instance: UnsafeMutableRawPointer, _ detailedSignal: UnsafePointer<CChar>,
_ 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<UnsafeMutableRawPointer, Never>
init(_ continuation: CheckedContinuation<UnsafeMutableRawPointer, Never>) {
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<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)
}
/// Copies a transfer-full C string into a Swift `String` and frees the source.
@inline(__always)
func _takeString(_ p: UnsafeMutablePointer<CChar>!) -> 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<CChar>?) -> 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<R>(_ s: String?, _ body: (UnsafePointer<CChar>?) 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<R>(_ strings: [String], _ body: (UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?) throws -> R) rethrows -> R {
var cStrings: [UnsafeMutablePointer<CChar>?] = 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<R>(_ strings: [String], _ body: (UnsafePointer<UnsafePointer<CChar>?>?) throws -> R) rethrows -> R {
var cStrings: [UnsafeMutablePointer<CChar>?] = 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<CChar>?).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<R>(_ strings: [String], _ body: (UnsafeMutablePointer<UnsafePointer<CChar>?>?) throws -> R) rethrows -> R {
var cStrings: [UnsafeMutablePointer<CChar>?] = 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<CChar>?).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<GtkWidget>?`) element types are served by one helper.
@inline(__always)
func _withPointerArray<E, R>(_ pointers: [UnsafeMutableRawPointer], _ body: (UnsafeMutablePointer<E>?) 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<T, R>(_ values: [T], _ body: (UnsafeMutablePointer<T>?) 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<CChar>?", 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..<allParams.count).map { "w\($0)" }.joined(separator: ", ")
let shadowBody = shadowLines.map { " \($0)" }.joined(separator: "\n")
let bridge = plan.returnMapping.flatMap(signalReturnBridge)
let call = "box.closure(\(wrapperRefs))"
let tail = bridge.map { $0.emit(call) } ?? [call]
let wrapperBody = (wrapperLines + tail).map { " \($0)" }.joined(separator: "\n")
lines.append("@_cdecl(\"\(plan.trampolineCName)\")")
lines.append("nonisolated func \(plan.trampolineCName)(\(cDecl))\(bridge.map { " -> \($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"
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)"
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<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 "UnsafeMutablePointer<Int8>?"
}
}
/// `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<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)\(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<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 \(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<GError>? = 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<GError>? = 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<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
}
}
// MARK: - Doc comment helper
/// Renders a GObject property as a Swift computed property. Each accessor is
/// either a one-line delegation to a generated method or a GValue-machinery
/// body, per its ``PropertyAccessorPlan``.
private func renderProperty(_ plan: PropertyPlan) -> [String] {
var lines: [String] = []
if let doc = plan.doc {
lines.append(contentsOf: renderDocComment(doc).map { " \($0)" })
}
let prefix = spiPrefix(propertyHasRawPointer(plan))
let getterBody = propertyGetterBody(plan.getter, plan: plan)
if let setter = plan.setter {
let setterBody = propertySetterBody(setter, plan: plan)
lines.append(" \(prefix)public var \(plan.swiftName): \(plan.swiftType) {")
lines.append(" get {")
for line in getterBody { lines.append(" \(line)") }
lines.append(" }")
lines.append(" set {")
for line in setterBody { lines.append(" \(line)") }
lines.append(" }")
lines.append(" }")
} else {
lines.append(" \(prefix)public var \(plan.swiftName): \(plan.swiftType) {")
for line in getterBody { lines.append(" \(line)") }
lines.append(" }")
}
return lines
}
/// Builds the getter body for a property accessor.
private func propertyGetterBody(_ accessor: PropertyAccessorPlan, plan: PropertyPlan) -> [String] {
switch accessor {
case .delegate(let method, _):
return ["\(method)()"]
case .gvalue(let typeMacro, let suffix, let hasCopyFunction):
return gvalueGetterBody(swiftType: plan.swiftType, girName: plan.girName,
typeMacro: typeMacro, suffix: suffix, hasCopyFunction: hasCopyFunction)
}
}
/// Builds the setter body for a property accessor.
private func propertySetterBody(_ accessor: PropertyAccessorPlan, plan: PropertyPlan) -> [String] {
switch accessor {
case .delegate(let method, let label):
if let label { return ["\(method)(\(label): newValue)"] }
return ["\(method)(newValue)"]
case .gvalue(let typeMacro, let suffix, _):
return gvalueSetterBody(swiftType: plan.swiftType, girName: plan.girName,
typeMacro: typeMacro, suffix: suffix)
}
}
/// Builds a GValue-machinery getter body: init a GValue, read the property into
/// it, extract the Swift value (with the per-category bridge), and clean up.
private func gvalueGetterBody(swiftType: String, girName: String,
typeMacro: String, suffix: String, hasCopyFunction: Bool = false) -> [String] {
let typeMacroSwift = camelCased(typeMacro)
let resultExpr: String
if suffix == "boolean" {
resultExpr = "g_value_get_boolean(&gvalue) != 0"
} else if suffix == "enum" {
// `g_value_get_enum` returns a `gint` (Int32); generated enums use an
// `Int` raw value, so bridge through `numericCast` (matching the enum
// marshalling idiom used elsewhere in the renderer).
resultExpr = "\(swiftType)(rawValue: numericCast(g_value_get_enum(&gvalue)))!"
} else if suffix == "flags" {
resultExpr = "\(swiftType)(rawValue: numericCast(g_value_get_flags(&gvalue)))"
} else if suffix == "string" {
let isOptional = swiftType.hasSuffix("?")
if isOptional {
resultExpr = "g_value_get_string(&gvalue).map { String(cString: $0) }"
} else {
resultExpr = "String(cString: g_value_get_string(&gvalue))"
}
} else if suffix == "object" {
let isOptional = swiftType.hasSuffix("?")
let baseType = isOptional ? String(swiftType.dropLast()) : swiftType
if isOptional {
resultExpr = "g_value_get_object(&gvalue).map { \(baseType)(retaining: $0) }"
} else {
resultExpr = "\(baseType)(retaining: g_value_get_object(&gvalue))"
}
} else if suffix == "boxed" {
resultExpr = hasCopyFunction
? "\(swiftType)(retaining: g_value_get_boxed(&gvalue))"
: "\(swiftType)(takingOwnership: g_value_get_boxed(&gvalue))"
} else {
resultExpr = "g_value_get_\(suffix)(&gvalue)"
}
return [
"var gvalue = GValue()",
"g_value_init(&gvalue, \(typeMacroSwift))",
"g_object_get_property(_instancePointer(pointer), \"\(girName)\", &gvalue)",
"let result = \(resultExpr)",
"g_value_unset(&gvalue)",
"return result",
]
}
/// Builds a GValue-machinery setter body: pack `newValue` into a GValue and
/// write it back through `g_object_set_property`.
private func gvalueSetterBody(swiftType: String, girName: String,
typeMacro: String, suffix: String) -> [String] {
let typeMacroSwift = camelCased(typeMacro)
if suffix == "string" {
return [
"newValue.withCString { cstr in",
" var gvalue = GValue()",
" g_value_init(&gvalue, \(typeMacroSwift))",
" g_value_set_string(&gvalue, cstr)",
" g_object_set_property(_instancePointer(pointer), \"\(girName)\", &gvalue)",
" g_value_unset(&gvalue)",
"}",
]
}
let setCall: String
if suffix == "boolean" {
setCall = "g_value_set_boolean(&gvalue, newValue ? 1 : 0)"
} else if suffix == "enum" || suffix == "flags" {
setCall = "g_value_set_\(suffix)(&gvalue, numericCast(newValue.rawValue))"
} else if suffix == "object" {
setCall = swiftType.hasSuffix("?")
? "g_value_set_object(&gvalue, newValue?.pointer)"
: "g_value_set_object(&gvalue, newValue.pointer)"
} else if suffix == "boxed" {
setCall = "g_value_set_boxed(&gvalue, newValue.pointer)"
} else {
setCall = "g_value_set_\(suffix)(&gvalue, newValue)"
}
return [
"var gvalue = GValue()",
"g_value_init(&gvalue, \(typeMacroSwift))",
setCall,
"g_object_set_property(_instancePointer(pointer), \"\(girName)\", &gvalue)",
"g_value_unset(&gvalue)",
]
}
private func renderDocComment(_ text: String) -> [String] {
convertGtkDocToDocC(text).components(separatedBy: "\n").map { "/// \($0)" }
}
/// Converts raw gtk-doc markup to DocC-compatible Markdown:
/// - `|[<!-- language="C" -->` 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
}