1
0
Fork 0
gobject-generator/Sources/SwiftGtkGenCore/Planner.swift
Brendan Szymanski 5abb48e889 Fix Phase D signal/callback remediation defects
Destroy-notify ABI fixed to GClosureNotify's real 2-arg signature and
wired into every connect method (was leaking every _ClosureBox, and
UB on non-x86_64 with the wrong arg count). Trampolines restore
MainActor isolation via assumeIsolated, with narrowly-scoped
nonisolated(unsafe) shadow copies to satisfy Swift 6's sending
checker. Interface-signal rendering implemented and unit-tested.
Dead code removed (SignalHandlePlan), destroyTrampoline made
non-optional, D7 deferral documented in-code. CoverageStats gained
boundCallbacks/boundSignals counters. Added SignalGenerationTests,
InterfaceSignalGenerationTests, and CallbackGenerationTests (12 new
tests, 187/187 total). Fixed the dead nonDetailedSignal smoke test to
actually mutate a property and assert the closure fired.

Callback-param planner-side binding (D4.3) stays disabled: enabling
it trips a genuine Swift compiler crash on g_qsort_with_data's
GCompareDataFunc parameter. The renderer-side box setup/release logic
is implemented and unit-tested by constructing plans directly,
bypassing the blocked planner path.

Verified: swift test (187/187), compile-gate.sh 1 --fresh (PASS),
smoke-test.sh --fresh (18/18).
2026-07-18 20:45:56 -04:00

1463 lines
67 KiB
Swift

