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

1006 lines
46 KiB
Swift

// Planner.swift
// The binding planner: walks a parsed GIR namespace, resolves every type
// through the registry and TypeMapper, and produces either a complete
// `TypePlan` or a `SkipEntry` with a machine-readable reason.
//
// This is where Rule 3 of the rearchitecture lives: if any part of a symbol
// cannot be planned, the WHOLE symbol is skipped with a reason. There is no
// code path that emits a partially-understood binding.
import Foundation
// MARK: - Module Planner
/// Plans every type in every namespace of a multi-package analysis, producing
/// a `ModulePlan` per generated Swift module.
///
/// - Parameters:
/// - analysis: The resolved multi-package analysis with parsed repositories.
/// - registry: The global type registry built from those repositories.
/// - Returns: A dictionary mapping Swift module name to its `ModulePlan`.
public func planModules(
analysis: MultiPackageAnalysis,
registry: TypeRegistry
) -> [String: ModulePlan] {
var modulePlans: [String: ModulePlan] = [:]
for (moduleName, repo) in analysis.repositories {
let ns = repo.namespaces.first { $0.name == moduleName } ?? repo.namespaces.first
guard let namespace = ns else { continue }
let context = MapContext(
registry: registry,
currentModule: moduleName,
currentNamespace: namespace.name
)
let plan = planNamespace(namespace, context: context)
modulePlans[moduleName] = plan
}
return modulePlans
}
// MARK: - Namespace-level planning
/// Plans every type in a single namespace, delegating to per-category helpers.
private func planNamespace(_ ns: Namespace, context: MapContext) -> ModulePlan {
var types: [TypePlan] = []
var skips: [SkipEntry] = []
var boundTypes = 0
var totalTypes = 0
var boundCallables = 0
var totalCallables = 0
// Enumerations
for enumeration in ns.enumerations {
totalTypes += 1; boundTypes += planEnum(into: &types, skips: &skips, enumeration: enumeration, context: context)
}
// Bitfields
for bitfield in ns.bitfields {
totalTypes += 1; boundTypes += planBit(into: &types, skips: &skips, bitfield: bitfield, context: context)
}
// Constants
// Case conversion can merge distinct GIR names (`CSET_A_2_Z` and
// `CSET_a_2_z` both become `csetA2Z`), so collisions are detected here
// and the later occurrence is skipped.
var seenConstantNames: Set<String> = []
for constant in ns.constants {
totalTypes += 1
boundTypes += planConst(into: &types, skips: &skips, seen: &seenConstantNames,
constant: constant, context: context)
}
// Aliases
for alias in ns.aliases {
totalTypes += 1; boundTypes += planAliasType(into: &types, skips: &skips, alias: alias, context: context)
}
// Classes, Interfaces, Records, Callbacks skip with reasons (Phase B6+)
for klass in ns.classes {
totalTypes += 1
boundTypes += skipClass(into: &skips, into: &types,
boundCallables: &boundCallables, totalCallables: &totalCallables,
klass: klass, namespace: ns.name, context: context)
}
for iface in ns.interfaces {
totalTypes += 1; boundTypes += skipInterface(into: &skips, into: &types, iface: iface, namespace: ns.name, context: context)
}
for record in ns.records {
totalTypes += 1; boundTypes += skipRecord(into: &skips, into: &types, record: record, namespace: ns.name, context: context)
}
for callback in ns.callbacks {
totalTypes += 1; boundTypes += skipCallback(into: &skips, callback: callback, namespace: ns.name)
}
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
)
return ModulePlan(module: context.currentModule, types: types, skips: skips, coverage: coverage)
}
// 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
}
private func skipCallback(into skips: inout [SkipEntry], callback: Callback, namespace: String) -> Int {
let fullName = "\(namespace).\(callback.name)"
skips.append(SkipEntry(symbol: fullName, cIdentifier: callback.cType,
reason: .callbackWithoutUserData, detail: "callback planned for Phase D2"))
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)"
// Symbols the system library does not export (macros, inline functions,
// or GType getters absent from the shared object) cannot be called.
if knownMissingCFunctions.contains(fn.cIdentifier) {
skips.append(SkipEntry(symbol: fullName, cIdentifier: fn.cIdentifier,
reason: .unknownType, detail: "C symbol '\(fn.cIdentifier)' is not exported by the system library"))
return 0
}
if knownMisleadingCFunctions.contains(fn.cIdentifier) {
skips.append(SkipEntry(symbol: fullName, cIdentifier: fn.cIdentifier,
reason: .unknownType, detail: "return type '\(fn.cIdentifier)' maps as value but is actually a C pointer"))
return 0
}
switch planFunction(fn, context: context) {
case .success(let plan):
types.append(.callable(plan))
return 1
case .skip(let entry):
skips.append(entry)
return 0
}
}
/// Known C symbols that aren't exported by the system library
/// (macros, inline functions, or GType getters for types not in the .so).
private let knownMissingCFunctions: Set<String> = [
"g_fsync",
"g_strv_get_type",
"g_variant_get_gtype",
"g_get_monotonic_time_ns", // returns uint64_t (UInt64), not guint64 (UInt)
// gstdio.h wrappers: on non-Windows these are `#define g_open open` macros,
// so no real symbol is exported and Swift cannot import them.
"g_access", "g_chdir", "g_chmod", "g_creat", "g_fopen", "g_freopen",
"g_fsync", "g_lstat", "g_mkdir", "g_open", "g_close", "g_remove", "g_rename",
"g_rmdir", "g_stat", "g_unlink", "g_utime",
// Removed from modern GLib (present in the GIR, absent from the shared
// object) these fail only at link time, not compile time.
"g_thread_init", "g_thread_init_with_errorcheck_mutexes",
]
let knownMisleadingCFunctions: Set<String> = [
"g_utf8_to_ucs4", // returns gunichar* (UInt32*)
"g_utf8_to_ucs4_fast", // returns gunichar* (UInt32*)
"g_utf8_to_utf16", // returns gunichar2* (UInt16*)
"g_get_charset", // const gchar** out-param const/mutable mismatch
"g_get_console_charset", // same as g_get_charset
]
/// Swift type names that boxed records must not shadow.
let reservedSwiftTypes: Set<String> = [
"String", "Int", "Bool", "Double", "Float", "Array", "Dictionary",
"Set", "Optional", "Data", "Date", "URL", "Error", "Void", "Any",
"Object", "Type", "Protocol",
]
/// Plans a boxed record as an opaque pointer wrapper, resolving the copy and
/// free functions that give it correct memory management (C4).
func planRecord(_ record: Record, context: MapContext) -> RecordPlan {
return RecordPlan(
name: record.name, cType: record.cType,
getTypeFunction: record.getTypeFunction,
copyFunction: resolvedCopyFunction(record),
freeFunction: resolvedFreeFunction(record),
doc: record.doc
)
}
/// Resolves a boxed record's destructor C identifier.
///
/// Prefers the explicit GIR `free-function` attribute; otherwise falls back to
/// the record's own parameterless `unref` (preferred, ref-counted) or `free`
/// method. `g_boxed_free` is deliberately *not* used as a fallback: it lives in
/// GObject, which the GLib module cannot link against (GObject depends on GLib,
/// not the reverse). Returns `nil` when no safe destructor exists the wrapper
/// then renders without a `deinit` (documented leak) rather than risk a wrong
/// free.
func resolvedFreeFunction(_ record: Record) -> String? {
if let explicit = record.freeFunction { return explicit }
return record.instanceReleaseMethod(named: ["unref", "free"])
}
/// Resolves a boxed record's copy/ref C identifier.
///
/// Prefers the explicit GIR `copy-function` attribute; otherwise falls back to
/// the record's own parameterless `ref` (preferred, ref-counted) or `copy`
/// method. Returns `nil` when none exists the wrapper then renders without
/// `init(retaining:)`, and borrowed (`transfer-ownership="none"`) returns of
/// this type must not be adopted as owned.
func resolvedCopyFunction(_ record: Record) -> String? {
if let explicit = record.copyFunction { return explicit }
return record.instanceReleaseMethod(named: ["ref", "copy"])
}
extension Record {
/// The C identifier of the first bindable instance method matching one of
/// `names` (in priority order) that takes no arguments beyond the instance
/// itself the shape every canonical `ref`/`unref`/`copy`/`free` shares.
func instanceReleaseMethod(named names: [String]) -> String? {
for wanted in names {
if let method = methods.first(where: {
$0.name == wanted && $0.symbolInfo.isBindable
&& $0.parameters.allSatisfy(\.isInstanceParameter)
}) {
return method.cIdentifier
}
}
return nil
}
}
/// Plans an interface as a Swift protocol, planning its instance methods as
/// protocol requirements.
///
/// A GObject interface method is dispatched through the implementing class's
/// instance pointer the concrete C symbol (e.g. `gtk_widget_get_buildable_id`)
/// lives on the class, not the interface. So each interface method becomes a
/// protocol *requirement* only; the implementing class supplies the body via
/// its own `planClass` methods. Methods that cannot be planned (unsupported
/// types, varargs, ) are returned as skip entries, mirroring `planClass`.
///
/// - Returns: The interface plan and the skip entries for its unplannable
/// methods.
func planInterface(_ iface: Interface, context: MapContext) -> (plan: InterfacePlan, memberSkips: [SkipEntry]) {
let registry = context.registry
// Resolve prerequisite types through registry
let prereqSwiftNames: [String] = iface.prereqs.compactMap { prereq in
let qualified = prereq.contains(".") ? prereq : "\(context.currentNamespace).\(prereq)"
guard let resolved = registry.resolve(girName: qualified) else { return nil }
return registry.swiftTypeName(for: resolved, in: context.currentModule)
}
// Instance methods become protocol requirements. Unplannable ones are
// recorded as skips, matching class member behaviour.
var memberSkips: [SkipEntry] = []
var methodPlans: [CallablePlan] = []
for method in iface.methods where method.symbolInfo.isBindable {
switch planMethod(method, context: context) {
case .success(let plan): methodPlans.append(plan)
case .skip(let entry): memberSkips.append(entry)
}
}
let plan = InterfacePlan(
name: iface.name, cType: iface.cType,
prereqs: prereqSwiftNames,
getTypeFunction: iface.getTypeFunction,
methods: methodPlans,
doc: iface.doc
)
return (plan, memberSkips)
}
/// Plans a class as a GObject wrapper with cross-module inheritance, planning
/// its constructors, instance methods, and static functions. Members that
/// cannot be planned are returned as skip entries (the class itself is still
/// generated Rule 3 skips the *member*, not the whole type).
///
/// - Returns: The class plan and the skip entries for its unplannable members.
func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberSkips: [SkipEntry]) {
let girName = "\(context.currentNamespace).\(klass.name)"
let registry = context.registry
// Resolve parent
let parentSwiftName: String?
if let parentGIR = klass.parent {
let qualifiedParent = parentGIR.contains(".") ? parentGIR : "\(context.currentNamespace).\(parentGIR)"
if let resolved = registry.resolve(girName: qualifiedParent) {
parentSwiftName = registry.swiftTypeName(for: resolved, in: context.currentModule)
} else {
parentSwiftName = nil // foreign or unknown treat as root
}
} else {
parentSwiftName = nil
}
let isOpen = registry.subclassedTypes().contains(girName)
let descendsIU = registry.descendsFromInitiallyUnowned(girName)
// Resolve implemented interfaces to their Swift names
let interfaceNames: [String] = klass.implements.compactMap { ifaceName in
let qualified = ifaceName.contains(".") ? ifaceName : "\(context.currentNamespace).\(ifaceName)"
guard let resolved = registry.resolve(girName: qualified) else { return nil }
return registry.swiftTypeName(for: resolved, in: context.currentModule)
}
// Members
var memberSkips: [SkipEntry] = []
/// Records the outcome of planning one member.
func collect(_ result: CallablePlanResult, into plans: inout [CallablePlan]) {
switch result {
case .success(let plan): plans.append(plan)
case .skip(let entry): memberSkips.append(entry)
}
}
var constructorPlans: [CallablePlan] = []
// Abstract classes cannot be instantiated directly their constructors
// belong to concrete subclasses, so skip them here.
if !klass.isAbstract {
for ctor in klass.constructors where ctor.symbolInfo.isBindable {
collect(planConstructor(ctor, className: klass.name, descendsIU: descendsIU, context: context),
into: &constructorPlans)
}
}
var methodPlans: [CallablePlan] = []
for method in klass.methods where method.symbolInfo.isBindable {
collect(planMethod(method, context: context), into: &methodPlans)
}
var functionPlans: [CallablePlan] = []
for fn in klass.functions where fn.symbolInfo.isBindable {
collect(planFunction(fn, context: context), into: &functionPlans)
}
let plan = ClassPlan(
name: klass.name, cType: klass.cType,
parent: parentSwiftName,
isOpen: isOpen,
isAbstract: klass.isAbstract,
getTypeFunction: klass.getTypeFunction,
descendsFromInitiallyUnowned: descendsIU,
interfaces: interfaceNames,
constructors: constructorPlans,
methods: methodPlans,
functions: functionPlans,
doc: klass.doc
)
return (plan, memberSkips)
}
/// Plans an enumeration, deduplicating raw values.
///
/// Members with duplicate raw values become `public static var` aliases
/// pointing to the first case with that value.
func planEnumeration(_ enumeration: Enumeration, context: MapContext) -> EnumPlan {
var cases: [EnumCase] = []
var aliases: [EnumAlias] = []
var seenRawValues: [String: String] = [:] // rawValue first case name
for member in enumeration.members {
let swiftName = swiftEnumCaseName(member.name)
if let firstCase = seenRawValues[member.value] {
aliases.append(EnumAlias(name: swiftName, targetCaseName: firstCase))
} else {
cases.append(EnumCase(name: swiftName, rawValue: member.value, cIdentifier: member.cIdentifier))
seenRawValues[member.value] = swiftName
}
}
return EnumPlan(
name: enumeration.name, cType: enumeration.cType,
cases: cases, aliases: aliases,
hasGType: enumeration.getTypeFunction != nil,
doc: enumeration.doc
)
}
/// Plans a bitfield as an `OptionSet` with verbatim raw values.
func planBitfield(_ bitfield: Bitfield, context: MapContext) -> BitfieldPlan {
let members = bitfield.members.map { member in
EnumCase(
name: swiftEnumCaseName(member.name),
rawValue: member.value,
cIdentifier: member.cIdentifier
)
}
return BitfieldPlan(
name: bitfield.name, cType: bitfield.cType,
members: members,
hasGType: bitfield.getTypeFunction != nil,
doc: bitfield.doc
)
}
/// Plans a global constant. Returns `nil` when the constant's type cannot
/// be mapped, so the caller records a skip entry.
///
/// The GIR name (`PARAM_MASK`) is converted to Swift's lowerCamelCase
/// convention for constants (`paramMask`, like `Double.pi`); the original
/// spelling is preserved in `girName` for the rendered doc comment.
func planConstant(_ constant: Constant, context: MapContext) -> ConstantPlan? {
let mappingResult = Result { try map(constant.type, nullable: false, transfer: .none, context: context) }
guard case .success(let mapping) = mappingResult else { return nil }
return ConstantPlan(name: swiftConstantName(constant.name), girName: constant.name,
value: constant.value, swiftType: mapping.swiftType, doc: constant.doc)
}
/// Outcome of alias planning: success with a plan, or skip with a reason.
enum AliasPlanResult {
case success(AliasPlan)
case skip(SkipEntry)
}
/// Plans a type alias, resolving the target type through the mapper.
func planAlias(_ alias: Alias, context: MapContext) -> AliasPlanResult {
let fullName = "\(context.currentNamespace).\(alias.name)"
let mappingResult = Result { try map(alias.target, nullable: false, transfer: .none, context: context) }
switch mappingResult {
case .success(let mapping):
return .success(AliasPlan(name: alias.name, swiftType: mapping.swiftType, doc: alias.doc))
case .failure(let error as MapError):
return .skip(SkipEntry(symbol: fullName, cIdentifier: alias.cType,
reason: error.reason, detail: error.detail))
case .failure:
return .skip(SkipEntry(symbol: fullName, cIdentifier: alias.cType,
reason: .unknownType, detail: "alias target type could not be mapped"))
}
}
// MARK: - Callable planning
/// Outcome of trying to plan a callable: either a complete plan or a skip entry.
enum CallablePlanResult {
case success(CallablePlan)
case skip(SkipEntry)
}
/// Plans a function or method: checks every parameter and the return type for
/// mappability, skipping the whole callable if any part fails. The instance
/// parameter (for methods) is handled by `planParameters` as a `self`
/// passthrough.
///
/// - Parameters:
/// - fullName: Fully qualified GIR symbol name for skip reporting.
/// - swiftName: The Swift function/method name (keyword-escaped).
/// - cIdentifier: The C function symbol to call.
/// - parameters: The callable's parameters (may include an instance parameter).
/// - returnValue: The callable's return value.
/// - throwsGError: Whether the callable takes a trailing `GError**`.
/// - doc: Documentation from the GIR.
/// - isStatic: `true` for free/static functions, `false` for instance methods.
/// - context: The resolution context.
/// - Returns: A complete plan, or a skip entry with a machine-readable reason.
private func planCallable(
fullName: String, swiftName: String, cIdentifier: String,
parameters: [Parameter], returnValue: ReturnValue, throwsGError: Bool,
doc: String?, isStatic: Bool, context: MapContext
) -> CallablePlanResult {
// Check parameters (GError** is implicit in throws="1", not in the
// parameter list the renderer synthesizes the &error arg via
// cArguments(error: true). Do NOT strip the last parameter, it's real.)
let paramPlanResult = planParameters(parameters, context: context)
guard case .success(let paramPlans) = paramPlanResult else {
if case .skip(let entry) = paramPlanResult {
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
reason: entry.reason, detail: entry.detail))
}
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
reason: .unknownType, detail: "parameter planning failed"))
}
// Map return type
let returnMapping: Mapping?
if returnValue.type != .void {
switch Result(catching: { try map(returnValue.type, nullable: returnValue.isNullable,
transfer: returnValue.transferOwnership, context: context) }) {
case .success(let returnMap):
if !returnMap.isReadyForCallables {
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
reason: .unknownType,
detail: "return type '\(returnMap.swiftType)' is not yet generated (category: \(returnMap.category))"))
}
// Interfaces produce unsupported marshalOut (protocols can't be
// constructed). Params still work via .objectPointer.
if case .unsupported(let reason) = returnMap.marshalOut {
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
reason: .unknownType, detail: "return type: \(reason)"))
}
returnMapping = returnMap
case .failure(let error as MapError):
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
reason: error.reason, detail: error.detail))
case .failure:
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
reason: .unknownType, detail: "return type mapping failed"))
}
} else {
returnMapping = nil
}
return .success(CallablePlan(
name: swiftName, cIdentifier: cIdentifier,
parameters: paramPlans, returnMapping: returnMapping,
isStatic: isStatic, isConstructor: false, ownershipInit: nil, throwsError: throwsGError, doc: doc
))
}
/// Plans a namespace-level (or static) function.
func planFunction(_ fn: GlobalFunction, context: MapContext) -> CallablePlanResult {
planCallable(
fullName: "\(context.currentNamespace).\(fn.name)",
swiftName: swiftFunctionName(fn.name), cIdentifier: fn.cIdentifier,
parameters: fn.parameters, returnValue: fn.returnValue,
throwsGError: fn.throwsGError, doc: fn.doc, isStatic: true, context: context)
}
/// Plans an instance method. The instance parameter becomes `self.pointer`.
func planMethod(_ method: Method, context: MapContext) -> CallablePlanResult {
planCallable(
fullName: "\(context.currentNamespace).\(method.name)",
swiftName: swiftFunctionName(method.name), cIdentifier: method.cIdentifier,
parameters: method.parameters, returnValue: method.returnValue,
throwsGError: method.throwsGError, doc: method.doc, isStatic: false, context: context)
}
/// Plans a constructor as a Swift `convenience init`. The C constructor's
/// returned instance pointer is adopted through the class's designated
/// `init(takingOwnership:)`, which sinks a floating reference when the class
/// descends from `InitiallyUnowned`.
///
/// - Parameters:
/// - ctor: The GIR constructor.
/// - className: The owning class's name (for skip reporting).
/// - descendsIU: Whether the class descends from `InitiallyUnowned`.
/// - context: The resolution context.
/// - Returns: A constructor plan, or a skip entry.
func planConstructor(_ ctor: Constructor, className: String, descendsIU: Bool, context: MapContext) -> CallablePlanResult {
let fullName = "\(context.currentNamespace).\(className).\(ctor.name)"
// GError** is implicit in throws="1", not in the parameter list.
let paramPlanResult = planParameters(ctor.parameters, context: context)
guard case .success(let paramPlans) = paramPlanResult else {
if case .skip(let entry) = paramPlanResult {
return .skip(SkipEntry(symbol: fullName, cIdentifier: ctor.cIdentifier,
reason: entry.reason, detail: entry.detail))
}
return .skip(SkipEntry(symbol: fullName, cIdentifier: ctor.cIdentifier,
reason: .unknownType, detail: "constructor parameter planning failed"))
}
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] = []
for (index, param) in parameters.enumerated() {
// The instance parameter is always `self` passed as `self.pointer`,
// never type-checked (its type is the enclosing class, which is a
// `.needsClass` mapping that would otherwise fail the ready check).
if param.isInstanceParameter {
plans.append(ParameterPlan(
swiftName: "self", cArgIndex: index,
mapping: Mapping(swiftType: "Self", cSwiftType: "UnsafeMutableRawPointer",
marshalIn: .direct, marshalOut: .direct),
isInstanceParameter: true))
continue
}
// Variadic parameter (GIR names it "..." with a <varargs/> child):
// C variadic calls cannot be bridged from Swift.
if param.name == "..." {
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .varargs, detail: "variadic ('...') parameter"))
}
// Direction check
if param.direction == .out {
// Caller-allocates out-params (`char *outbuf`, `gunichar *result`)
// hand the C function a pointer to storage the CALLER must size and
// own unlike callee-allocates out-params (`T **out`) where the
// callee fills in the pointee. Our out-param marshalling declares a
// single Swift scalar and passes its address, which is correct only
// for the callee-allocates shape; for caller-allocates it lets C
// write buffer payload past a one-element slot (memory corruption)
// or reinterpret payload as a pointer (segfault, e.g.
// g_unichar_to_utf8). GIR carries no buffer-size field, so there is
// nothing to allocate against skip rather than guess.
if param.callerAllocates {
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .outParameter,
detail: "caller-allocates out-param '\(param.name)' (no buffer size in GIR)"))
}
// Out-params are collected and returned as Swift values rather than
// passed as arguments. Map the VALUE type (not the pointer-to-pointer).
let mappingResult = Result { try map(param.type, nullable: param.isNullable,
transfer: param.transferOwnership, context: context) }
switch mappingResult {
case .success(let paramMapping):
if !paramMapping.isReadyForCallables {
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .unknownType,
detail: "out-param '\(param.name)' type '\(paramMapping.swiftType)' not yet generated"))
}
// Skip object/boxed out-params wrapper construction from a
// raw pointer requires the type's init(takingOwnership:) which
// is class-specific and deferred.
if paramMapping.category == .needsClass || paramMapping.category == .needsRecord {
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .outParameter,
detail: "object/boxed out-param '\(param.name)' deferred"))
}
let swiftName = swiftParameterName(param.name)
plans.append(ParameterPlan(
swiftName: swiftName, cArgIndex: index,
mapping: paramMapping, isOutParameter: true))
continue
case .failure(let error as MapError):
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: error.reason,
detail: "out-param '\(param.name)': \(error.detail)"))
case .failure:
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .unknownType,
detail: "out-param '\(param.name)' type not mappable"))
}
}
if param.direction == .inout_ {
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .inoutParameter, detail: "'\(param.name)' has direction=inout"))
}
// Map the type
let mappingResult = Result { try map(param.type, nullable: param.isNullable,
transfer: param.transferOwnership, context: context) }
switch mappingResult {
case .success(let paramMapping):
if !paramMapping.isReadyForCallables {
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .unknownType,
detail: "parameter '\(param.name)' type '\(paramMapping.swiftType)' is not yet generated"))
}
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
// boxed records need address-of / wrapper marshalling deferred.
// Object and boxed params have their own marshalIn paths (.objectPointer,
// .boxedPointer) that pass the wrapper's pointer.
if param.cType.hasSuffix("*") && !isString,
paramMapping.category != .needsClass, paramMapping.category != .needsRecord {
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
reason: .unknownType, detail: "parameter '\(param.name)' C type '\(param.cType)' is a pointer"))
}
let swiftName = swiftParameterName(param.name)
plans.append(ParameterPlan(
swiftName: swiftName, cArgIndex: index,
mapping: paramMapping, isInstanceParameter: param.isInstanceParameter
))
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 parts = snake.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",
]