1
0
Fork 0
gobject-generator/Sources/SwiftGtkGenCore/Planner.swift

1741 lines
84 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,
dependencyModules: (analysis.directDependencies[moduleName] ?? []).sorted()
)
let plan = planNamespace(namespace, context: context)
modulePlans[moduleName] = plan
}
// Global post-pass: drop class properties/methods already declared,
// with the identical Swift name, by an ancestor class possibly in a
// different module (e.g. `Pango.Coverage.ref()`/`unref()` already
// declared by `GObject.Object`). GObject inheritance provides these for
// free, and Swift errors on the redundant redeclaration. Keyed by
// unqualified Swift name: safe because `duplicateOfDependency` skipping
// guarantees surviving cross-module type names are unique along
// dependency edges, and inheritance only follows those edges.
var classesByGIRName: [String: ClassPlan] = [:]
for (_, plan) in modulePlans {
for case .class(let p) in plan.types { classesByGIRName[p.girName] = p }
}
// Swift's override-conflict diagnostic ("requires an 'override' keyword"
// / "overriding non-open instance method outside of its defining
// module") matches candidates by SELECTOR name + parameter labels/
// types regardless of return type. So the ancestor-collision key must
// do the same: excluding return type catches true selector collisions
// (e.g. `Pango.Coverage.ref()` vs. inherited `GObject.Object.ref()`
// same selector, would silently violate the override rule if kept) while
// still keeping legitimate different-selector overloads across an
// inheritance edge (e.g. `MenuButton.setDirection(direction: ArrowType)`
// vs. inherited `Widget.setDirection(dir: TextDirection)` different
// labels AND types, a different selector, no relation to include).
func methodSignature(_ m: CallablePlan) -> String {
let params = m.parameters
.filter { !$0.isInstanceParameter && !$0.isOutParameter && $0.synthesizedLengthOf == nil }
.map { "\($0.swiftName):\($0.mapping.swiftType)" }
.joined(separator: ",")
return "\(m.name)|throws:\(m.throwsError)|(\(params))"
}
// Walked by GIR name, not Swift spelling: two classes in different
// modules can share a Swift simple name (`Gst.Object` / `GObject.Object`
// both spell `Object`), which would make a bare-name-keyed walk and its
// cycle guard falsely conflate them terminating the ancestor walk one
// level early and missing real inherited members. GIR names are always
// unique.
func ancestorNames(of plan: ClassPlan) -> (props: Set<String>, methods: Set<String>) {
var propNames: Set<String> = []
var methodSigs: Set<String> = []
var current = plan.parentGIRName
var seen: Set<String> = [plan.girName]
while let parentGIRName = current, !seen.contains(parentGIRName), let parentPlan = classesByGIRName[parentGIRName] {
seen.insert(parentGIRName)
propNames.formUnion(parentPlan.properties.map(\.swiftName))
methodSigs.formUnion(parentPlan.methods.map(methodSignature))
current = parentPlan.parentGIRName
}
return (propNames, methodSigs)
}
for (moduleName, modulePlan) in modulePlans {
var skips = modulePlan.skips
var changed = false
let newTypes = modulePlan.types.map { typePlan -> TypePlan in
guard case .class(let plan) = typePlan else { return typePlan }
let inherited = ancestorNames(of: plan)
guard !inherited.props.isEmpty || !inherited.methods.isEmpty else { return typePlan }
let filteredMethods = plan.methods.filter { method in
guard inherited.methods.contains(methodSignature(method)) else { return true }
skips.append(SkipEntry(symbol: "\(moduleName).\(plan.name).\(method.name)",
cIdentifier: method.cIdentifier, reason: .inheritedMember,
detail: "method '\(method.name)' already declared by ancestor"))
return false
}
// Own-method selectors that survived filtering used below to
// catch properties left delegating to a method this pass just
// removed (the property was planned before this global pass ran,
// using the class's full pre-filter method list).
let survivingSelectors = Set(filteredMethods.map {
"\($0.name)/\($0.parameters.filter { !$0.isInstanceParameter && !$0.isOutParameter }.count)"
})
func delegatesToDroppedMethod(_ accessor: PropertyAccessorPlan?) -> Bool {
guard case .delegate(let method, let label) = accessor else { return false }
let arity = label == nil ? 0 : 1
return !survivingSelectors.contains("\(method)/\(arity)")
}
let filteredProps = plan.properties.filter { prop in
if inherited.props.contains(prop.swiftName) {
skips.append(SkipEntry(symbol: "\(moduleName).\(plan.name).\(prop.swiftName)",
cIdentifier: prop.girName, reason: .inheritedMember,
detail: "property '\(prop.swiftName)' already declared by ancestor"))
return false
}
if delegatesToDroppedMethod(prop.getter) || delegatesToDroppedMethod(prop.setter) {
// The delegated getter/setter method collided with an
// ancestor's identically-selectored method and was
// dropped above the property can no longer delegate to
// it safely (it would silently resolve to the ancestor's
// differently-typed method instead).
skips.append(SkipEntry(symbol: "\(moduleName).\(plan.name).\(prop.swiftName)",
cIdentifier: prop.girName, reason: .inheritedMember,
detail: "property '\(prop.swiftName)' delegated to a getter=/setter= method that collided with an ancestor's identically-selectored method and was dropped"))
return false
}
return true
}
guard filteredProps.count != plan.properties.count || filteredMethods.count != plan.methods.count else {
return typePlan
}
changed = true
let newPlan = ClassPlan(
name: plan.name, girName: plan.girName, cType: plan.cType,
parent: plan.parent, parentGIRName: plan.parentGIRName,
isOpen: plan.isOpen, isAbstract: plan.isAbstract,
getTypeFunction: plan.getTypeFunction,
descendsFromInitiallyUnowned: plan.descendsFromInitiallyUnowned,
refFunc: plan.refFunc,
unrefFunc: plan.unrefFunc,
interfaces: plan.interfaces,
constructors: plan.constructors, methods: filteredMethods,
functions: plan.functions, properties: filteredProps,
signals: plan.signals, doc: plan.doc
)
return .class(newPlan)
}
guard changed else { continue }
modulePlans[moduleName] = ModulePlan(
module: modulePlan.module, dependencyModules: modulePlan.dependencyModules,
types: newTypes, skips: skips, coverage: modulePlan.coverage
)
}
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
// A type declared here is skipped if a type of the same GIR name is
// already declared in a dependency module e.g. GObject re-declaring
// GLib's `IOCondition`. Keeping both would make Step 1's unqualified
// cross-module names ambiguous. Applies to type declarations only
// (enums, bitfields, records, classes, interfaces, aliases), never to
// constants/functions/callbacks.
func duplicateDependencyModule(_ girSimpleName: String) -> String? {
context.registry.droppedShadow("\(ns.name).\(girSimpleName)")
}
func skipIfDuplicate(_ girSimpleName: String) -> Bool {
guard let dep = duplicateDependencyModule(girSimpleName) else { return false }
skips.append(SkipEntry(symbol: "\(ns.name).\(girSimpleName)",
cIdentifier: girSimpleName,
reason: .duplicateOfDependency,
detail: "'\(girSimpleName)' is already declared in dependency module '\(dep)'"))
return true
}
// Enumerations
for enumeration in ns.enumerations {
totalTypes += 1
if skipIfDuplicate(enumeration.name) { continue }
boundTypes += planEnum(into: &types, skips: &skips, enumeration: enumeration, context: context)
}
// Bitfields
for bitfield in ns.bitfields {
totalTypes += 1
if skipIfDuplicate(bitfield.name) { continue }
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
if skipIfDuplicate(alias.name) { continue }
boundTypes += planAliasType(into: &types, skips: &skips, alias: alias, context: context)
}
for klass in ns.classes {
totalTypes += 1
if skipIfDuplicate(klass.name) { continue }
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
if skipIfDuplicate(iface.name) { continue }
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
if skipIfDuplicate(record.name) { continue }
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
}
let coverage = CoverageStats(
boundCallables: boundCallables, totalCallables: totalCallables,
boundTypes: boundTypes, totalTypes: totalTypes,
boundCallbacks: boundCallbacks, totalCallbacks: totalCallbacks,
boundSignals: boundSignals, totalSignals: totalSignals
)
return ModulePlan(module: context.currentModule, dependencyModules: context.dependencyModules, types: types, skips: skips, coverage: coverage)
}
// MARK: - Signature deduplication
/// Canonical key for comparing Swift functional signatures (parameter labels
/// + types, throwing-ness, and return type). Excludes instance/out params.
private func signatureKey(_ plan: CallablePlan) -> String {
let retType = plan.returnMapping?.swiftType ?? "Void"
let params = plan.parameters
.filter { !$0.isInstanceParameter && !$0.isOutParameter && $0.synthesizedLengthOf == nil }
.map { "\($0.swiftName):\($0.mapping.swiftType)" }
.joined(separator: ",")
return "throws:\(plan.throwsError)|ret:\(retType)|(\(params))"
}
/// Drops callables whose full Swift signature collides with an earlier one
/// e.g. async `_finish` constructors that all map to the SAME rendered
/// `init(res: AsyncResult) throws` regardless of their distinct Swift names
/// (`newFinish`, `newForAddressFinish`, ): constructors render as bare
/// `init(...)`, so `plan.name` never appears in the emitted signature and
/// MUST be excluded from the dedup key. Methods, by contrast, keep their own
/// name in the emitted signature, so it stays part of their key.
/// The first occurrence wins; later collisions are recorded as skips.
private func dedupBySignature(
_ plans: [CallablePlan], isConstructor: Bool, symbolPrefix: String, skips: inout [SkipEntry]
) -> [CallablePlan] {
var seen: Set<String> = []
var result: [CallablePlan] = []
for plan in plans {
let key = isConstructor ? signatureKey(plan) : "\(plan.name)|\(signatureKey(plan))"
if seen.contains(key) {
let renderedName = isConstructor ? "init" : plan.name
skips.append(SkipEntry(
symbol: "\(symbolPrefix).\(plan.cIdentifier)",
cIdentifier: plan.cIdentifier,
reason: .nameCollision,
detail: "Swift signature '\(renderedName)(\(signatureKey(plan)))' already emitted by an earlier symbol"
))
} else {
seen.insert(key)
result.append(plan)
}
}
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",
// Declared in <gio/gsettingsbackend.h>, which the public umbrella
// (<gio/gio.h>) deliberately does not include (implementor-only API)
// exported by the .so but invisible to the Clang importer.
"g_settings_backend_changed", "g_settings_backend_changed_tree",
"g_settings_backend_get_default", "g_settings_backend_path_changed",
"g_settings_backend_path_writable_changed", "g_settings_backend_writable_changed",
"g_null_settings_backend_new", "g_memory_settings_backend_new", "g_keyfile_settings_backend_new",
// Declared in <gio/gnetworking.h>, likewise excluded from <gio/gio.h>.
"g_networking_init",
// Declared in the GdkPixbuf GIR but not exported through the public
// <gdk-pixbuf/gdk-pixbuf.h> umbrella header.
"gdk_pixbuf_non_anim_new",
// Declared in <gsk/broadway/gskbroadwayrenderer.h>, which the public
// umbrella (<gsk/gsk.h>) deliberately does not include Broadway is an
// optional backend; unlike the GPU renderers (<gsk/gpu/gskglrenderer.h>,
// <gsk/gpu/gskvulkanrenderer.h>, both included), its header is excluded.
"gsk_broadway_renderer_new",
]
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 {
let pair = resolvedCopyFreePair(record)
return RecordPlan(
name: record.name, cType: record.cType,
getTypeFunction: record.getTypeFunction,
copyFunction: pair.copy, copyReturnsVoid: pair.copyReturnsVoid,
freeFunction: pair.free,
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.
///
/// `copyReturnsVoid` flags a chosen copy/ref function whose C return type is
/// `void` (e.g. `gst_atomic_queue_ref`) rather than the pointer most
/// `ref`/`copy` functions follow the `T *fn(T *)` self-returning convention
/// `init(retaining:)` assumes, but some plain refcount bumps don't.
private func resolvedCopyFreePair(_ record: Record) -> (copy: String?, copyReturnsVoid: Bool, free: String?) {
if let explicitCopy = record.copyFunction, let explicitFree = record.freeFunction {
return (explicitCopy, false, explicitFree)
}
// Prefer refcount semantics (ref + unref together).
let ref = record.instanceReleaseMethod(named: ["ref"])
let unref = record.instanceReleaseMethod(named: ["unref"])
if let ref, let unref {
return (ref.cIdentifier, ref.returnsVoid, unref.cIdentifier)
}
// Fall back to copy/free semantics.
let copy = record.instanceReleaseMethod(named: ["copy"])
let free = record.instanceReleaseMethod(named: ["free"])
if let copy, let free {
return (copy.cIdentifier, copy.returnsVoid, free.cIdentifier)
}
// 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, false, record.freeFunction)
}
return (nil, false, nil)
}
func resolvedFreeFunction(_ record: Record) -> String? {
resolvedCopyFreePair(record).free
}
func resolvedCopyFunction(_ record: Record) -> String? {
resolvedCopyFreePair(record).copy
}
extension Record {
/// 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. Returns
/// the C identifier alongside whether the method's return type is
/// `void` (see `resolvedCopyFreePair`'s `copyReturnsVoid`).
func instanceReleaseMethod(named names: [String]) -> (cIdentifier: String, returnsVoid: Bool)? {
for wanted in names {
if let method = methods.first(where: {
$0.name == wanted && $0.symbolInfo.isBindable
&& $0.parameters.allSatisfy(\.isInstanceParameter)
}) {
return (method.cIdentifier, method.returnValue.type == .void)
}
}
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)
}
// A prerequisite that resolves to a *class* (not another interface) means
// every conformer must inherit from that class (Swift: `Self: SomeClass`
// constraint from the protocol's own inheritance clause). The concrete
// `<Name>Ref` wrapper must then subclass it directly, rather than
// declaring its own bare `pointer`/ref-count storage, to satisfy that
// constraint (e.g. `TlsServerConnection` requires `Self: TlsConnection`).
let classPrereq: String? = iface.prereqs.compactMap { prereq -> String? in
let qualified = prereq.contains(".") ? prereq : "\(context.currentNamespace).\(prereq)"
guard let resolved = registry.resolve(girName: qualified) else { return nil }
guard case .object = resolved.category else { return nil }
return registry.swiftTypeName(for: resolved, in: context.currentModule)
}.first
// 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, ownerIsInterface: true, 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,
classPrereq: classPrereq,
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, hasCopyFunction: $0.hasCopyFunction) }
}
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?
let parentGIRNameResolved: 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)
parentGIRNameResolved = resolved.girName
} else {
parentSwiftName = nil // foreign or unknown treat as root
parentGIRNameResolved = nil
}
} else {
parentSwiftName = nil
parentGIRNameResolved = nil
}
let isOpen = registry.subclassedTypes().contains(girName)
let descendsIU = registry.descendsFromInitiallyUnowned(girName)
let (refFunc, unrefFunc) = registry.refUnrefFunctions(for: girName)
// Resolve implemented interfaces to their Swift names, excluding any
// already provided by an ancestor class (redundant conformance is a
// Swift error, e.g. `DataInputStream: , Seekable` when its ancestor
// `BufferedInputStream` already conforms).
let ancestorInterfaceGirNames: Set<String> = Set(registry.ancestry(of: girName).flatMap { ancestor -> [String] in
if case .object(_, _, _, let ifaces, _, _) = ancestor.category { return ifaces }
return []
})
let interfaceNames: [String] = klass.implements.compactMap { ifaceName in
let qualified = ifaceName.contains(".") ? ifaceName : "\(context.currentNamespace).\(ifaceName)"
guard !ancestorInterfaceGirNames.contains(qualified) else { return nil }
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)
}
}
constructorPlans = dedupBySignature(constructorPlans, isConstructor: true, symbolPrefix: girName, skips: &memberSkips)
var methodPlans: [CallablePlan] = []
for method in klass.methods where method.symbolInfo.isBindable {
collect(planMethod(method, declaringGIRName: girName, context: context), into: &methodPlans)
}
methodPlans = dedupBySignature(methodPlans, isConstructor: false, symbolPrefix: girName, skips: &memberSkips)
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)
}
}
// A property whose Swift name collides with a planned method/constructor/
// function of the same class is dropped Swift rejects the redundant
// redeclaration (e.g. `Pango.FontFamily.isVariable` property vs.
// `isVariable()` method, both derived from the same GIR member).
let takenCallableNames: Set<String> = Set(
methodPlans.map(\.name) + constructorPlans.map(\.name) + functionPlans.map(\.name)
)
propertyPlans = propertyPlans.filter { prop in
guard takenCallableNames.contains(prop.swiftName) else { return true }
memberSkips.append(SkipEntry(symbol: "\(girName).\(prop.swiftName)",
cIdentifier: prop.girName, reason: .nameCollision,
detail: "property '\(prop.swiftName)' collides with a method of the same name"))
return false
}
// 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, girName: girName, cType: klass.cType,
parent: parentSwiftName, parentGIRName: parentGIRNameResolved,
isOpen: isOpen,
isAbstract: klass.isAbstract,
getTypeFunction: klass.getTypeFunction,
descendsFromInitiallyUnowned: descendsIU,
refFunc: refFunc,
unrefFunc: unrefFunc,
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 }
// A bitfield/enum-typed constant's GIR value is a bare C integer
// literal (e.g. `"15"`), but the Swift type is an `OptionSet` struct or
// a raw-value `enum` neither is integer-literal-expressible, so the
// literal must be routed through `Type(rawValue:)`. Every other
// constant type (numeric, string, boolean) already carries a
// Swift-literal-compatible value verbatim.
let value: String
switch mapping.marshalIn {
case .bitfieldRaw:
value = "\(mapping.swiftType)(rawValue: numericCast(\(constant.value)))"
case .enumRaw:
value = "\(mapping.swiftType)(rawValue: \(constant.value))!"
default:
value = constant.value
}
return ConstantPlan(name: swiftConstantName(constant.name), girName: constant.name,
value: 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, isOverride: Bool = false, context: MapContext
) -> CallablePlanResult {
// Applies to methods too, not just free functions: some GIR-declared C
// symbols (e.g. GSettingsBackend's `g_settings_backend_*` implementor
// API) are exported by the .so but declared only in a header the public
// umbrella (`<gio/gio.h>`) does not include, so Clang never sees the
// prototype and the generated call is `cannot find in scope`.
if knownMissingCFunctions.contains(cIdentifier) {
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
reason: .unknownType,
detail: "C symbol '\(cIdentifier)' is not visible through the public umbrella header"))
}
// 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"))
}
// A scalar type (`.direct`/`.numericCast` marshalOut) returned
// through a pointer C type with no `<array>` length (e.g.
// `guint8` from `c:type="const guint8*"`) is a raw buffer
// pointer the mapper's scalar-name match hid cannot be
// marshalled as the pointee value.
if returnValue.cType.hasSuffix("*") {
switch returnMap.marshalOut {
case .direct, .numericCast:
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
reason: .unknownType,
detail: "return C type '\(returnValue.cType)' is a pointer to a scalar with no array length"))
default:
break
}
}
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,
isOverride: isOverride, 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`.
/// `declaringGIRName`, when given, is the fully qualified GIR name of the
/// class the method is planned for used to detect whether an ancestor
/// already declares a same-selector method (see
/// `TypeRegistry.overridesAncestorMethod`), requiring Swift's `override`
/// keyword. `nil` for interface methods, which have no class ancestry.
func planMethod(_ method: Method, declaringGIRName: String? = nil, context: MapContext) -> CallablePlanResult {
let isOverride = declaringGIRName.map { girName in
context.registry.overridesAncestorMethod(
named: method.name,
paramNames: method.parameters.filter { !$0.isInstanceParameter }.map(\.name),
in: girName
)
} ?? false
return 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,
isOverride: isOverride, 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, ownerIsInterface: Bool = false,
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)
// The C/Swift identifier segment must be a valid identifier; GIR signal
// names may contain hyphens (`"drive-changed"`). GObject treats `-` and
// `_` as equivalent in signal names, so this cannot collide with a
// distinct signal. The *string* passed to `g_signal_connect_data` keeps
// the original hyphenated `girName` below.
let trampolineSignalSegment = camelCased(signal.name.replacingOccurrences(of: "-", with: "_"))
let trampolineCName = "_trampoline\(namespace)\(className)" + trampolineSignalSegment.prefix(1).uppercased() + trampolineSignalSegment.dropFirst()
let plan = SignalPlan(
owningClassName: className, girName: signal.name,
swiftName: swiftName,
isDetailed: signal.isDetailed,
parameters: signalParams,
returnMapping: returnMapping,
trampolineCName: trampolineCName,
ownerIsInterface: ownerIsInterface,
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)"
if knownMissingCFunctions.contains(ctor.cIdentifier) {
return .skip(SkipEntry(symbol: fullName, cIdentifier: ctor.cIdentifier,
reason: .unknownType,
detail: "C symbol '\(ctor.cIdentifier)' is not visible through the public umbrella header"))
}
// 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"))
}
// Derive ownership from the GIR return's transfer-ownership annotation.
// Most GObject _new constructors return transfer-ownership="full"
// (or a floating reference for InitiallyUnowned descendants). A
// transfer-ownership="none" return indicates a borrowed reference,
// which the convenience init must wrap via init(retaining:) instead
// of init(takingOwnership:).
//
// CRITICAL: descendsIU takes precedence over the GIR annotation because
// most widget constructors declare transfer-ownership="none" even though
// they return a *floating* reference that must be sunk, not borrowed.
let ownership: OwnershipInit
if descendsIU {
ownership = .sinkingRef
} else if ctor.returnValue.transferOwnership == .none {
ownership = .retaining
} else {
ownership = .takingOwnership
}
return .success(CallablePlan(
name: swiftFunctionName(ctor.name), cIdentifier: ctor.cIdentifier,
parameters: paramPlans, returnMapping: nil,
isStatic: false, isConstructor: true,
ownershipInit: ownership,
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))
// Pre-scan length parameters for C arrays of strings (e.g. argc for argv).
// Map the length param's position to the array's Swift name.
let hasInstance = parameters.contains(where: \.isInstanceParameter)
var lengthElision: [Int: String] = [:]
for p in parameters {
if case .cArray(let el, let info) = p.type,
el == .string || el == .filename,
let li = info.lengthParameterIndex {
lengthElision[li + (hasInstance ? 1 : 0)] = swiftParameterName(p.name)
}
}
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,
cType: param.cType) }
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"))
}
// Intercept length params (argc) synthesize from array's count.
if let arrName = lengthElision[index] {
plans.append(ParameterPlan(
swiftName: swiftParameterName(param.name), cArgIndex: index,
mapping: Mapping(swiftType: "Int", cSwiftType: "", marshalIn: .direct, marshalOut: .direct),
synthesizedLengthOf: arrName))
continue
}
// Intercept C arrays of strings with a known length param [String].
if case .cArray(let el, let info) = param.type,
el == .string || el == .filename, info.lengthParameterIndex != nil {
plans.append(ParameterPlan(
swiftName: swiftParameterName(param.name), cArgIndex: index,
mapping: .stringArrayMapping))
continue
}
// 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
// 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 {
let camel = camelCased(girName)
guard !camel.isEmpty else { return girName }
return swiftKeywords.contains(camel) ? "`\(camel)`" : camel
}
/// 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",
]