// Phase D7 (per-signature scope=notified destroy trampolines): DEFERRED.
// Verified zero scope="notified" callback params flow through the plan
// layer in tier 1 (GLib + GObject). All notified scopes are on
// g_signal_connect_data (shortcircuited by the @_silgen_name shim in
// renderSupport) or on callables whose callback params are skipped with
// .callbackWithoutUserData until D4.3 lands. Deferral is plan-sanctioned
// per local://phase-d-signals-callbacks-plan.md §D7 contingency.
// When a tier-1 callable with a notified callback param surfaces,
// renderCallable's callback-box branch must populate
// destroyTrampoline="<trampoline name>" and emit a per-signature
// @_cdecl destroy trampoline.
// Planner.swift
// The binding planner: walks a parsed GIR namespace, resolves every type
// through the registry and TypeMapper, and produces either a complete
// `TypePlan` or a `SkipEntry` with a machine-readable reason.
//
// This is where Rule 3 of the rearchitecture lives: if any part of a symbol
// cannot be planned, the WHOLE symbol is skipped with a reason. There is no
// code path that emits a partially-understood binding.
import Foundation
// MARK: - Module Planner
/// Plans every type in every namespace of a multi-package analysis, producing
/// a `ModulePlan` per generated Swift module.
///
/// - Parameters:
/// - analysis: The resolved multi-package analysis with parsed repositories.
/// - registry: The global type registry built from those repositories.
/// - Returns: A dictionary mapping Swift module name to its `ModulePlan`.
public func planModules(
analysis: MultiPackageAnalysis,
registry: TypeRegistry
) -> [String: ModulePlan] {
var modulePlans: [String: ModulePlan] = [:]
for (moduleName, repo) in analysis.repositories {
let ns = repo.namespaces.first { $0.name == moduleName } ?? repo.namespaces.first
guard let namespace = ns else { continue }
let context = MapContext(
registry: registry,
currentModule: moduleName,
currentNamespace: namespace.name
)
let plan = planNamespace(namespace, context: context)
modulePlans[moduleName] = plan
}
return modulePlans
}
// MARK: - Namespace-level planning
/// Plans every type in a single namespace, delegating to per-category helpers.
private func planNamespace(_ ns: Namespace, context: MapContext) -> ModulePlan {
var types: [TypePlan] = []
var skips: [SkipEntry] = []
var boundTypes = 0
var totalTypes = 0
var boundCallables = 0
var totalCallables = 0
var boundCallbacks = 0
var totalCallbacks = 0
var boundSignals = 0
var totalSignals = 0
// Enumerations
for enumeration in ns.enumerations {
totalTypes += 1; boundTypes += planEnum(into: &types, skips: &skips, enumeration: enumeration, context: context)
}
// Bitfields
for bitfield in ns.bitfields {
totalTypes += 1; boundTypes += planBit(into: &types, skips: &skips, bitfield: bitfield, context: context)
}
// Constants
// Case conversion can merge distinct GIR names (`CSET_A_2_Z` and
// `CSET_a_2_z` both become `csetA2Z`), so collisions are detected here
// and the later occurrence is skipped.
var seenConstantNames: Set<String> = []
for constant in ns.constants {
totalTypes += 1
boundTypes += planConst(into: &types, skips: &skips, seen: &seenConstantNames,
constant: constant, context: context)
}
// Aliases
for alias in ns.aliases {
totalTypes += 1; boundTypes += planAliasType(into: &types, skips: &skips, alias: alias, context: context)
}
for klass in ns.classes {
totalTypes += 1
boundTypes += skipClass(into: &skips, into: &types,
boundCallables: &boundCallables, totalCallables: &totalCallables,
klass: klass, namespace: ns.name, context: context)
if case .class(let cp) = types.last { totalSignals += klass.signals.filter(\.symbolInfo.isBindable).count; boundSignals += cp.signals.count }
}
for iface in ns.interfaces {
totalTypes += 1; boundTypes += skipInterface(into: &skips, into: &types, iface: iface, namespace: ns.name, context: context)
if case .interface(let ip) = types.last { totalSignals += iface.signals.filter(\.symbolInfo.isBindable).count; boundSignals += ip.signals.count }
}
for record in ns.records {
totalTypes += 1; boundTypes += skipRecord(into: &skips, into: &types, record: record, namespace: ns.name, context: context)
}
for callback in ns.callbacks {
totalCallbacks += 1
totalTypes += 1; boundTypes += planCallbackType(into: &skips, into: &types,
callback: callback, namespace: ns.name, context: context)
if case .callback(_) = types.last { boundCallbacks += 1 }
}
for fn in ns.functions {
totalCallables += 1
boundCallables += planOrSkipFunction(into: &skips, into: &types, fn: fn, namespace: ns.name, context: context)
}
// Cross-category name collision detection
// Constants and functions can case-fold to the same Swift name
// (e.g. ATOMIC_REF_COUNT_INIT and g_atomic_ref_count_init both map to
// atomicRefCountInit). Functions (callables) win over constants since
// they are typically more commonly used. Intra-category collisions
// (constant-vs-constant, callable-vs-callable) use GIR declaration order.
do {
// Pass 1: collect all callable names (callables have priority)
var callableNames: Set<String> = []
for typePlan in types {
if case .callable(let plan) = typePlan {
callableNames.insert(plan.name)
}
}
// Pass 2: filter with priority
var seenNames: Set<String> = []
var filtered: [TypePlan] = []
var lostTypes = 0
var lostCallables = 0
for typePlan in types {
switch typePlan {
case .constant(let plan):
// Constant loses if a callable has the same name (callables
// have priority), or if another constant already claimed it.
if callableNames.contains(plan.name) {
skips.append(SkipEntry(symbol: "\(ns.name).\(plan.girName)",
cIdentifier: plan.girName,
reason: .nameCollision,
detail: "Swift name '\(plan.name)' conflicts with a function in this module"))
lostTypes += 1
} else if seenNames.contains(plan.name) {
skips.append(SkipEntry(symbol: "\(ns.name).\(plan.girName)",
cIdentifier: plan.girName,
reason: .nameCollision,
detail: "Swift name '\(plan.name)' already taken by an earlier constant"))
lostTypes += 1
} else {
seenNames.insert(plan.name)
filtered.append(typePlan)
}
case .callable(let plan):
// Callables only lose to earlier callables
if seenNames.contains(plan.name) {
skips.append(SkipEntry(symbol: "\(ns.name).\(plan.cIdentifier)",
cIdentifier: plan.cIdentifier,
reason: .nameCollision,
detail: "Swift name '\(plan.name)' already taken by an earlier symbol"))
lostCallables += 1
} else {
seenNames.insert(plan.name)
filtered.append(typePlan)
}
default:
filtered.append(typePlan)
}
}
types = filtered
boundTypes -= lostTypes
boundCallables -= lostCallables
}
// Post-pass: filter each interface's protocol requirements to only those
// whose Swift signature is witnessable by ALL implementing classes.
types = filterInterfaceRequirements(types, skips: &skips)
let coverage = CoverageStats(
boundCallables: boundCallables, totalCallables: totalCallables,
boundTypes: boundTypes, totalTypes: totalTypes,
boundCallbacks: boundCallbacks, totalCallbacks: totalCallbacks,
boundSignals: boundSignals, totalSignals: totalSignals
)
return ModulePlan(module: context.currentModule, types: types, skips: skips, coverage: coverage)
}
// MARK: - Interface conformance reconciliation
/// Canonical key for comparing Swift functional signatures.
/// Excludes instance parameters and out-params, matching `swiftSignature`.
private func signatureKey(_ plan: CallablePlan) -> String {
let retType = plan.returnMapping?.swiftType ?? "Void"
let params = plan.parameters
.filter { !$0.isInstanceParameter && !$0.isOutParameter }
.map { "\($0.swiftName):\($0.mapping.swiftType)" }
.joined(separator: ",")
return "throws:\(plan.throwsError)|ret:\(retType)|(\(params))"
}
/// Post-pass: drops interface protocol requirements whose Swift signature is
/// NOT witnessable by every implementing class (where the class planned a method
/// with the same GIR name but a different Swift return/parameter type).
/// Dropped requirements are recorded as skip entries.
func filterInterfaceRequirements(
_ types: [TypePlan], skips: inout [SkipEntry]
) -> [TypePlan] {
// Build interface name implementer ClassPlans
var implementersByInterface: [String: [ClassPlan]] = [:]
for case .class(let plan) in types {
for iface in plan.interfaces {
implementersByInterface[iface, default: []].append(plan)
}
}
var result: [TypePlan] = []
for typePlan in types {
guard case .interface(let plan) = typePlan else {
result.append(typePlan)
continue
}
let implementers = implementersByInterface[plan.qualifiedName] ?? []
guard !implementers.isEmpty else {
// Orphan interface: no class implements it; keep all requirements.
result.append(.interface(plan))
continue
}
var filteredMethods: [CallablePlan] = []
for req in plan.methods {
let reqKey = signatureKey(req)
let witnessedByAll = implementers.allSatisfy { implPlan in
implPlan.methods.contains { candidate in
candidate.name == req.name && signatureKey(candidate) == reqKey
}
}
if witnessedByAll {
filteredMethods.append(req)
} else {
skips.append(SkipEntry(
symbol: "\(plan.qualifiedName).\(req.name)",
cIdentifier: req.cIdentifier,
reason: .interfaceMethodSignatureDrift,
detail: "Method '\(req.name)' on interface '\(plan.qualifiedName)' has no consistently-witnessed implementation across all implementing classes; dropped from protocol requirement"
))
}
}
if filteredMethods.count == plan.methods.count {
result.append(.interface(plan))
} else {
let newPlan = InterfacePlan(
name: plan.name, cType: plan.cType,
prereqs: plan.prereqs,
getTypeFunction: plan.getTypeFunction,
methods: filteredMethods,
properties: plan.properties,
qualifiedName: plan.qualifiedName,
doc: plan.doc
)
result.append(.interface(newPlan))
}
}
return result
}
// MARK: - Per-category planning (returns 1 for bound, 0 for skipped)
private func planEnum(
into types: inout [TypePlan], skips: inout [SkipEntry],
enumeration: Enumeration, context: MapContext
) -> Int {
let fullName = "\(context.currentNamespace).\(enumeration.name)"
if let skip = checkBindable(enumeration.symbolInfo, fullName: fullName, cIdentifier: enumeration.cType) {
skips.append(skip); return 0
}
types.append(.enumeration(planEnumeration(enumeration, context: context)))
return 1
}
private func planBit(
into types: inout [TypePlan], skips: inout [SkipEntry],
bitfield: Bitfield, context: MapContext
) -> Int {
let fullName = "\(context.currentNamespace).\(bitfield.name)"
if let skip = checkBindable(bitfield.symbolInfo, fullName: fullName, cIdentifier: bitfield.cType) {
skips.append(skip); return 0
}
types.append(.bitfield(planBitfield(bitfield, context: context)))
return 1
}
private func planConst(
into types: inout [TypePlan], skips: inout [SkipEntry], seen: inout Set<String>,
constant: Constant, context: MapContext
) -> Int {
let fullName = "\(context.currentNamespace).\(constant.name)"
if let plan = planConstant(constant, context: context) {
if !seen.insert(plan.name).inserted {
skips.append(SkipEntry(symbol: fullName, cIdentifier: constant.name,
reason: .nameCollision,
detail: "Swift name '\(plan.name)' already taken by an earlier constant"))
return 0
}
types.append(.constant(plan)); return 1
}
skips.append(SkipEntry(symbol: fullName, cIdentifier: constant.name,
reason: .configIgnored, detail: "constant type not mappable"))
return 0
}
private func planAliasType(
into types: inout [TypePlan], skips: inout [SkipEntry],
alias: Alias, context: MapContext
) -> Int {
let planResult = planAlias(alias, context: context)
switch planResult {
case .success(let plan):
types.append(.alias(plan)); return 1
case .skip(let entry):
skips.append(entry); return 0
}
}
// MARK: - Skip helpers for not-yet-planned categories
private func checkBindable(_ info: SymbolInfo, fullName: String, cIdentifier: String) -> SkipEntry? {
if !info.isBindable {
let reason: SkipReason = info.shadowedBy != nil ? .shadowedSymbol
: !info.isIntrospectable ? .notIntrospectable
: .deprecatedRemoved
return SkipEntry(symbol: fullName, cIdentifier: cIdentifier, reason: reason, detail: reason.rawValue)
}
return nil
}
private func skipClass(
into skips: inout [SkipEntry], into types: inout [TypePlan],
boundCallables: inout Int, totalCallables: inout Int,
klass: Class, namespace: String, context: MapContext
) -> Int {
let fullName = "\(namespace).\(klass.name)"
// Every constructor/method/static function is a callable considered for
// coverage, whether or not it ends up planned.
totalCallables += klass.constructors.count + klass.methods.count + klass.functions.count
if !klass.symbolInfo.isBindable {
skips.append(SkipEntry(symbol: fullName, cIdentifier: klass.cType,
reason: .notIntrospectable, detail: "non-introspectable class"))
return 0
}
let (plan, memberSkips) = planClass(klass, context: context)
skips.append(contentsOf: memberSkips)
boundCallables += plan.constructors.count + plan.methods.count + plan.functions.count
types.append(.class(plan))
return 1
}
private func skipInterface(into skips: inout [SkipEntry], into types: inout [TypePlan], iface: Interface, namespace: String, context: MapContext) -> Int {
let fullName = "\(namespace).\(iface.name)"
if !iface.symbolInfo.isBindable {
skips.append(SkipEntry(symbol: fullName, cIdentifier: iface.cType,
reason: .notIntrospectable, detail: "non-introspectable interface"))
return 0
}
let (plan, memberSkips) = planInterface(iface, context: context)
skips.append(contentsOf: memberSkips)
types.append(.interface(plan))
return 1
}
private func skipRecord(into skips: inout [SkipEntry], into types: inout [TypePlan], record: Record, namespace: String, context: MapContext) -> Int {
let fullName = "\(namespace).\(record.name)"
if let forType = record.isGTypeStructFor {
skips.append(SkipEntry(symbol: fullName, cIdentifier: record.cType,
reason: .gtypeStruct, detail: "GObject class struct for '\(forType)'"))
return 0
}
if record.isBoxed {
if reservedSwiftTypes.contains(record.name) {
skips.append(SkipEntry(symbol: fullName, cIdentifier: record.cType,
reason: .unknownType,
detail: "record name '\(record.name)' shadows Swift stdlib type"))
return 0
}
types.append(.record(planRecord(record, context: context)))
return 1
}
skips.append(SkipEntry(symbol: fullName, cIdentifier: record.cType,
reason: .plainRecord, detail: "no GType registration"))
return 0
}
/// Plans a namespace-level callback as a `CallbackTypePlan`.
///
/// Tries to map every parameter and return value through the TypeMapper.
/// On success, produces a `.callback(CallbackTypePlan)` type plan.
/// On failure (unmappable param/return), emits a `SkipEntry` with the
/// existing `.callbackWithoutUserData` reason and a detail explaining why.
/// - Returns: 1 when the callback was bound, 0 when skipped.
private func planCallbackType(into skips: inout [SkipEntry], into types: inout [TypePlan],
callback: Callback, namespace: String, context: MapContext) -> Int {
let fullName = "\(namespace).\(callback.name)"
// Map the callback through the TypeMapper this recursively maps
// every parameter and return value. If any fails, we skip with the
// existing callbackWithoutUserData reason (baseline-stable).
let refType = GIRType.typeRef(callback.name, namespace: namespace)
do {
let mapping = try map(refType, nullable: false, transfer: .none, context: context)
let plan = CallbackTypePlan(
name: callback.name,
swiftType: mapping.swiftType,
cSwiftType: mapping.cSwiftType,
doc: callback.doc
)
types.append(.callback(plan))
return 1
} catch let error as MapError {
skips.append(SkipEntry(symbol: fullName, cIdentifier: callback.cType,
reason: error.reason == .callbackWithoutUserData ? .callbackWithoutUserData : error.reason,
detail: "callback '\(callback.name)' has unmappable param/return: \(error.detail)"))
return 0
} catch {
skips.append(SkipEntry(symbol: fullName, cIdentifier: callback.cType,
reason: .callbackWithoutUserData,
detail: "callback '\(callback.name)' unmappable"))
return 0
}
}
/// Plans a namespace-level function, appending a `.callable` type plan on
/// success or a `SkipEntry` on failure. Returns 1 when bound, 0 when skipped.
private func planOrSkipFunction(into skips: inout [SkipEntry], into types: inout [TypePlan], fn: GlobalFunction, namespace: String, context: MapContext) -> Int {
let fullName = "\(namespace).\(fn.name)"
// Check bindability: introspectable, not shadowed, not deprecated-removed
if let skipEntry = checkBindable(fn.symbolInfo, fullName: fullName, cIdentifier: fn.cIdentifier) {
skips.append(skipEntry)
return 0
}
// Symbols the system library does not export (macros, inline functions,
// or GType getters absent from the shared object) cannot be called.
if knownMissingCFunctions.contains(fn.cIdentifier) {
skips.append(SkipEntry(symbol: fullName, cIdentifier: fn.cIdentifier,
reason: .unknownType, detail: "C symbol '\(fn.cIdentifier)' is not exported by the system library"))
return 0
}
if knownMisleadingCFunctions.contains(fn.cIdentifier) {
skips.append(SkipEntry(symbol: fullName, cIdentifier: fn.cIdentifier,
reason: .unknownType, detail: "return type '\(fn.cIdentifier)' maps as value but is actually a C pointer"))
return 0
}
switch planFunction(fn, context: context) {
case .success(let plan):
types.append(.callable(plan))
return 1
case .skip(let entry):
skips.append(entry)
return 0
}
}
/// Known C symbols that aren't exported by the system library
/// (macros, inline functions, or GType getters for types not in the .so).
private let knownMissingCFunctions: Set<String> = [
"g_fsync",
"g_strv_get_type",
"g_variant_get_gtype",
"g_get_monotonic_time_ns", // returns uint64_t (UInt64), not guint64 (UInt)
// gstdio.h wrappers: on non-Windows these are `#define g_open open` macros,
// so no real symbol is exported and Swift cannot import them.
"g_access", "g_chdir", "g_chmod", "g_creat", "g_fopen", "g_freopen",
"g_fsync", "g_lstat", "g_mkdir", "g_open", "g_close", "g_remove", "g_rename",
"g_rmdir", "g_stat", "g_unlink", "g_utime",
// Removed from modern GLib (present in the GIR, absent from the shared
// object) these fail only at link time, not compile time.
"g_thread_init", "g_thread_init_with_errorcheck_mutexes",
]
let knownMisleadingCFunctions: Set<String> = [
"g_utf8_to_ucs4", // returns gunichar* (UInt32*)
"g_utf8_to_ucs4_fast", // returns gunichar* (UInt32*)
"g_utf8_to_utf16", // returns gunichar2* (UInt16*)
"g_get_charset", // const gchar** out-param const/mutable mismatch
"g_get_console_charset", // same as g_get_charset
]
/// Swift type names that boxed records must not shadow.
let reservedSwiftTypes: Set<String> = [
"String", "Int", "Bool", "Double", "Float", "Array", "Dictionary",
"Set", "Optional", "Data", "Date", "URL", "Error", "Void", "Any",
"Object", "Type", "Protocol",
]
/// Plans a boxed record as an opaque pointer wrapper, resolving the copy and
/// free functions that give it correct memory management (C4).
func planRecord(_ record: Record, context: MapContext) -> RecordPlan {
return RecordPlan(
name: record.name, cType: record.cType,
getTypeFunction: record.getTypeFunction,
copyFunction: resolvedCopyFunction(record),
freeFunction: resolvedFreeFunction(record),
doc: record.doc
)
}
/// Resolves a boxed record's destructor C identifier.
///
/// Prefers the explicit GIR `free-function` attribute; otherwise falls back to
/// the record's own parameterless `unref` (preferred, ref-counted) or `free`
/// method. `g_boxed_free` is deliberately *not* used as a fallback: it lives in
/// GObject, which the GLib module cannot link against (GObject depends on GLib,
/// not the reverse). Returns `nil` when no safe destructor exists the wrapper
/// then renders without a `deinit` (documented leak) rather than risk a wrong
/// free.
/// Resolves a boxed record's paired copy and free functions, ensuring they
/// come from a consistent category (refcount: `ref`/`unref`, or copy:
/// `copy`/`free`). Returns `(nil, nil)` when no consistent pair exists.
private func resolvedCopyFreePair(_ record: Record) -> (copy: String?, free: String?) {
if let explicitCopy = record.copyFunction, let explicitFree = record.freeFunction {
return (explicitCopy, explicitFree)
}
// Prefer refcount semantics (ref + unref together).
let ref = record.instanceReleaseMethod(named: ["ref"])
let unref = record.instanceReleaseMethod(named: ["unref"])
if ref != nil, unref != nil {
return (ref, unref)
}
// Fall back to copy/free semantics.
let copy = record.instanceReleaseMethod(named: ["copy"])
let free = record.instanceReleaseMethod(named: ["free"])
if copy != nil, free != nil {
return (copy, free)
}
// No consistent pair found via method scan. If an explicit GIR attribute
// was provided (e.g. only `copy-function`), trust it; when both sides come
// from auto-detected methods of conflicting categories, return nil for both
// to avoid mixing e.g. copy + unref.
if record.copyFunction != nil || record.freeFunction != nil {
return (record.copyFunction, record.freeFunction)
}
return (nil, nil)
}
func resolvedFreeFunction(_ record: Record) -> String? {
resolvedCopyFreePair(record).free
}
func resolvedCopyFunction(_ record: Record) -> String? {
resolvedCopyFreePair(record).copy
}
extension Record {
/// The C identifier of the first bindable instance method matching one of
/// `names` (in priority order) that takes no arguments beyond the instance
/// itself the shape every canonical `ref`/`unref`/`copy`/`free` shares.
func instanceReleaseMethod(named names: [String]) -> String? {
for wanted in names {
if let method = methods.first(where: {
$0.name == wanted && $0.symbolInfo.isBindable
&& $0.parameters.allSatisfy(\.isInstanceParameter)
}) {
return method.cIdentifier
}
}
return nil
}
}
/// Plans an interface as a Swift protocol, planning its instance methods as
/// protocol requirements.
///
/// A GObject interface method is dispatched through the implementing class's
/// instance pointer the concrete C symbol (e.g. `gtk_widget_get_buildable_id`)
/// lives on the class, not the interface. So each interface method becomes a
/// protocol *requirement* only; the implementing class supplies the body via
/// its own `planClass` methods. Methods that cannot be planned (unsupported
/// types, varargs, ) are returned as skip entries, mirroring `planClass`.
///
/// - Returns: The interface plan and the skip entries for its unplannable
/// methods.
func planInterface(_ iface: Interface, context: MapContext) -> (plan: InterfacePlan, memberSkips: [SkipEntry]) {
let registry = context.registry
// Resolve prerequisite types through registry
let prereqSwiftNames: [String] = iface.prereqs.compactMap { prereq in
let qualified = prereq.contains(".") ? prereq : "\(context.currentNamespace).\(prereq)"
guard let resolved = registry.resolve(girName: qualified) else { return nil }
return registry.swiftTypeName(for: resolved, in: context.currentModule)
}
// Instance methods become protocol requirements. Unplannable ones are
// recorded as skips, matching class member behaviour.
var memberSkips: [SkipEntry] = []
var methodPlans: [CallablePlan] = []
for method in iface.methods where method.symbolInfo.isBindable {
switch planMethod(method, context: context) {
case .success(let plan): methodPlans.append(plan)
case .skip(let entry): memberSkips.append(entry)
}
}
// Properties
// Interfaces publish properties as `{ get }` / `{ get set }` requirements,
// not bodies, so there is nothing to delegate to the conforming class
// supplies the implementation. Plan them with no delegation candidates.
var propertyPlans: [PropertyPlan] = []
for prop in iface.properties where prop.symbolInfo.isBindable {
switch planProperty(prop, fullName: "\(context.currentNamespace).\(iface.name).\(prop.name)",
methods: [], context: context) {
case .success(let plan): propertyPlans.append(plan)
case .skip(let entry): memberSkips.append(entry)
}
}
// Signals
var signalPlans: [SignalPlan] = []
for signal in iface.signals where signal.symbolInfo.isBindable {
let result = planSignal(signal, onClass: iface.name, namespace: context.currentNamespace, context: context)
if let plan = result.plan {
signalPlans.append(plan)
} else if let skip = result.skip {
memberSkips.append(skip)
}
}
// Compute the module-qualified name for matching ClassPlan.interfaces.
let qualifiedName: String = {
let girName = "\(context.currentNamespace).\(iface.name)"
guard let resolved = registry.resolve(girName: girName) else {
return iface.name
}
return registry.swiftTypeName(for: resolved, in: context.currentModule)
}()
let plan = InterfacePlan(
name: iface.name, cType: iface.cType,
prereqs: prereqSwiftNames,
getTypeFunction: iface.getTypeFunction,
methods: methodPlans,
properties: propertyPlans,
signals: signalPlans,
qualifiedName: qualifiedName,
doc: iface.doc
)
return (plan, memberSkips)
}
// MARK: - Property planning
/// Outcome of planning a single property.
private enum PropertyResult {
case success(PropertyPlan)
case skip(SkipEntry)
}
/// The user-facing parameters of a callable, i.e. everything except the
/// implicit instance parameter and any out-parameters.
private func userParameters(_ plan: CallablePlan) -> [ParameterPlan] {
plan.parameters.filter { !$0.isInstanceParameter && !$0.isOutParameter }
}
/// Finds an instance method usable as a property getter: named `name`, taking
/// no user arguments, returning a value, and not throwing.
private func delegableGetter(named name: String, in methods: [CallablePlan]) -> CallablePlan? {
methods.first { m in
m.name == name && !m.isStatic && !m.isConstructor && !m.throwsError
&& m.returnMapping != nil
&& m.parameters.contains(where: \.isInstanceParameter)
&& userParameters(m).isEmpty
}
}
/// Finds an instance method usable as a property setter: named `name`, taking
/// exactly one user argument, and not throwing. Returns the method paired with
/// that sole argument.
private func delegableSetter(named name: String, in methods: [CallablePlan]) -> (method: CallablePlan, argument: ParameterPlan)? {
for m in methods where m.name == name && !m.isStatic && !m.isConstructor && !m.throwsError
&& m.parameters.contains(where: \.isInstanceParameter) {
let args = userParameters(m)
if args.count == 1 { return (m, args[0]) }
}
return nil
}
/// Plans a GObject property as a Swift computed property.
///
/// Each accessor is resolved independently, preferring delegation to a
/// `getter=` / `setter=` method (from `methods`) over the uniform GValue path,
/// because the method already encodes the correct nullability and ownership.
/// Delegation is only chosen when it yields a single consistent Swift type for
/// the property (getter return type == setter argument type); otherwise the
/// property falls back to GValue accessors, and is skipped if its type has no
/// GValue support either.
private func planProperty(_ property: Property, fullName: String,
methods: [CallablePlan], context: MapContext) -> PropertyResult {
func skip(_ reason: SkipReason, _ detail: String) -> PropertyResult {
.skip(SkipEntry(symbol: fullName, cIdentifier: property.name, reason: reason, detail: detail))
}
do {
let mapping = try map(property.type, nullable: property.isNullable,
transfer: property.transferOwnership, context: context)
let writable = property.isWritable && !property.isConstructOnly
let getM = property.getter.flatMap { delegableGetter(named: swiftFunctionName($0), in: methods) }
let setM = property.setter.flatMap { delegableSetter(named: swiftFunctionName($0), in: methods) }
/// The GValue fallback accessor for this property's mapped type, or nil
/// if the type cannot be represented as a GValue.
func gvalueAccessor() -> PropertyAccessorPlan? {
mapping.gvalue.map { .gvalue(typeMacro: $0.typeMacro, valueSuffix: $0.getterSuffix) }
}
let swiftType: String
let getter: PropertyAccessorPlan
let setter: PropertyAccessorPlan?
if writable {
// A writable computed property needs get and set to share one type.
// Delegate both only when their types agree; otherwise use GValue
// for both so the type is unambiguous.
if let g = getM, let s = setM,
let gType = g.returnMapping?.swiftType, gType == s.argument.mapping.swiftType {
swiftType = gType
getter = .delegate(method: g.name, argumentLabel: nil)
setter = .delegate(method: s.method.name, argumentLabel: s.argument.swiftName)
} else {
guard let gv = gvalueAccessor() else {
return skip(.unsupportedGValueCategory,
"writable property type '\(mapping.swiftType)' has no GValue support and no matching getter=/setter= methods")
}
swiftType = mapping.swiftType
getter = gv
setter = gv
}
} else {
// Read-only: delegate the getter when available, else GValue.
if let g = getM, let gType = g.returnMapping?.swiftType {
swiftType = gType
getter = .delegate(method: g.name, argumentLabel: nil)
setter = nil
} else {
guard let gv = gvalueAccessor() else {
return skip(.unsupportedGValueCategory,
"property type '\(mapping.swiftType)' has no GValue support and no matching getter= method")
}
swiftType = mapping.swiftType
getter = gv
setter = nil
}
}
return .success(PropertyPlan(
swiftName: swiftFunctionName(property.name),
girName: property.name,
swiftType: swiftType,
getter: getter, setter: setter,
doc: property.doc))
} catch let err as MapError {
return skip(err.reason, err.detail)
} catch {
return skip(.unknownType, "unexpected error: \(error)")
}
}
/// Plans a class as a GObject wrapper with cross-module inheritance, planning
/// its constructors, instance methods, and static functions. Members that
/// cannot be planned are returned as skip entries (the class itself is still
/// generated Rule 3 skips the *member*, not the whole type).
///
/// - Returns: The class plan and the skip entries for its unplannable members.
func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberSkips: [SkipEntry]) {
let girName = "\(context.currentNamespace).\(klass.name)"
let registry = context.registry
// Resolve parent
let parentSwiftName: String?
if let parentGIR = klass.parent {
let qualifiedParent = parentGIR.contains(".") ? parentGIR : "\(context.currentNamespace).\(parentGIR)"
if let resolved = registry.resolve(girName: qualifiedParent) {
parentSwiftName = registry.swiftTypeName(for: resolved, in: context.currentModule)
} else {
parentSwiftName = nil // foreign or unknown treat as root
}
} else {
parentSwiftName = nil
}
let isOpen = registry.subclassedTypes().contains(girName)
let descendsIU = registry.descendsFromInitiallyUnowned(girName)
// Resolve implemented interfaces to their Swift names
let interfaceNames: [String] = klass.implements.compactMap { ifaceName in
let qualified = ifaceName.contains(".") ? ifaceName : "\(context.currentNamespace).\(ifaceName)"
guard let resolved = registry.resolve(girName: qualified) else { return nil }
return registry.swiftTypeName(for: resolved, in: context.currentModule)
}
// Members
var memberSkips: [SkipEntry] = []
/// Records the outcome of planning one member.
func collect(_ result: CallablePlanResult, into plans: inout [CallablePlan]) {
switch result {
case .success(let plan): plans.append(plan)
case .skip(let entry): memberSkips.append(entry)
}
}
var constructorPlans: [CallablePlan] = []
// Abstract classes cannot be instantiated directly their constructors
// belong to concrete subclasses, so skip them here.
if !klass.isAbstract {
for ctor in klass.constructors where ctor.symbolInfo.isBindable {
collect(planConstructor(ctor, className: klass.name, descendsIU: descendsIU, context: context),
into: &constructorPlans)
}
}
var methodPlans: [CallablePlan] = []
for method in klass.methods where method.symbolInfo.isBindable {
collect(planMethod(method, context: context), into: &methodPlans)
}
var functionPlans: [CallablePlan] = []
for fn in klass.functions where fn.symbolInfo.isBindable {
collect(planFunction(fn, context: context), into: &functionPlans)
}
// Properties
// Pass the planned methods so accessors can delegate to a `getter=`/
// `setter=` method when GIR names one and it was itself successfully planned.
var propertyPlans: [PropertyPlan] = []
for prop in klass.properties where prop.symbolInfo.isBindable {
switch planProperty(prop, fullName: "\(girName).\(prop.name)", methods: methodPlans, context: context) {
case .success(let plan): propertyPlans.append(plan)
case .skip(let entry): memberSkips.append(entry)
}
}
// Signals
var signalPlans: [SignalPlan] = []
for signal in klass.signals where signal.symbolInfo.isBindable {
let result = planSignal(signal, onClass: klass.name, namespace: context.currentNamespace, context: context)
if let plan = result.plan {
signalPlans.append(plan)
} else if let skip = result.skip {
memberSkips.append(skip)
}
}
let plan = ClassPlan(
name: klass.name, cType: klass.cType,
parent: parentSwiftName,
isOpen: isOpen,
isAbstract: klass.isAbstract,
getTypeFunction: klass.getTypeFunction,
descendsFromInitiallyUnowned: descendsIU,
interfaces: interfaceNames,
constructors: constructorPlans,
methods: methodPlans,
functions: functionPlans,
properties: propertyPlans,
signals: signalPlans,
doc: klass.doc
)
return (plan, memberSkips)
}
/// Plans an enumeration, deduplicating raw values.
///
/// Members with duplicate raw values become `public static var` aliases
/// pointing to the first case with that value.
func planEnumeration(_ enumeration: Enumeration, context: MapContext) -> EnumPlan {
var cases: [EnumCase] = []
var aliases: [EnumAlias] = []
var seenRawValues: [String: String] = [:] // rawValue first case name
for member in enumeration.members {
let swiftName = swiftEnumCaseName(member.name)
if let firstCase = seenRawValues[member.value] {
aliases.append(EnumAlias(name: swiftName, targetCaseName: firstCase))
} else {
cases.append(EnumCase(name: swiftName, rawValue: member.value, cIdentifier: member.cIdentifier))
seenRawValues[member.value] = swiftName
}
}
return EnumPlan(
name: enumeration.name, cType: enumeration.cType,
cases: cases, aliases: aliases,
hasGType: enumeration.getTypeFunction != nil,
doc: enumeration.doc
)
}
/// Plans a bitfield as an `OptionSet` with verbatim raw values.
func planBitfield(_ bitfield: Bitfield, context: MapContext) -> BitfieldPlan {
let members = bitfield.members.map { member in
EnumCase(
name: swiftEnumCaseName(member.name),
rawValue: member.value,
cIdentifier: member.cIdentifier
)
}
return BitfieldPlan(
name: bitfield.name, cType: bitfield.cType,
members: members,
hasGType: bitfield.getTypeFunction != nil,
doc: bitfield.doc
)
}
/// Plans a global constant. Returns `nil` when the constant's type cannot
/// be mapped, so the caller records a skip entry.
///
/// The GIR name (`PARAM_MASK`) is converted to Swift's lowerCamelCase
/// convention for constants (`paramMask`, like `Double.pi`); the original
/// spelling is preserved in `girName` for the rendered doc comment.
func planConstant(_ constant: Constant, context: MapContext) -> ConstantPlan? {
let mappingResult = Result { try map(constant.type, nullable: false, transfer: .none, context: context) }
guard case .success(let mapping) = mappingResult else { return nil }
return ConstantPlan(name: swiftConstantName(constant.name), girName: constant.name,
value: constant.value, swiftType: mapping.swiftType, doc: constant.doc)
}
/// Outcome of alias planning: success with a plan, or skip with a reason.
enum AliasPlanResult {
case success(AliasPlan)
case skip(SkipEntry)
}
/// Plans a type alias, resolving the target type through the mapper.
func planAlias(_ alias: Alias, context: MapContext) -> AliasPlanResult {
let fullName = "\(context.currentNamespace).\(alias.name)"
let mappingResult = Result { try map(alias.target, nullable: false, transfer: .none, context: context) }
switch mappingResult {
case .success(let mapping):
return .success(AliasPlan(name: alias.name, swiftType: mapping.swiftType, doc: alias.doc))
case .failure(let error as MapError):
return .skip(SkipEntry(symbol: fullName, cIdentifier: alias.cType,
reason: error.reason, detail: error.detail))
case .failure:
return .skip(SkipEntry(symbol: fullName, cIdentifier: alias.cType,
reason: .unknownType, detail: "alias target type could not be mapped"))
}
}
// MARK: - Callable planning
/// Outcome of trying to plan a callable: either a complete plan or a skip entry.
enum CallablePlanResult {
case success(CallablePlan)
case skip(SkipEntry)
}
/// Plans a function or method: checks every parameter and the return type for
/// mappability, skipping the whole callable if any part fails. The instance
/// parameter (for methods) is handled by `planParameters` as a `self`
/// passthrough.
///
/// - Parameters:
/// - fullName: Fully qualified GIR symbol name for skip reporting.
/// - swiftName: The Swift function/method name (keyword-escaped).
/// - cIdentifier: The C function symbol to call.
/// - parameters: The callable's parameters (may include an instance parameter).
/// - returnValue: The callable's return value.
/// - throwsGError: Whether the callable takes a trailing `GError**`.
/// - doc: Documentation from the GIR.
/// - isStatic: `true` for free/static functions, `false` for instance methods.
/// - context: The resolution context.
/// - Returns: A complete plan, or a skip entry with a machine-readable reason.
private func planCallable(
fullName: String, swiftName: String, cIdentifier: String,
parameters: [Parameter], returnValue: ReturnValue, throwsGError: Bool,
doc: String?, isStatic: Bool, context: MapContext
) -> CallablePlanResult {
// Check parameters (GError** is implicit in throws="1", not in the
// parameter list the renderer synthesizes the &error arg via
// cArguments(error: true). Do NOT strip the last parameter, it's real.)
let paramPlanResult = planParameters(parameters, context: context)
guard case .success(let paramPlans) = paramPlanResult else {
if case .skip(let entry) = paramPlanResult {
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
reason: entry.reason, detail: entry.detail))
}
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
reason: .unknownType, detail: "parameter planning failed"))
}
// Map return type
let returnMapping: Mapping?
if returnValue.type != .void {
switch Result(catching: { try map(returnValue.type, nullable: returnValue.isNullable,
transfer: returnValue.transferOwnership, context: context) }) {
case .success(let returnMap):
if !returnMap.isReadyForCallables {
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
reason: .unknownType,
detail: "return type '\(returnMap.swiftType)' is not yet generated (category: \(returnMap.category))"))
}
// Interfaces produce unsupported marshalOut (protocols can't be
// constructed). Params still work via .objectPointer.
if case .unsupported(let reason) = returnMap.marshalOut {
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
reason: .unknownType, detail: "return type: \(reason)"))
}
// Callback return types cannot be constructed from C function
// pointers yet the renderer has no marshal-out support.
if returnMap.category == .callback {
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
reason: .callbackWithoutUserData,
detail: "callback return type '\(returnMap.swiftType)' deferred"))
}
returnMapping = returnMap
case .failure(let error as MapError):
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
reason: error.reason, detail: error.detail))
case .failure:
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
reason: .unknownType, detail: "return type mapping failed"))
}
} else {
returnMapping = nil
}
return .success(CallablePlan(
name: swiftName, cIdentifier: cIdentifier,
parameters: paramPlans, returnMapping: returnMapping,
isStatic: isStatic, isConstructor: false, ownershipInit: nil, throwsError: throwsGError, doc: doc
))
}
/// Plans a namespace-level (or static) function.
func planFunction(_ fn: GlobalFunction, context: MapContext) -> CallablePlanResult {
planCallable(
fullName: "\(context.currentNamespace).\(fn.name)",
swiftName: swiftFunctionName(fn.name), cIdentifier: fn.cIdentifier,
parameters: fn.parameters, returnValue: fn.returnValue,
throwsGError: fn.throwsGError, doc: fn.doc, isStatic: true, context: context)
}
/// Plans an instance method. The instance parameter becomes `self.pointer`.
func planMethod(_ method: Method, context: MapContext) -> CallablePlanResult {
planCallable(
fullName: "\(context.currentNamespace).\(method.name)",
swiftName: swiftFunctionName(method.name), cIdentifier: method.cIdentifier,
parameters: method.parameters, returnValue: method.returnValue,
throwsGError: method.throwsGError, doc: method.doc, isStatic: false, context: context)
}
/// Plans a GObject signal for a class or interface.
///
/// Maps every signal parameter and the return value through the TypeMapper.
/// The implicit instance parameter (C-arg 0) is synthesized using the owning
/// class's type. Returns a ``SignalPlan`` on success or a skip entry on
/// failure (e.g. unmappable parameter type).
///
/// - Parameters:
/// - signal: The signal to plan.
/// - className: The owning class/interface Swift name.
/// - namespace: The GIR namespace name.
/// - context: The resolution context.
/// - Returns: A tuple of optional plan and optional skip entry (exactly one is non-nil).
private func planSignal(_ signal: Signal, onClass className: String,
namespace: String, context: MapContext) -> (plan: SignalPlan?, skip: SkipEntry?) {
let girName = "\(context.currentNamespace).\(className).\(signal.name)"
var signalParams: [ParameterPlan] = []
// Instance parameter: C-arg 0, the emitting GObject pointer.
// Its Swift type is the owning class; its C type is UnsafeMutableRawPointer.
let instanceParam = ParameterPlan(
swiftName: "instance", cArgIndex: 0,
mapping: Mapping(swiftType: className, cSwiftType: "UnsafeMutableRawPointer",
marshalIn: .direct, marshalOut: .direct),
isInstanceParameter: true
)
signalParams.append(instanceParam)
// Map each real signal parameter
for (idx, param) in signal.parameters.enumerated() {
do {
let mapping = try map(param.type, nullable: param.isNullable,
transfer: param.transferOwnership, context: context)
guard mapping.isReadyForCallables else {
return (nil, SkipEntry(symbol: girName, cIdentifier: nil,
reason: .signalUnmappableParam,
detail: "param '\(param.name)' type '\(mapping.swiftType)' not ready for callables"))
}
let pName = swiftParameterName(param.name)
signalParams.append(ParameterPlan(
swiftName: pName, cArgIndex: idx + 1, // +1 because instance param is index 0
mapping: mapping
))
} catch let error as MapError {
return (nil, SkipEntry(symbol: girName, cIdentifier: nil,
reason: .signalUnmappableParam,
detail: "param '\(param.name)': \(error.detail)"))
} catch {
return (nil, SkipEntry(symbol: girName, cIdentifier: nil,
reason: .signalUnmappableParam,
detail: "param '\(param.name)': unexpected error"))
}
}
// Map return value
let returnMapping: Mapping?
if signal.returnValue.type != .void {
do {
let mapped = try map(signal.returnValue.type, nullable: signal.returnValue.isNullable,
transfer: signal.returnValue.transferOwnership, context: context)
guard mapped.isReadyForCallables else {
return (nil, SkipEntry(symbol: girName, cIdentifier: nil,
reason: .signalUnmappableParam,
detail: "return type '\(mapped.swiftType)' not ready for callables"))
}
returnMapping = mapped
} catch let error as MapError {
return (nil, SkipEntry(symbol: girName, cIdentifier: nil,
reason: .signalUnmappableParam,
detail: "return: \(error.detail)"))
} catch {
return (nil, SkipEntry(symbol: girName, cIdentifier: nil,
reason: .signalUnmappableParam,
detail: "return: unexpected error"))
}
} else {
returnMapping = nil
}
let swiftName = swiftFunctionName(signal.name)
let trampolineCName = "_trampoline_\(namespace)_\(className)_\(signal.name)"
let plan = SignalPlan(
owningClassName: className, girName: signal.name,
swiftName: swiftName,
isDetailed: signal.isDetailed,
parameters: signalParams,
returnMapping: returnMapping,
trampolineCName: trampolineCName,
doc: signal.doc
)
return (plan, nil)
}
/// Plans a constructor as a Swift `convenience init`. The C constructor's
/// returned instance pointer is adopted through the class's designated
/// `init(takingOwnership:)`, which sinks a floating reference when the class
/// descends from `InitiallyUnowned`.
///
/// - Parameters:
/// - ctor: The GIR constructor.
/// - className: The owning class's name (for skip reporting).
/// - descendsIU: Whether the class descends from `InitiallyUnowned`.
/// - context: The resolution context.
/// - Returns: A constructor plan, or a skip entry.
func planConstructor(_ ctor: Constructor, className: String, descendsIU: Bool, context: MapContext) -> CallablePlanResult {
let fullName = "\(context.currentNamespace).\(className).\(ctor.name)"
// GError** is implicit in throws="1", not in the parameter list.
let paramPlanResult = planParameters(ctor.parameters, context: context)
guard case .success(let paramPlans) = paramPlanResult else {
if case .skip(let entry) = paramPlanResult {
return .skip(SkipEntry(symbol: fullName, cIdentifier: ctor.cIdentifier,
reason: entry.reason, detail: entry.detail))
}
return .skip(SkipEntry(symbol: fullName, cIdentifier: ctor.cIdentifier,
reason: .unknownType, detail: "constructor parameter planning failed"))
}
// Constructors with out-params cannot be expressed as Swift inits,
// which cannot also return out-value tuples.
if paramPlans.contains(where: \.isOutParameter) {
return .skip(SkipEntry(symbol: fullName, cIdentifier: ctor.cIdentifier,
reason: .constructorOutParams,
detail: "out-param constructors are not expressible as Swift init"))
}
return .success(CallablePlan(
name: swiftFunctionName(ctor.name), cIdentifier: ctor.cIdentifier,
parameters: paramPlans, returnMapping: nil,
isStatic: false, isConstructor: true,
ownershipInit: descendsIU ? .sinkingRef : .takingOwnership,
throwsError: ctor.throwsGError, doc: ctor.doc))
}
/// Plans the parameters of a callable. Returns `.skip` if any parameter
/// has an unsupported direction, or if any type cannot be mapped.
func planParameters(
_ parameters: [Parameter], context: MapContext
) -> ParameterPlanResult {
var plans: [ParameterPlan] = []
// Pre-scan: collect indices that are user-data targets for callbacks.
// These are plain gpointer params that would otherwise fail the pointer
// check the callback-box mechanism handles them.
let closureTargets = Set(parameters.compactMap(\.closureIndex))
for (index, param) in parameters.enumerated() {
// The instance parameter is always `self` passed as `self.pointer`,
// never type-checked (its type is the enclosing class, which is a
// `.needsClass` mapping that would otherwise fail the ready check).
if param.isInstanceParameter {
plans.append(ParameterPlan(
swiftName: "self", cArgIndex: index,
mapping: Mapping(swiftType: "Self", cSwiftType: "UnsafeMutableRawPointer",
marshalIn: .direct, marshalOut: .direct),
isInstanceParameter: true))
continue
}
// Variadic parameter (GIR names it "..." with a <varargs/> child):
// C variadic calls cannot be bridged from Swift.
if param.name == "..." {
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .varargs, detail: "variadic ('...') parameter"))
}
// Direction check
if param.direction == .out {
// Caller-allocates out-params (`char *outbuf`, `gunichar *result`)
// hand the C function a pointer to storage the CALLER must size and
// own unlike callee-allocates out-params (`T **out`) where the
// callee fills in the pointee. Our out-param marshalling declares a
// single Swift scalar and passes its address, which is correct only
// for the callee-allocates shape; for caller-allocates it lets C
// write buffer payload past a one-element slot (memory corruption)
// or reinterpret payload as a pointer (segfault, e.g.
// g_unichar_to_utf8). GIR carries no buffer-size field, so there is
// nothing to allocate against skip rather than guess.
if param.callerAllocates {
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .outParameter,
detail: "caller-allocates out-param '\(param.name)' (no buffer size in GIR)"))
}
// Out-params are collected and returned as Swift values rather than
// passed as arguments. Map the VALUE type (not the pointer-to-pointer).
let mappingResult = Result { try map(param.type, nullable: param.isNullable,
transfer: param.transferOwnership, context: context) }
switch mappingResult {
case .success(let paramMapping):
if !paramMapping.isReadyForCallables {
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .unknownType,
detail: "out-param '\(param.name)' type '\(paramMapping.swiftType)' not yet generated"))
}
// Skip object/boxed out-params wrapper construction from a
// raw pointer requires the type's init(takingOwnership:) which
// is class-specific and deferred.
if paramMapping.category == .needsClass || paramMapping.category == .needsRecord {
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .outParameter,
detail: "object/boxed out-param '\(param.name)' deferred"))
}
let swiftName = swiftParameterName(param.name)
plans.append(ParameterPlan(
swiftName: swiftName, cArgIndex: index,
mapping: paramMapping, isOutParameter: true))
continue
case .failure(let error as MapError):
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: error.reason,
detail: "out-param '\(param.name)': \(error.detail)"))
case .failure:
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .unknownType,
detail: "out-param '\(param.name)' type not mappable"))
}
}
if param.direction == .inout_ {
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .inoutParameter, detail: "'\(param.name)' has direction=inout"))
}
// Map the type
let mappingResult = Result { try map(param.type, nullable: param.isNullable,
transfer: param.transferOwnership, context: context) }
switch mappingResult {
case .success(var paramMapping):
// D4.3 callback-param binding remains deferred: passing a Swift
// closure captured as `@convention(c)` through generic
// `_ClosureBox<T>` storage triggers a Swift compiler ICE
// ("failed to produce diagnostic for expression") on functions
// like `g_qsort_with_data`/`g_dataset_foreach`. See HANDOFF.md.
if case .callbackBox = paramMapping.marshalIn {
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .callbackWithoutUserData,
detail: "callback param '\(param.name)' deferred to Phase D4.3"))
}
let isString = paramMapping.marshalIn == .stringToC
// A nullable string needs an optional-aware bridge; deferred.
if isString && paramMapping.swiftType.hasSuffix("?") {
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .unknownType, detail: "parameter '\(param.name)' is a nullable string"))
}
// Only a single-level `const gchar*` input string can be bridged
// from an immutable Swift `String` via `withCString`. A mutable
// `gchar*` is a caller-allocated output buffer, and a `gchar**`
// (two pointer levels, e.g. `const char* const*`) is a string
// vector both are deferred to array/out-parameter handling.
if isString {
let pointerLevels = param.cType.filter { $0 == "*" }.count
if !param.cType.contains("const") || pointerLevels != 1 {
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .unknownType, detail: "parameter '\(param.name)' is not a single const input string ('\(param.cType)')"))
}
}
// Non-string pointer parameters that aren't mapped as objects or
if param.cType.hasSuffix("*") && !isString,
paramMapping.category != .needsClass, paramMapping.category != .needsRecord,
!closureTargets.contains(index) {
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .unknownType, detail: "parameter '\(param.name)' C type '\(param.cType)' is a pointer"))
}
let swiftName = swiftParameterName(param.name)
// Only record a separate closureIndex when it differs from the
// callback param's own C arg index (callback doubling as user-data
// is the common case and needs no replacement).
let planClosure: Int? = (param.closureIndex != index) ? param.closureIndex : nil
plans.append(ParameterPlan(
swiftName: swiftName, cArgIndex: index,
mapping: paramMapping, isInstanceParameter: param.isInstanceParameter,
closureIndex: planClosure, destroyIndex: param.destroyIndex
))
case .failure(let error as MapError):
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: error.reason, detail: "parameter '\(param.name)': \(error.detail)"))
case .failure:
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .unknownType, detail: "parameter '\(param.name)' type not mappable"))
}
}
return .success(plans)
}
enum ParameterPlanResult {
case success([ParameterPlan])
case skip(SkipEntry)
}
// MARK: - Naming helpers
/// Converts a snake_case identifier to lowerCamelCase without dropping any
/// segment. The GIR `name` attribute is already namespace-stripped (e.g.
/// `"set_application_name"`, not `"g_set_application_name"`), so no prefix
/// removal is wanted doing so was the bug that turned setters like
/// `g_set_application_name` into `applicationName`, silently dropping the verb.
///
/// Fully-uppercase segments are GIR *words*, not acronyms to preserve:
/// `cclosure_marshal_BOOLEAN__BOXED_BOXED` means "boolean, boxed, boxed" and
/// `PARAM_MASK` means "param mask". Passing them through verbatim produced
/// unreadable runs (`cclosureMarshalBOOLEANBOXEDBOXED`), so each segment is
/// case-normalized to a single capitalized word (`Boolean`, `Boxed`).
///
/// A leading digit (which cannot begin a Swift identifier even when
/// backtick-escaped) is guarded with an underscore.
///
/// - Parameter snake: A snake_case identifier from the GIR.
/// - Returns: The lowerCamelCase spelling.
func camelCased(_ snake: String) -> String {
let normalized = snake.replacingOccurrences(of: "-", with: "_")
let parts = normalized.split(separator: "_", omittingEmptySubsequences: true)
.map { normalizeUppercaseSegment(String($0)) }
guard let first = parts.first else { return snake }
var result = first.lowercased()
for word in parts.dropFirst() {
result += word.prefix(1).uppercased() + word.dropFirst()
}
if result.first?.isNumber == true { result = "_\(result)" }
return result
}
/// Case-normalizes one underscore-delimited GIR segment: a segment that is
/// entirely uppercase (letters/digits, at least two characters, e.g. `BOOLEAN`,
/// `UINT`, `CSET`) is folded to a single capitalized word (`Boolean`, `Uint`,
/// `Cset`); everything else passes through unchanged.
private func normalizeUppercaseSegment(_ segment: String) -> String {
guard segment.count >= 2,
segment.contains(where: \.isUppercase),
segment.allSatisfy({ $0.isUppercase || $0.isNumber })
else { return segment }
return segment.prefix(1) + segment.dropFirst().lowercased()
}
/// Converts a GIR function/method name to a Swift name (lowerCamelCase),
/// escaping keywords. Example: `"set_application_name"` `"setApplicationName"`,
/// `"show"` `"show"`.
func swiftFunctionName(_ girName: String) -> String {
let camel = camelCased(girName)
guard !camel.isEmpty else { return girName }
return swiftKeywords.contains(camel) ? "`\(camel)`" : camel
}
/// Converts a GIR constant name (`SCREAMING_SNAKE`) to a Swift constant name
/// (lowerCamelCase, per the API Design Guidelines like `Double.pi`),
/// escaping keywords. Example: `"PARAM_MASK"` `"paramMask"`.
func swiftConstantName(_ girName: String) -> String {
let camel = camelCased(girName)
guard !camel.isEmpty else { return girName }
return swiftKeywords.contains(camel) ? "`\(camel)`" : camel
}
/// Converts a GIR parameter name to a Swift parameter name (lowerCamelCase),
/// escaping keywords. Example: `"application_name"` `"applicationName"`.
func swiftParameterName(_ girName: String) -> String {
let camel = camelCased(girName)
guard !camel.isEmpty else { return girName }
if swiftKeywords.contains(camel) { return "`\(camel)`" }
return camel
}
/// Converts a GIR member name to a Swift enum case name, escaping keywords.
///
/// Swift keywords and names starting with a digit are escaped with backticks.
/// The `_` prefix is stripped from GObject naming conventions (e.g. `_normal`
/// `normal`).
func swiftEnumCaseName(_ girName: String) -> String {
var name = girName
// Strip leading underscore (GObject naming convention)
if name.hasPrefix("_") {
let stripped = String(name.dropFirst())
// If stripping the underscore leaves a name starting with a digit,
// keep the underscore Swift requires identifiers to start with a
// letter or underscore, and backtick escaping doesn't lift this
// restriction.
if stripped.first?.isNumber != true {
name = stripped
}
}
// Names starting with a digit cannot be identifiers even backtick-escaped;
// prefix with underscore.
if name.first?.isNumber == true {
name = "_\(name)"
}
// Escaped keyword
if swiftKeywords.contains(name) { return "`\(name)`" }
return name
}
/// Swift reserved words that need backtick escaping when used as identifiers.
private let swiftKeywords: Set<String> = [
"as", "associativity", "break", "case", "catch", "class", "continue",
"default", "defer", "deinit", "do", "else", "enum", "extension",
"fallthrough", "false", "fileprivate", "for", "func", "get", "guard",
"if", "import", "in", "init", "inout", "internal", "is", "let", "nil",
"open", "operator", "optional", "override", "postfix", "prefix",
"private", "protocol", "public", "repeat", "required", "return",
"self", "set", "static", "struct", "subscript", "super", "switch",
"throw", "throws", "true", "try", "typealias", "var", "weak", "where",
"while", "willSet", "didSet", "PrecedenceGroup", "indirect", "left",
"none", "nonmutating", "precedencegroup", "right",
"Any", "Self", "Type", "Protocol",
]