1
0
Fork 0
gobject-generator/Sources/SwiftGtkGenCore/BindingPlan.swift
Brendan Szymanski 164f8b7b9a Generate boxed-record memory management and interface requirements
Boxed records (C4): resolve each record's copy/free function from the GIR
copy-function/free-function attribute, else its own ref/copy and unref/free
method, and render init(retaining:) plus an isolated deinit. The deinit is
isolated because the package's default MainActor isolation makes accessing the
non-Sendable pointer from a nonisolated deinit a hard error. The registry uses
the same resolution so transfer=none returns copy instead of adopting a
borrowed pointer. This supersedes the earlier deferral: the hypothesised
OpaquePointer/Sendable barriers either did not hold or were solvable.

Interfaces (C5): plan each interface method through planMethod and render it as
a protocol requirement (no body); the conforming class supplies the C call.
2026-07-17 21:04:37 -04:00

557 lines
24 KiB
Swift

// BindingPlan.swift
// Types describing the outcome of binding analysis: which symbols were bound,
// which were skipped, and why. This file is the foundation of the binding-plan
// layer the explicit marshalling plans (`CallablePlan` et al.) that replace
// direct IR-to-string code generation are added here as the layer is built out.
import Foundation
/// Whether a mapped type's Swift declaration has been generated yet.
/// The planner uses this to defer callables that reference types not yet
/// emitted (e.g. functions taking `Source` before the `Source` class is
/// generated).
public enum BindingCategory: String, Equatable, Sendable {
/// Primitive, enum, bitfield, string, or alias no dependencies.
case ready
/// Object or interface needs a class/stub before callables can use it.
case needsClass
/// Boxed record needs a record wrapper.
case needsRecord
/// A type that will never be generated (foreign, unknown).
case unavailable
}
// MARK: - Value Marshalling
/// How a Swift value is marshalled **into** a C function call argument.
///
/// Describes the transformation applied to each parameter before the call.
/// The renderer never sees `GIRType` it only combines these marshalling
/// shapes with the plan's type names.
public enum MarshalIn: Equatable, Sendable {
/// Passed directly with no conversion (e.g. `Int32` `gint32`).
case direct
/// Passed through a numeric cast to the target type (e.g. `Int` `gint8`).
/// - Parameter targetType: The Swift numeric type to cast to.
case numericCast(targetType: String)
/// Convert `Bool` to `gboolean` via `? 1 : 0`.
case boolToGboolean
/// Convert `String` to a C string (`withCString` / `utf8` pointer).
case stringToC
/// Access the underlying pointer of an object or interface wrapper.
case objectPointer
/// Pass the `rawValue` of an enum (Int C int).
case enumRaw
/// Pass the `rawValue` of a bitfield (UInt32 C uint).
case bitfieldRaw
/// Pass the pointer of a boxed record wrapper.
case boxedPointer
/// Unsupported causes the whole callable to be skipped.
/// - Parameter reason: Why the parameter cannot be marshalled.
case unsupported(reason: String)
}
/// How a C return value is marshalled **out** into Swift.
///
/// Describes the transformation applied to the C function's return value
/// to produce the Swift-typed result that callers see.
public enum MarshalOut: Equatable, Sendable {
/// Direct assignment with no conversion.
case direct
/// Cast from a C numeric type to the target Swift type.
/// - Parameter fromType: The C type being cast from.
case numericCast(fromType: String)
/// Convert `gboolean` (Int32) to `Bool` via `!= 0`.
case gbooleanToBool
/// Copy a C string, optionally freeing the source.
/// - Parameter free: When `true`, the caller owns the string and must `g_free` it.
case stringCopy(free: Bool)
/// Wrap an object pointer, optionally sinking a floating ref.
/// - Parameter sink: When `true`, call `g_object_ref_sink` (for InitiallyUnowned constructors).
case objectWrap(sink: Bool)
/// Wrap an object pointer with `g_object_ref` (for transfer=none returns).
case objectRetain
/// Convert an integer raw value to an enum case.
/// - Parameter swiftType: The fully qualified Swift enum type name.
case enumFromRaw(swiftType: String)
/// Convert a C flags value to a Swift `OptionSet` bitfield.
/// - Parameter swiftType: The fully qualified Swift bitfield type name.
case bitfieldFromRaw(swiftType: String)
/// Wrap a boxed record pointer, optionally copying.
/// - Parameter copy: When `true`, call the copy function to take a reference.
/// - Parameter copyFunction: The C copy/ref function from the GIR (e.g. `g_value_copy`).
case boxedWrap(copy: Bool, copyFunction: String?)
/// Unsupported causes the whole callable to be skipped.
/// - Parameter reason: Why the return value cannot be marshalled.
case unsupported(reason: String)
}
/// How a type is accessed through the GValue machinery.
///
/// Properties without a GIR `getter=` / `setter=` annotation fall back to
/// `g_object_get` / `g_object_set`, which dispatches through the GType system.
/// These ops supply the correct `g_value_get_*` / `g_value_set_*` calls and
/// any follow-on cast for the type.
public struct GValueOps: Equatable, Sendable {
/// The GType macro name used with `g_object_get`/`_set`.
/// Examples: `"G_TYPE_INT"`, `"G_TYPE_STRING"`, `"G_TYPE_OBJECT"`.
public let typeMacro: String
/// The `g_value_get_*` function suffix, e.g. `"int"`, `"enum"`, `"boxed"`.
public let getterSuffix: String
/// The `g_value_set_*` function suffix, e.g. `"int"`, `"enum"`, `"boxed"`.
public let setterSuffix: String
/// Whether the getter result needs a cast to the specific type (e.g.
/// `g_value_get_enum` returns a bare `gint` and must be cast to the enum).
public let needsCast: Bool
public init(typeMacro: String, getterSuffix: String, setterSuffix: String, needsCast: Bool = false) {
self.typeMacro = typeMacro; self.getterSuffix = getterSuffix
self.setterSuffix = setterSuffix; self.needsCast = needsCast
}
}
/// The reason a GIR symbol was excluded from binding generation.
///
/// Every symbol the generator cannot (or should not) bind is skipped as a
/// whole, with one of these reasons recorded in the module's ``SkipReport``.
/// The compile gate diffs reports against a committed baseline: a new skip in
/// a category the generator claims to support is a regression.
public enum SkipReason: String, Codable, CaseIterable, Sendable {
/// The symbol references a namespace with no loaded GIR (e.g. `cairo`).
case foreignNamespace
/// A parameter, return, or field type could not be resolved.
case unknownType
/// The callable is variadic (`...` / `va_list`), which cannot be bridged.
case varargs
/// The callable has an unsupported `direction="out"` parameter.
case outParameter
/// The callable has an unsupported `direction="inout"` parameter.
case inoutParameter
/// A C array parameter or return has no length annotation.
case arrayWithoutLength
/// The type involves a container (GList, GSList, GHashTable, ) that is
/// not yet bridged.
case containerType
/// A callback parameter has no user-data slot, so a Swift closure cannot
/// be attached.
case callbackWithoutUserData
/// A callback parameter has a `scope` the generator does not support.
case unsupportedCallbackScope
/// The record has no GType registration and no configured lifetime
/// functions, so it cannot be safely wrapped.
case plainRecord
/// The record is a class/interface structure (`glib:is-gtype-struct-for`)
/// and is never bound.
case gtypeStruct
/// The symbol is marked `introspectable="0"` in the GIR.
case notIntrospectable
/// The symbol is deprecated and configured for removal.
case deprecatedRemoved
/// The symbol is shadowed by another (`shadowed-by`).
case shadowedSymbol
/// The symbol was explicitly ignored via configuration.
case configIgnored
/// A property type has no supported GValue accessor category.
case unsupportedGValueCategory
}
/// A single skipped symbol: what was skipped, and why.
///
/// Entries are written to `skip-reports/<Module>.json` by the CLI and diffed
/// by `scripts/compile-gate.sh` against `docs/skip-baseline/`.
public struct SkipEntry: Codable, Equatable, Sendable {
/// Fully qualified GIR symbol name, e.g. `"Gtk.Widget.measure"`.
public let symbol: String
/// The C identifier of the symbol, when known (e.g. `"gtk_widget_measure"`).
public let cIdentifier: String?
/// The category of the skip.
public let reason: SkipReason
/// Human-readable specifics, e.g. `"parameter 'baseline' has direction=out"`.
public let detail: String
/// Creates a skip entry.
///
/// - Parameters:
/// - symbol: Fully qualified GIR symbol name.
/// - cIdentifier: The C identifier, if known.
/// - reason: The skip category.
/// - detail: Human-readable specifics for the report.
public init(symbol: String, cIdentifier: String? = nil, reason: SkipReason, detail: String) {
self.symbol = symbol
self.cIdentifier = cIdentifier
self.reason = reason
self.detail = detail
}
}
/// Binding coverage statistics for one generated module.
///
/// Coverage is the fraction of introspectable callables that received a
/// complete binding plan. The compile gate requires coverage to be
/// monotonically non-decreasing across changes.
public struct CoverageStats: Codable, Equatable, Sendable {
/// Number of callables successfully planned and emitted.
public var boundCallables: Int
/// Total introspectable callables considered (bound + skipped).
public var totalCallables: Int
/// Number of types successfully planned and emitted.
public var boundTypes: Int
/// Total introspectable types considered (bound + skipped).
public var totalTypes: Int
/// Creates coverage statistics.
///
/// - Parameters:
/// - boundCallables: Callables successfully planned and emitted.
/// - totalCallables: Total introspectable callables considered.
/// - boundTypes: Types successfully planned and emitted.
/// - totalTypes: Total introspectable types considered.
public init(boundCallables: Int = 0, totalCallables: Int = 0, boundTypes: Int = 0, totalTypes: Int = 0) {
self.boundCallables = boundCallables
self.totalCallables = totalCallables
self.boundTypes = boundTypes
self.totalTypes = totalTypes
}
}
/// The complete skip report for one generated module.
///
/// Serialized as stable, sorted, pretty-printed JSON so reports are
/// line-diffable by the compile gate and reviewable in version control.
public struct SkipReport: Codable, Equatable, Sendable {
/// The Swift module this report describes (e.g. `"GLib"`).
public let module: String
/// All skipped symbols, sorted by symbol name.
public let entries: [SkipEntry]
/// Coverage statistics for the module.
public let stats: CoverageStats
/// Creates a skip report, sorting entries by symbol name for stable output.
///
/// - Parameters:
/// - module: The Swift module name.
/// - entries: Skipped symbols in any order; stored sorted.
/// - stats: Coverage statistics for the module.
public init(module: String, entries: [SkipEntry], stats: CoverageStats) {
self.module = module
self.entries = entries.sorted { $0.symbol < $1.symbol }
self.stats = stats
}
/// Encodes the report as stable pretty-printed JSON with sorted keys.
///
/// - Returns: UTF-8 JSON data suitable for writing to disk and diffing.
/// - Throws: An `EncodingError` if serialization fails.
public func jsonData() throws -> Data {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
return try encoder.encode(self)
}
}
// MARK: - Type Plans
/// The binding plan for a single GIR type (enum, bitfield, constant, alias,
/// class, interface, record, or callback). The renderer consumes these and
/// never sees `GIRType`, `IRModel`, or the registry directly.
public enum TypePlan: Sendable {
/// An enumeration with deduplicated raw values.
case enumeration(EnumPlan)
/// An OptionSet bitfield with verbatim raw values.
case bitfield(BitfieldPlan)
/// A global constant.
case constant(ConstantPlan)
/// A type alias.
case alias(AliasPlan)
/// A GObject class wrapper.
case `class`(ClassPlan)
/// A GObject interface wrapper.
case interface(InterfacePlan)
/// A boxed record wrapper.
case record(RecordPlan)
/// A callable (function, method, or constructor).
case callable(CallablePlan)
}
/// The plan for a GIR enumeration: cases and alias variables (deduplicated
/// raw values).
public struct EnumPlan: Equatable, Sendable {
/// The Swift enum name (unqualified), e.g. `"ConnectFlags"`.
public let name: String
/// The C type name, e.g. `"GConnectFlags"`.
public let cType: String
/// The primary cases (first occurrence of each raw value).
public let cases: [EnumCase]
/// Duplicate raw values emitted as `public static var` aliases.
public let aliases: [EnumAlias]
/// Whether this enum has a GType registration (for GValue access).
public let hasGType: Bool
/// Documentation from the GIR `<doc>` element.
public let doc: String?
public init(name: String, cType: String, cases: [EnumCase], aliases: [EnumAlias],
hasGType: Bool = false, doc: String? = nil) {
self.name = name; self.cType = cType; self.cases = cases
self.aliases = aliases; self.hasGType = hasGType; self.doc = doc
}
}
/// A single case of an enumeration: name, raw value, and C identifier.
public struct EnumCase: Equatable, Sendable {
/// The Swift case name (keyword-escaped), e.g. `"after"`.
public let name: String
/// The numeric raw value as a string, e.g. `"1"`.
public let rawValue: String
/// The full C identifier, e.g. `"G_CONNECT_AFTER"`.
public let cIdentifier: String
public init(name: String, rawValue: String, cIdentifier: String) {
self.name = name; self.rawValue = rawValue; self.cIdentifier = cIdentifier
}
}
/// A duplicate raw-value member emitted as a `public static var` alias.
public struct EnumAlias: Equatable, Sendable {
/// The alias variable name, e.g. `"baselineFill"`.
public let name: String
/// The primary case this alias points to, e.g. `"baseline"`.
public let targetCaseName: String
public init(name: String, targetCaseName: String) {
self.name = name; self.targetCaseName = targetCaseName
}
}
/// The plan for a GIR bitfield (flags type) emitted as an `OptionSet`.
public struct BitfieldPlan: Equatable, Sendable {
/// The Swift struct name (unqualified), e.g. `"ConnectFlags"`.
public let name: String
/// The C type name, e.g. `"GConnectFlags"`.
public let cType: String
/// The flag members with their verbatim raw values.
public let members: [EnumCase]
/// Whether this bitfield has a GType registration.
public let hasGType: Bool
/// Documentation from the GIR `<doc>` element.
public let doc: String?
public init(name: String, cType: String, members: [EnumCase],
hasGType: Bool = false, doc: String? = nil) {
self.name = name; self.cType = cType; self.members = members
self.hasGType = hasGType; self.doc = doc
}
}
/// The plan for a global constant.
public struct ConstantPlan: Equatable, Sendable {
/// The Swift name of the constant, e.g. `"MAJOR_VERSION"`.
public let name: String
/// The literal value as a string, e.g. `"2"`.
public let value: String
/// The Swift type of the constant, e.g. `"Int"`.
public let swiftType: String
/// Documentation from the GIR `<doc>` element.
public let doc: String?
public init(name: String, value: String, swiftType: String, doc: String? = nil) {
self.name = name; self.value = value; self.swiftType = swiftType; self.doc = doc
}
}
/// The plan for a type alias.
public struct AliasPlan: Equatable, Sendable {
/// The Swift alias name, e.g. `"MyType"`.
public let name: String
/// The target type name, module-qualified when cross-module.
public let swiftType: String
/// Documentation from the GIR `<doc>` element.
public let doc: String?
public init(name: String, swiftType: String, doc: String? = nil) {
self.name = name; self.swiftType = swiftType; self.doc = doc
}
}
/// The plan for a GObject class wrapper.
public struct ClassPlan: Equatable, Sendable {
/// The Swift class name (unqualified), e.g. `"Object"`.
public let name: String
/// The C type name, e.g. `"GObject"`.
public let cType: String
/// The module-qualified Swift parent class name, or `nil` for root.
/// E.g. `nil` for `GObject.Object`, `"GObject.InitiallyUnowned"` for `Gtk.Widget`.
public let parent: String?
/// Whether the class is `open` (subclassed by at least one other type).
public let isOpen: Bool
/// Whether the class is abstract no constructors emitted.
public let isAbstract: Bool
/// The `glib:get-type` function name, e.g. `"g_object_get_type"`.
public let getTypeFunction: String?
/// Whether this class descends from `InitiallyUnowned`, requiring
/// `g_object_ref_sink` on construction.
public let descendsFromInitiallyUnowned: Bool
/// Planned constructors (empty for abstract classes).
public let constructors: [CallablePlan]
/// Planned instance methods.
public let methods: [CallablePlan]
/// Planned static/class functions.
public let functions: [CallablePlan]
/// Module-qualified names of implemented interfaces (e.g. ["GObject.TypePlugin"]).
public let interfaces: [String]
/// Documentation from the GIR `<doc>` element.
public let doc: String?
public init(name: String, cType: String, parent: String? = nil,
isOpen: Bool = false, isAbstract: Bool = false,
getTypeFunction: String? = nil,
descendsFromInitiallyUnowned: Bool = false,
interfaces: [String] = [],
constructors: [CallablePlan] = [], methods: [CallablePlan] = [],
functions: [CallablePlan] = [], doc: String? = nil) {
self.name = name; self.cType = cType; self.parent = parent
self.isOpen = isOpen; self.isAbstract = isAbstract
self.getTypeFunction = getTypeFunction
self.descendsFromInitiallyUnowned = descendsFromInitiallyUnowned
self.interfaces = interfaces
self.constructors = constructors; self.methods = methods
self.functions = functions; self.doc = doc
}
}
/// The plan for a boxed record wrapper (opaque pointer class).
public struct RecordPlan: Equatable, Sendable {
public let name: String
public let cType: String
public let getTypeFunction: String?
public let copyFunction: String?
public let freeFunction: String?
public let doc: String?
public init(name: String, cType: String, getTypeFunction: String? = nil,
copyFunction: String? = nil, freeFunction: String? = nil,
doc: String? = nil) {
self.name = name; self.cType = cType
self.getTypeFunction = getTypeFunction
self.copyFunction = copyFunction; self.freeFunction = freeFunction
self.doc = doc
}
}
/// The plan for a GObject interface wrapper.
public struct InterfacePlan: Equatable, Sendable {
public let name: String
public let cType: String
public let prereqs: [String]
public let getTypeFunction: String?
/// The interface's instance methods, rendered as protocol requirements.
/// The implementing class supplies each method body (the C symbol lives on
/// the class, not the interface see `planInterface`).
public let methods: [CallablePlan]
public let doc: String?
public init(name: String, cType: String, prereqs: [String] = [],
getTypeFunction: String? = nil, methods: [CallablePlan] = [],
doc: String? = nil) {
self.name = name; self.cType = cType; self.prereqs = prereqs
self.getTypeFunction = getTypeFunction; self.methods = methods; self.doc = doc
}
}
/// The complete plan for one Swift module: every type planned, every symbol
/// skipped, and the coverage statistics computed from them.
public struct ModulePlan: Sendable {
/// The Swift module name, e.g. `"GLib"`.
public let module: String
/// All successfully planned types.
public let types: [TypePlan]
/// Every skipped symbol with its reason.
public let skips: [SkipEntry]
/// Coverage statistics derived from planned and skipped counts.
public let coverage: CoverageStats
public init(module: String, types: [TypePlan], skips: [SkipEntry],
coverage: CoverageStats) {
self.module = module; self.types = types; self.skips = skips
self.coverage = coverage
}
/// The skip report serialised from this module's skips and coverage.
public var skipReport: SkipReport {
SkipReport(module: module, entries: skips, stats: coverage)
}
}
// MARK: - Callable Plans
/// The complete binding plan for one callable (function, method, or constructor).
///
/// Every parameter has been fully mapped by the TypeMapper; the renderer only
/// combines the `Mapping` values into a Swift function signature and a C call.
public struct CallablePlan: Equatable, Sendable {
/// The Swift name of the function/method/constructor (keyword-escaped).
public let name: String
/// The C function identifier, e.g. `"gtk_widget_set_visible"`.
public let cIdentifier: String
/// The parameters in Swift declaration order.
public let parameters: [ParameterPlan]
/// The return mapping, or `nil` for a void function.
public let returnMapping: Mapping?
/// `true` when this callable is a class/static function (not an instance
/// method). Constructor callables are implicitly static.
public let isStatic: Bool
/// `true` when this callable is a constructor.
public let isConstructor: Bool
/// The ownership init variant for constructor callables: `nil` for
/// non-constructors; `.takingOwnership`, `.retaining`, or
/// `.sinkingRef` for constructors.
public let ownershipInit: OwnershipInit?
/// `true` when this callable takes a trailing `GError**` and can throw.
public let throwsError: Bool
/// Documentation from the GIR `<doc>` element.
public let doc: String?
public init(name: String, cIdentifier: String, parameters: [ParameterPlan],
returnMapping: Mapping? = nil, isStatic: Bool = false,
isConstructor: Bool = false, ownershipInit: OwnershipInit? = nil,
throwsError: Bool = false, doc: String? = nil) {
self.name = name; self.cIdentifier = cIdentifier; self.parameters = parameters
self.returnMapping = returnMapping; self.isStatic = isStatic
self.isConstructor = isConstructor; self.ownershipInit = ownershipInit
self.throwsError = throwsError; self.doc = doc
}
}
/// How a constructor takes ownership of the new GObject instance.
public enum OwnershipInit: String, Equatable, Sendable {
/// `transfer-ownership="full"`: store the pointer, no additional ref.
case takingOwnership
/// `transfer-ownership="none"`, non-floating: call `g_object_ref`.
case retaining
/// The class descends from `InitiallyUnowned`: call `g_object_ref_sink`.
case sinkingRef
}
/// One parameter of a `CallablePlan`: its Swift name, C argument position,
/// and the complete type mapping.
public struct ParameterPlan: Equatable, Sendable {
/// The Swift parameter name (keyword-escaped).
public let swiftName: String
/// The zero-based index in the C function's argument list (all parameters,
/// including instance parameter).
public let cArgIndex: Int
/// The complete type mapping for this parameter.
public let mapping: Mapping
/// `true` when this parameter is the implicit instance (`self`).
public let isInstanceParameter: Bool
/// `true` if this parameter has `direction="out"` and will be returned from
/// the Swift function rather than passed as an argument.
public let isOutParameter: Bool
public init(swiftName: String, cArgIndex: Int, mapping: Mapping,
isInstanceParameter: Bool = false, isOutParameter: Bool = false) {
self.swiftName = swiftName; self.cArgIndex = cArgIndex
self.mapping = mapping; self.isInstanceParameter = isInstanceParameter
self.isOutParameter = isOutParameter
}
}