2171 lines
96 KiB
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
|
|
}
|