Relaxes the generated-filename PascalCase validator's uppercase-run cap (>3 -> >4) so legitimate acronym type names (PluginAPIFlags, AuthNTLM) stop crashing generation. Fixes four compounding cross-module correctness bugs the larger tier-6 GIR set exposed at scale: - The duplicate-of-dependency drop only checked simple-name collision, wrongly dropping genuinely distinct C types that merely share a post-namespace-stripping Swift name (Gst.Object/GstObject vs GObject.Object/GObject, plus three tier-4/5 cases: Gdk.AppLaunchContext, Gdk.Gravity, Gdk.Rectangle). Now also requires matching cType. - Qualifying a cross-module reference as "GObject.X" broke wherever a raw C struct also named GObject was in scope (every C target's import), since Swift resolved the module name to the shadowing struct. GObject's Support.swift now exports collision-free GLibObject/GLibValueArray aliases used instead. - Missing `override` keyword: added ancestor-method-selector detection (name + parameter labels, same-module only, since none of these members are open) so a subclass narrowing an ancestor's return type compiles. - The pre-existing cross-module inherited-member dedup pass keyed ancestors by bare Swift name; its cycle guard falsely self-terminated once two classes shared a name, missing real inherited members (e.g. GstObject's own ref()/unref() were never recognized as duplicating GObject.Object's, producing an illegal redeclaration). Rewired to walk by unambiguous GIR name via new ClassPlan.girName/parentGIRName fields. Also fixes bitfield/enum-typed global constants (wraps the raw literal in Type(rawValue:)) and a void-returning ref function (gst_atomic_queue_ref, unlike GstBuffer/GObject's T*-returning convention) via new RecordPlan.copyReturnsVoid. Adds tier-6 smoke tests (Gst/Soup/Adw version calls against the real linked libraries) and refreshes the tier-4/5 skip baselines for the duplicate-detection fix's legitimate coverage growth. Zero skip-baseline drift on tiers 1-6; 220/220 unit tests and all tier smoke suites pass.
1715 lines
83 KiB
Swift
1715 lines
83 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 }
|
|
.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,
|
|
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 }
|
|
.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",
|
|
// 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 = registry.refUnrefFunctions(for: girName).ref
|
|
|
|
// 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,
|
|
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 = signal.name.replacingOccurrences(of: "-", with: "_")
|
|
let trampolineCName = "_trampoline_\(namespace)_\(className)_\(trampolineSignalSegment)"
|
|
|
|
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"))
|
|
}
|
|
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,
|
|
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"))
|
|
}
|
|
|
|
// 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",
|
|
]
|