1
0
Fork 0

Tier 3 compile-gate compliance: Pango, GdkPixbuf, Graphene

Root causes fixed, all in TypeMapper.swift / TypeRegistry.swift /
Planner.swift / PlanRenderer.swift / BindingPlan.swift / IRModel.swift /
XMLParser.swift:

- Global (cross-module) inherited-member post-pass in planModules
  replaces the old in-module-only version, dropping properties/methods
  redeclared by an ancestor in a DIFFERENT module (e.g.
  Pango.Coverage.ref()/unref() vs. GObject.Object)
- transfer-ownership="none" GObject-class returns now route through
  init(retaining:) instead of adopting a borrowed pointer as owned
- Non-GObject fundamental root classes (GParamSpec) capture their GIR
  glib:ref-func/glib:unref-func instead of hardcoding g_object_ref
- Interface conformance clauses annotate each interface with @MainActor
  to satisfy isolated-conformance checking across module boundaries
- Scalar marshalOut (.direct/.numericCast) returned through a pointer
  C type with no <array> length now skips instead of misreading a raw
  buffer pointer as its pointee value
- Property/method Swift-name collisions on the same class are resolved
  by dropping the property (e.g. Pango.FontFamily.isVariable)
- MultiPackageAnalyzer only walks includes for configured packages
- No unconditional Foundation import (collided with Gio.InputStream)
- gdk_pixbuf_non_anim_new added to knownMissingCFunctions (GIR-declared,
  not exported through the public umbrella header)

Smoke tests: smoke/tier3/PixbufSmoke.swift (Pixbuf construction +
dimension round-trip against real libgdk_pixbuf) and
smoke/tier3/PangoSmoke.swift (findBaseDir against real libpango,
Latin vs. Hebrew script direction).

208 unit tests pass (+14: InheritedMemberTests + E2 sections in
PropertyGenerationTests/RendererCallableTests/others). compile-gate.sh
1, 2, and 3 all PASS. smoke-test.sh 3 passes 21/21 (18 tier-1 base +
1 GdkPixbuf + 2 Pango).
This commit is contained in:
Brendan Szymanski 2026-07-20 01:06:38 -04:00
parent 29872707f2
commit ea41629a1f
18 changed files with 518 additions and 80 deletions

View file

@ -503,6 +503,12 @@ public struct ClassPlan: Equatable, Sendable {
/// Whether this class descends from `InitiallyUnowned`, requiring
/// `g_object_ref_sink` on construction.
public let descendsFromInitiallyUnowned: Bool
/// The C function that takes a reference on this class's underlying
/// pointer (`init(retaining:)`). `"g_object_ref"` for ordinary
/// `GObject`-derived classes; overridden for classes that root their own
/// non-`GObject` fundamental type hierarchy (e.g. `GParamSpec`'s
/// `g_param_spec_ref_sink` see `TypeRegistry.refUnrefFunctions`).
public let refFunc: String
/// Planned constructors (empty for abstract classes).
public let constructors: [CallablePlan]
/// Planned instance methods.
@ -522,6 +528,7 @@ public struct ClassPlan: Equatable, Sendable {
isOpen: Bool = false, isAbstract: Bool = false,
getTypeFunction: String? = nil,
descendsFromInitiallyUnowned: Bool = false,
refFunc: String = "g_object_ref",
interfaces: [String] = [],
constructors: [CallablePlan] = [], methods: [CallablePlan] = [],
functions: [CallablePlan] = [], properties: [PropertyPlan] = [],
@ -531,6 +538,7 @@ public struct ClassPlan: Equatable, Sendable {
self.isOpen = isOpen; self.isAbstract = isAbstract
self.getTypeFunction = getTypeFunction
self.descendsFromInitiallyUnowned = descendsFromInitiallyUnowned
self.refFunc = refFunc
self.interfaces = interfaces
self.constructors = constructors; self.methods = methods
self.functions = functions; self.properties = properties

View file

@ -129,6 +129,14 @@ public struct Class {
public var signals: [Signal]
/// The functions associated with this class.
public var functions: [GlobalFunction]
/// The GIR `glib:ref-func` attribute, present only on the root class of a
/// non-`GObject` fundamental type hierarchy (e.g. `GParamSpec`'s
/// `g_param_spec_ref_sink`). `nil` for ordinary `GObject`-derived classes,
/// which use `g_object_ref`/`g_object_ref_sink` instead.
public var refFunc: String?
/// The GIR `glib:unref-func` attribute see `refFunc`. `nil` for ordinary
/// `GObject`-derived classes, which use `g_object_unref` instead.
public var unrefFunc: String?
/// Creates a new class definition.
/// - Parameters:
/// - name: The class name, e.g. `"Widget"`.
@ -146,12 +154,14 @@ public struct Class {
/// - signals: The signals. Defaults to empty.
/// - functions: The associated functions. Defaults to empty.
/// - doc: Documentation comment from the GIR XML.
/// - refFunc: The GIR `glib:ref-func` override, if any.
/// - unrefFunc: The GIR `glib:unref-func` override, if any.
public init(name: String, cType: String, parent: String?, isAbstract: Bool = false,
isFinal: Bool = false, getTypeFunction: String? = nil, typeName: String? = nil,
symbolInfo: SymbolInfo = SymbolInfo(),
implements: [String] = [], constructors: [Constructor] = [], methods: [Method] = [],
properties: [Property] = [], signals: [Signal] = [], functions: [GlobalFunction] = [],
doc: String? = nil) {
doc: String? = nil, refFunc: String? = nil, unrefFunc: String? = nil) {
self.name = name; self.cType = cType; self.parent = parent
self.isAbstract = isAbstract; self.isFinal = isFinal
self.getTypeFunction = getTypeFunction; self.typeName = typeName
@ -159,6 +169,7 @@ public struct Class {
self.constructors = constructors; self.methods = methods
self.properties = properties; self.signals = signals; self.functions = functions
self.doc = doc
self.refFunc = refFunc; self.unrefFunc = unrefFunc
}
}
@ -917,6 +928,10 @@ public struct ReturnValue: Equatable, Sendable {
public var transferOwnership: TransferOwnership
/// Documentation comment from the GIR XML `<doc>` element.
public var doc: String?
/// The raw GIR `c:type` attribute, e.g. `"const guint8*"`. Empty when
/// the GIR omitted it. Used to detect scalar types returned through a
/// pointer (no `<array>` length) that the type name alone hides.
public var cType: String
/// Creates a return value description.
///
@ -925,12 +940,14 @@ public struct ReturnValue: Equatable, Sendable {
/// - isNullable: Whether `NULL` may be returned. Defaults to `false`.
/// - transferOwnership: Ownership transfer to the caller. Defaults to `.none`.
/// - doc: Documentation comment from the GIR XML.
/// - cType: The raw GIR `c:type` attribute. Defaults to `""`.
public init(type: GIRType = .void, isNullable: Bool = false,
transferOwnership: TransferOwnership = .none, doc: String? = nil) {
transferOwnership: TransferOwnership = .none, doc: String? = nil, cType: String = "") {
self.type = type
self.isNullable = isNullable
self.transferOwnership = transferOwnership
self.doc = doc
self.cType = cType
}
}

View file

@ -84,12 +84,14 @@ public struct MultiPackageAnalyzer {
let parser = GIRParser()
let configuredModules = Set(config.packages.map(\.name))
for entry in config.packages {
let repo = try parser.parse(fileURL: URL(fileURLWithPath: entry.girPath))
repositories[entry.name] = repo
var deps = Set<String>()
for include in repo.includedPackages {
for include in repo.includedPackages where configuredModules.contains(include.swiftModule) {
deps.insert(include.swiftModule)
}
directDeps[entry.name] = deps

View file

@ -38,7 +38,6 @@ public func renderModule(_ plan: ModulePlan) -> [String: String] {
// Generated by SwiftGtkGen. DO NOT EDIT.
import C\(plan.module)
import Foundation
\(depImports)
"""
@ -283,7 +282,6 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [],
// Generated by SwiftGtkGen. DO NOT EDIT.
import C\(moduleName)
import Foundation
\(depImports)
/// Reinterprets a wrapper's raw instance pointer as an `OpaquePointer`,
/// selected when the C function's parameter is an opaque struct pointer.
@ -599,11 +597,11 @@ private func renderClass(_ plan: ClassPlan) -> String {
let access = plan.isOpen ? "open" : "public"
let parentDecl: String
if let parent = plan.parent {
let ifaces = plan.interfaces.isEmpty ? "" : ", \(plan.interfaces.joined(separator: ", "))"
let ifaces = plan.interfaces.isEmpty ? "" : ", \(plan.interfaces.map { "@MainActor \($0)" }.joined(separator: ", "))"
parentDecl = ": \(parent)\(ifaces)"
} else if !plan.interfaces.isEmpty {
parentDecl = ": \(plan.interfaces.joined(separator: ", "))"
parentDecl = ": \(plan.interfaces.map { "@MainActor \($0)" }.joined(separator: ", "))"
} else {
parentDecl = ""
}
@ -642,15 +640,20 @@ private func renderClass(_ plan: ClassPlan) -> String {
}
lines.append("")
// Retaining init
// Retaining init. `g_object_ref`/`g_object_ref_sink` take a plain
// `gpointer` and accept `pointer` as-is; a class rooting its own
// fundamental hierarchy (e.g. `GParamSpec`) declares a ref function
// that takes its typed C struct pointer, requiring `_instancePointer`.
let refArg = plan.refFunc == "g_object_ref" ? "pointer" : "_instancePointer(pointer)"
if isRoot {
lines.append(" public init(retaining pointer: UnsafeMutableRawPointer) {")
lines.append(" self.pointer = pointer")
lines.append(" g_object_ref(pointer)")
lines.append(" \(plan.refFunc)(\(refArg))")
lines.append(" }")
} else {
lines.append(" public override init(retaining pointer: UnsafeMutableRawPointer) {")
lines.append(" g_object_ref(pointer)")
lines.append(" \(plan.refFunc)(\(refArg))")
lines.append(" super.init(takingOwnership: pointer)")
lines.append(" }")
}
@ -1419,12 +1422,19 @@ private func marshalReturn(_ cCall: String, mapping: Mapping) -> String {
return "String(cString: \(cCall)) /* TODO: g_free */"
}
return "String(cString: \(cCall))"
case .objectWrap, .objectRetain:
case .objectWrap:
let isOptional = mapping.swiftType.hasSuffix("?")
let baseType = isOptional ? String(mapping.swiftType.dropLast()) : mapping.swiftType
let wrapExpr = "\(baseType)(takingOwnership: _rawPointer(\(cCall)))"
if isOptional { return "\(cCall).map { \(baseType)(takingOwnership: _rawPointer($0)) }" }
return wrapExpr
return "\(baseType)(takingOwnership: _rawPointer(\(cCall)))"
case .objectRetain:
// transfer-ownership="none": the C call keeps its own reference, so
// the wrapper must take a new one instead of adopting the borrowed
// pointer outright (see TypeMapper.swift's `.object` case).
let isOptional = mapping.swiftType.hasSuffix("?")
let baseType = isOptional ? String(mapping.swiftType.dropLast()) : mapping.swiftType
if isOptional { return "\(cCall).map { \(baseType)(retaining: _rawPointer($0)) }" }
return "\(baseType)(retaining: _rawPointer(\(cCall)))"
case .interfaceWrap(let adopt):
let isOptional = mapping.swiftType.hasSuffix("?")
let baseType = isOptional ? String(mapping.swiftType.dropLast()) : mapping.swiftType

View file

@ -51,6 +51,83 @@ public func planModules(
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 classesByName: [String: ClassPlan] = [:]
for (_, plan) in modulePlans {
for case .class(let p) in plan.types { classesByName[p.name] = p }
}
func methodSignature(_ m: CallablePlan) -> String {
"\(m.name)/\(m.parameters.filter { !$0.isInstanceParameter }.count)"
}
func ancestorNames(of plan: ClassPlan) -> (props: Set<String>, methods: Set<String>) {
var propNames: Set<String> = []
var methodSigs: Set<String> = []
var current = plan.parent
var seen: Set<String> = [plan.name]
while let parentName = current, !seen.contains(parentName), let parentPlan = classesByName[parentName] {
seen.insert(parentName)
propNames.formUnion(parentPlan.properties.map(\.swiftName))
methodSigs.formUnion(parentPlan.methods.map(methodSignature))
current = parentPlan.parent
}
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 filteredProps = plan.properties.filter { prop in
guard inherited.props.contains(prop.swiftName) else { return true }
skips.append(SkipEntry(symbol: "\(moduleName).\(plan.name).\(prop.swiftName)",
cIdentifier: prop.girName, reason: .inheritedMember,
detail: "property '\(prop.swiftName)' already declared by ancestor"))
return false
}
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
}
guard filteredProps.count != plan.properties.count || filteredMethods.count != plan.methods.count else {
return typePlan
}
changed = true
let newPlan = ClassPlan(
name: plan.name, cType: plan.cType, parent: plan.parent,
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
}
@ -213,64 +290,6 @@ private func planNamespace(_ ns: Namespace, context: MapContext) -> ModulePlan {
boundCallables -= lostCallables
}
// Post-pass: drop class properties/methods already declared, with the
// identical Swift name, by an in-module ancestor GObject inheritance
// provides them for free, and Swift errors on the redundant redeclaration
// (e.g. `SimpleIOStream`'s construct-only `inputStream`/`outputStream`
// properties, already declared by its ancestor `IOStream`).
do {
var classesByName: [String: ClassPlan] = [:]
for case .class(let p) in types { classesByName[p.name] = p }
func ancestorNames(of plan: ClassPlan) -> Set<String> {
var propNames: Set<String> = []
var methodNames: Set<String> = []
var current = plan.parent
var seen: Set<String> = [plan.name]
while let parentName = current, !seen.contains(parentName), let parentPlan = classesByName[parentName] {
seen.insert(parentName)
propNames.formUnion(parentPlan.properties.map(\.swiftName))
methodNames.formUnion(parentPlan.methods.map(\.name))
current = parentPlan.parent
}
return propNames.union(methodNames)
}
types = types.map { typePlan in
guard case .class(let plan) = typePlan else { return typePlan }
let inherited = ancestorNames(of: plan)
guard !inherited.isEmpty else { return typePlan }
let filteredProps = plan.properties.filter { prop in
guard inherited.contains(prop.swiftName) else { return true }
skips.append(SkipEntry(symbol: "\(ns.name).\(plan.name).\(prop.swiftName)",
cIdentifier: prop.girName, reason: .inheritedMember,
detail: "property '\(prop.swiftName)' already declared by ancestor"))
return false
}
let filteredMethods = plan.methods.filter { method in
guard inherited.contains(method.name) else { return true }
skips.append(SkipEntry(symbol: "\(ns.name).\(plan.name).\(method.name)",
cIdentifier: method.cIdentifier, reason: .inheritedMember,
detail: "method '\(method.name)' already declared by ancestor"))
return false
}
guard filteredProps.count != plan.properties.count || filteredMethods.count != plan.methods.count else {
return typePlan
}
let newPlan = ClassPlan(
name: plan.name, cType: plan.cType, parent: plan.parent,
isOpen: plan.isOpen, isAbstract: plan.isAbstract,
getTypeFunction: plan.getTypeFunction,
descendsFromInitiallyUnowned: plan.descendsFromInitiallyUnowned,
interfaces: plan.interfaces,
constructors: plan.constructors, methods: filteredMethods,
functions: plan.functions, properties: filteredProps,
signals: plan.signals, doc: plan.doc
)
return .class(newPlan)
}
}
let coverage = CoverageStats(
boundCallables: boundCallables, totalCallables: totalCallables,
boundTypes: boundTypes, totalTypes: totalTypes,
@ -547,6 +566,9 @@ private let knownMissingCFunctions: Set<String> = [
"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",
]
let knownMisleadingCFunctions: Set<String> = [
@ -872,13 +894,14 @@ func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberS
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 }
if case .object(_, _, _, let ifaces, _, _) = ancestor.category { return ifaces }
return []
})
let interfaceNames: [String] = klass.implements.compactMap { ifaceName in
@ -932,6 +955,21 @@ func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberS
}
}
// 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 {
@ -950,6 +988,7 @@ func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberS
isAbstract: klass.isAbstract,
getTypeFunction: klass.getTypeFunction,
descendsFromInitiallyUnowned: descendsIU,
refFunc: refFunc,
interfaces: interfaceNames,
constructors: constructorPlans,
methods: methodPlans,
@ -1119,6 +1158,21 @@ private func planCallable(
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,
@ -1272,6 +1326,11 @@ private func planSignal(_ signal: Signal, onClass className: String,
/// - 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 {

View file

@ -241,8 +241,16 @@ private func mapTypeRef(
case .object:
let swiftType = context.registry.swiftTypeName(for: resolved, in: context.currentModule)
// `transfer-ownership="none"` means the C call does not hand us a new
// reference (e.g. GBinding.get_source()/get_target()): the wrapper
// must take its own ref via `init(retaining:)`, or its `deinit` will
// unref a reference it never owned (over-release -> use-after-free).
// `"full"` (and the degenerate `"container"` case, which doesn't
// apply to a bare object pointer) hands us an owned reference already,
// so the wrapper just adopts it via `init(takingOwnership:)`.
return Mapping(swiftType: swiftType, cSwiftType: "UnsafeMutableRawPointer?",
marshalIn: .objectPointer, marshalOut: .objectWrap(sink: false),
marshalIn: .objectPointer,
marshalOut: transfer == .full ? .objectWrap(sink: false) : .objectRetain,
gvalue: GValueOps(typeMacro: "G_TYPE_OBJECT",
getterSuffix: "object", setterSuffix: "object"),
category: .needsClass)

View file

@ -21,7 +21,11 @@ public enum TypeCategory: Equatable, Sendable {
/// - isAbstract: Whether the class cannot be instantiated directly.
/// - isFinal: Whether GIR forbids subclassing.
/// - interfaces: Fully qualified names of implemented interfaces.
case object(parentGIRName: String?, isAbstract: Bool, isFinal: Bool, interfaces: [String])
/// - refFunc: GIR `glib:ref-func` override, present only on the root of
/// a non-`GObject` fundamental hierarchy (e.g. `GParamSpec`).
/// - unrefFunc: GIR `glib:unref-func` override see `refFunc`.
case object(parentGIRName: String?, isAbstract: Bool, isFinal: Bool, interfaces: [String],
refFunc: String? = nil, unrefFunc: String? = nil)
/// A GObject interface.
/// - Parameter prereqs: Fully qualified prerequisite type names.
case interface(prereqs: [String])
@ -196,7 +200,9 @@ public struct TypeRegistry: Sendable {
parentGIRName: cls.parent.map(qualify),
isAbstract: cls.isAbstract,
isFinal: cls.isFinal,
interfaces: cls.implements.map(qualify)
interfaces: cls.implements.map(qualify),
refFunc: cls.refFunc,
unrefFunc: cls.unrefFunc
)
)
}
@ -327,7 +333,7 @@ public struct TypeRegistry: Sendable {
var seen: Set<String> = [girName]
var current = girName
while let resolved = resolve(girName: current),
case .object(let parent, _, _, _) = resolved.category,
case .object(let parent, _, _, _, _, _) = resolved.category,
let parentName = parent,
!seen.contains(parentName),
let parentType = resolve(girName: parentName)
@ -362,6 +368,31 @@ public struct TypeRegistry: Sendable {
return ancestry(of: girName).contains { $0.girName == Self.objectGIRName }
}
/// Resolves the C ref/unref functions that manage a class's lifetime.
///
/// Ordinary `GObject`-derived classes use `g_object_ref`/`g_object_unref`
/// (with `g_object_ref_sink` substituted separately for
/// `InitiallyUnowned` construction see `descendsFromInitiallyUnowned`).
/// Some classes are the root of their own *non*-`GObject` fundamental
/// type hierarchy e.g. `GParamSpec`, whose GIR declares
/// `glib:ref-func="g_param_spec_ref_sink"` /
/// `glib:unref-func="g_param_spec_unref"` on the root class only.
/// Calling `g_object_ref` on such a pointer trips
/// `G_IS_OBJECT`/`G_IS_OBJECT_CLASS` assertions at runtime, since it was
/// never actually a `GObject`.
///
/// - Parameter girName: The fully qualified class name.
/// - Returns: The ref/unref C function names to use for this class.
public func refUnrefFunctions(for girName: String) -> (ref: String, unref: String) {
for type in [resolve(girName: girName)].compactMap({ $0 }) + ancestry(of: girName) {
if case .object(_, _, _, _, let refFunc, let unrefFunc) = type.category,
let refFunc, let unrefFunc {
return (refFunc, unrefFunc)
}
}
return ("g_object_ref", "g_object_unref")
}
/// The Swift spelling of a resolved type as written from `module`.
///
/// Types from another module are qualified (`GObject.Object`); types from
@ -386,7 +417,7 @@ public struct TypeRegistry: Sendable {
public func subclassedTypes() -> Set<String> {
var result: Set<String> = []
for type in types.values {
if case .object(let parent, _, _, _) = type.category, let parent {
if case .object(let parent, _, _, _, _, _) = type.category, let parent {
result.insert(parent)
}
}

View file

@ -199,7 +199,9 @@ final class GIRXMLDelegate: NSObject, XMLParserDelegate {
isFinal: attributeDict["final"] == "1",
getTypeFunction: attributeDict["glib:get-type"],
typeName: attributeDict["glib:type-name"],
symbolInfo: Self.symbolInfo(from: attributeDict)
symbolInfo: Self.symbolInfo(from: attributeDict),
refFunc: attributeDict["glib:ref-func"],
unrefFunc: attributeDict["glib:unref-func"]
)))
case "interface":
@ -587,6 +589,7 @@ final class GIRXMLDelegate: NSObject, XMLParserDelegate {
$0 = .parameter(p)
case .returnValue(var rv):
rv.type = type
if !cType.isEmpty { rv.cType = cType }
$0 = .returnValue(rv)
case .property(var prop):
prop.type = type

View file

@ -134,6 +134,21 @@ struct FunctionGenerationTests {
}
}
@Test("A scalar return type whose c:type is a pointer with no array length is skipped")
func pointerToScalarReturnSkip() throws {
// Mirrors gdk_pixbuf_read_pixels: GIR names the return `guint8` but
// c:type is `const guint8*` a raw buffer pointer, not a scalar.
let fn = GlobalFunction(
name: "read_pixels", cIdentifier: "gdk_pixbuf_read_pixels",
parameters: [],
returnValue: ReturnValue(type: .uint8, cType: "const guint8*")
)
guard case .skip(let entry) = planFunction(fn, context: makeContext()) else {
Issue.record("expected pointer-to-scalar return to be skipped"); return
}
#expect(entry.reason == .unknownType)
}
@Test("Unexported C symbols never reach a plan")
func missingSymbolSkip() throws {
// g_open is a `#define g_open open` macro on non-Windows: not a real

View file

@ -0,0 +1,133 @@
// InheritedMemberTests.swift
// Covers Phase E2's global (cross-module) inherited-member post-pass in
// `planModules`: a class in one Swift module that redeclares a same-name,
// same-arity method already provided by an ancestor class in a DIFFERENT
// module (e.g. `Pango.Coverage.ref()`/`unref()` vs. `GObject.Object.ref()`/
// `unref()`) must have its own copy dropped Swift rejects the redundant
// redeclaration across module boundaries. A same-name method with a
// DIFFERENT arity (legitimate overload, e.g. `MemoryOutputStream.getData()`
// vs. inherited `Object.getData(key:)`) must NOT be dropped.
import Foundation
import Testing
@testable import SwiftGtkGenCore
@Suite("Cross-module inherited-member dedup")
struct InheritedMemberTests {
/// Builds a two-module `MultiPackageAnalysis` + `TypeRegistry`: module A
/// declares root class `Object` with `ref()` (no args) and `getData(key:)`
/// (one arg); module B declares `Coverage: Object` (redeclaring `ref()`
/// with the SAME arity) and `Stream: Object` (redeclaring `getData()`
/// with a DIFFERENT arity a legitimate overload).
private func planTwoModules() throws -> [String: ModulePlan] {
let tmpDir = NSTemporaryDirectory() + "inherited_member_test_\(UUID().uuidString)"
try FileManager.default.createDirectory(atPath: tmpDir, withIntermediateDirectories: true)
let moduleAGIR = """
<?xml version="1.0"?>
<repository version="1.2"
xmlns="http://www.gtk.org/introspection/core/1.0"
xmlns:c="http://www.gtk.org/introspection/c/1.0"
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
<c:include name="gobject.h"/>
<package name="gobject-2.0"/>
<namespace name="AMod" version="1.0"
shared-library="libamod.so.0" c:identifier-prefixes="A">
<class name="Object" c:type="AObject" parent="" glib:type-name="AObject"
glib:get-type="a_object_get_type">
<method name="ref" c:identifier="a_object_ref">
<return-value transfer-ownership="none"><type name="none" c:type="void"/></return-value>
</method>
<method name="get_data" c:identifier="a_object_get_data">
<return-value transfer-ownership="none"><type name="gpointer" c:type="gpointer"/></return-value>
<parameters>
<instance-parameter name="self" transfer-ownership="none">
<type name="Object" c:type="AObject*"/>
</instance-parameter>
<parameter name="key" transfer-ownership="none">
<type name="utf8" c:type="const char*"/>
</parameter>
</parameters>
</method>
</class>
</namespace>
</repository>
"""
let moduleAPath = tmpDir + "/AMod-1.0.gir"
try moduleAGIR.write(toFile: moduleAPath, atomically: true, encoding: .utf8)
let moduleBGIR = """
<?xml version="1.0"?>
<repository version="1.2"
xmlns="http://www.gtk.org/introspection/core/1.0"
xmlns:c="http://www.gtk.org/introspection/c/1.0"
xmlns:glib="http://www.gtk.org/introspection/glib/1.0">
<include name="AMod" version="1.0"/>
<c:include name="bmod.h"/>
<package name="bmod-1.0"/>
<namespace name="BMod" version="1.0"
shared-library="libbmod.so.0" c:identifier-prefixes="B">
<class name="Coverage" c:type="BCoverage" parent="AMod.Object"
glib:type-name="BCoverage" glib:get-type="b_coverage_get_type">
<method name="ref" c:identifier="b_coverage_ref">
<return-value transfer-ownership="none"><type name="none" c:type="void"/></return-value>
</method>
</class>
<class name="Stream" c:type="BStream" parent="AMod.Object"
glib:type-name="BStream" glib:get-type="b_stream_get_type">
<method name="get_data" c:identifier="b_stream_get_data">
<return-value transfer-ownership="none"><type name="gpointer" c:type="gpointer"/></return-value>
<parameters>
<instance-parameter name="self" transfer-ownership="none">
<type name="Stream" c:type="BStream*"/>
</instance-parameter>
</parameters>
</method>
</class>
</namespace>
</repository>
"""
let moduleBPath = tmpDir + "/BMod-1.0.gir"
try moduleBGIR.write(toFile: moduleBPath, atomically: true, encoding: .utf8)
let packages: [PackageEntry] = [
PackageEntry(name: "AMod", girPath: moduleAPath),
PackageEntry(name: "BMod", girPath: moduleBPath),
]
let config = MonorepoConfig(outputDir: tmpDir, packages: packages)
let analysis = try MultiPackageAnalyzer(config: config).analyze()
let registry = TypeRegistry(repositories: analysis.repositories)
return planModules(analysis: analysis, registry: registry)
}
@Test("A same-arity method redeclared across a cross-module inheritance edge is dropped")
func crossModuleSameArityCollisionIsDropped() throws {
let plans = try planTwoModules()
guard let bPlan = plans["BMod"],
case .class(let coverage)? = bPlan.types.first(where: {
if case .class(let c) = $0 { return c.name == "Coverage" }
return false
}) else {
Issue.record("expected BMod.Coverage to plan"); return
}
#expect(!coverage.methods.contains { $0.name == "ref" })
#expect(bPlan.skips.contains { $0.reason == .inheritedMember && $0.symbol.contains("Coverage.ref") })
}
@Test("A different-arity method with the same base name across modules is kept (legitimate overload)")
func crossModuleDifferentArityOverloadIsKept() throws {
let plans = try planTwoModules()
guard let bPlan = plans["BMod"],
case .class(let stream)? = bPlan.types.first(where: {
if case .class(let c) = $0 { return c.name == "Stream" }
return false
}) else {
Issue.record("expected BMod.Stream to plan"); return
}
// Stream.getData() (0 args) must survive despite Object.getData(key:)
// (1 arg) sharing the base name different arity, not a collision.
#expect(stream.methods.contains { $0.name == "getData" })
#expect(!bPlan.skips.contains { $0.reason == .inheritedMember && $0.symbol.contains("Stream.getData") })
}
}

View file

@ -76,7 +76,7 @@ struct InterfaceConformanceTests {
let classSrc = files["TypeModule.swift"] ?? ""
let ifaceSrc = files["TypePlugin.swift"] ?? ""
#expect(classSrc.contains("class TypeModule: Object, TypePlugin {"))
#expect(classSrc.contains("class TypeModule: Object, @MainActor TypePlugin {"))
// Protocol body is bare only the `pointer` requirement.
#expect(ifaceSrc.contains("public protocol TypePlugin {"))

View file

@ -88,4 +88,27 @@ struct MultiPackageAnalysisTests {
let gobjExtLibs = analysis.packageConfigs["GObject"]?.externalLibraries
#expect(gobjExtLibs?.contains("GLib") == true)
}
@Test func excludesForeignIncludesNotInConfig() throws {
// GObject's GIR <include>s both GLib (configured) and a foreign
// namespace ("HarfBuzz") that has no package entry mirroring
// Pango's real-world <include name="HarfBuzz"/>. The foreign
// include must not surface as a direct/transitive dependency,
// an external library, or a re-export target.
let config = try buildConfig()
var gobjSrc = try String(contentsOfFile: config.packages[1].girPath, encoding: .utf8)
gobjSrc = gobjSrc.replacingOccurrences(
of: "<include name=\"GLib\" version=\"2.0\"/>",
with: "<include name=\"GLib\" version=\"2.0\"/>\n <include name=\"HarfBuzz\" version=\"0.0\"/>"
)
try gobjSrc.write(toFile: config.packages[1].girPath, atomically: true, encoding: .utf8)
let analyzer = MultiPackageAnalyzer(config: config)
let analysis = try analyzer.analyze()
#expect(analysis.directDependencies["GObject"]?.contains("GLib") == true)
#expect(analysis.directDependencies["GObject"]?.contains("HarfBuzz") == false)
#expect(analysis.transitiveDependencies["GObject"]?.contains("HarfBuzz") == false)
#expect(analysis.packageConfigs["GObject"]?.externalLibraries.contains("HarfBuzz") == false)
}
}

View file

@ -146,6 +146,24 @@ struct ParserSemanticsTests {
#expect(method.returnValue.isNullable)
}
@Test("Return value c:type is captured (pointer-to-scalar detection)")
func returnValueCTypeIsCaptured() throws {
let repo = try GIRParser().parse(
xmlString: girDocument(
"""
<record name="Pixbuf" c:type="GdkPixbuf">
<method name="read_pixels" c:identifier="gdk_pixbuf_read_pixels">
<return-value transfer-ownership="none">
<type name="guint8" c:type="const guint8*"/>
</return-value>
</method>
</record>
"""))
let method = try #require(repo.namespaces.first?.records.first?.methods.first)
#expect(method.returnValue.type == .uint8)
#expect(method.returnValue.cType == "const guint8*")
}
@Test("Non-introspectable and shadowed symbols are marked unbindable")
func symbolInfoIsCaptured() throws {
let repo = try GIRParser().parse(

View file

@ -402,4 +402,22 @@ struct PropertyGenerationTests {
#expect(plan.properties[0].swiftType == "String?")
#expect(source.contains("g_value_get_string(&gvalue).map { String(cString: $0) }"))
}
// MARK: - E2: property/method name collision (Pango.FontFamily.isVariable)
@Test("A property whose Swift name collides with a planned method is dropped, keeping the method")
func propertyCollidingWithMethodIsSkipped() throws {
// GIR `is-variable` property and `is_variable` method both yield the
// Swift name `isVariable` `var isVariable: Bool` and
// `func isVariable() -> Bool` are an invalid redeclaration.
let prop = Property(name: "is-variable", type: .boolean, isReadable: true, isWritable: false)
let method = getterMethod("is_variable", returns: .boolean)
let (source, plan, skips) = renderClass(named: "FontFamily", properties: [prop], methods: [method])
#expect(plan.properties.isEmpty)
#expect(plan.methods.contains { $0.name == "isVariable" })
#expect(skips.contains { $0.reason == .nameCollision })
#expect(!source.contains("public var isVariable"))
#expect(source.contains("func isVariable("))
}
}

View file

@ -295,4 +295,15 @@ struct RendererCallableTests {
#expect(body.contains("var out0: UnsafeMutableRawPointer? = nil"))
#expect(!body.contains("var out0: UnsafeMutableRawPointer? = 0"))
}
// MARK: - E2: no unconditional Foundation import (collides with Gio.InputStream)
@Test("Generated module and support headers do not import Foundation")
func generatedHeadersOmitFoundationImport() throws {
let module = ModulePlan(module: "GLib", types: [], skips: [], coverage: CoverageStats())
let files = renderModule(module)
for (name, content) in files {
#expect(!content.contains("import Foundation"), "\(name) unexpectedly imports Foundation")
}
}
}

View file

@ -277,7 +277,7 @@ struct TypeMapperTests {
// MARK: - Type references
@Test("Object typeRef maps to pointer wrapper")
@Test("Object typeRef with transfer=none maps to objectRetain (must take its own ref)")
func objectTypeRef() throws {
let ctx = makeContext()
let mapping = try map(.typeRef("Object", namespace: "GObject"),
@ -285,6 +285,14 @@ struct TypeMapperTests {
#expect(mapping.swiftType == "Object")
#expect(mapping.cSwiftType == "UnsafeMutableRawPointer?")
#expect(mapping.marshalIn == .objectPointer)
#expect(mapping.marshalOut == .objectRetain)
}
@Test("Object typeRef with transfer=full maps to objectWrap (adopts the owned ref)")
func objectTypeRefFullTransfer() throws {
let ctx = makeContext()
let mapping = try map(.typeRef("Object", namespace: "GObject"),
nullable: false, transfer: .full, context: ctx)
#expect(mapping.marshalOut == .objectWrap(sink: false))
}

View file

@ -0,0 +1,38 @@
// PangoSmoke.swift
// Tier-3-only runtime smoke test for Pango (Phase E2). Proves against
// the REAL libpango, linked at runtime that:
// 1. `findBaseDir(text:length:)` reaches the real `pango_find_base_dir`
// C call, correctly marshalling a Swift `String` through `withCString`
// and marshalling the returned `PangoDirection` C enum back into the
// Swift `Direction` enum via `numericCast`.
// 2. The result reflects the REAL Unicode bidirectional algorithm run by
// libpango not stubbed/placeholder data by exercising both a
// strong-LTR script (Latin) and a strong-RTL script (Hebrew) and
// asserting the opposite directions libpango is known to report.
//
// Not generated. `scripts/smoke-test.sh <tier>` copies every `smoke/*.swift`
// plus `smoke/tier<N>/*.swift` into the generated SmokeTests target before
// running `swift test`. Only installed for tier >= 3 (Pango's module).
import Testing
import GLib
import GObject
import Pango
@Suite("Tier 3 Pango smoke tests")
struct PangoSmokeTests {
@Test("findBaseDir(text:length:) reaches the real pango_find_base_dir and reports LTR for Latin script")
func findBaseDirLatinIsLTR() throws {
let text = "hello world"
let direction = findBaseDir(text: text, length: -1)
#expect(direction == .ltr)
}
@Test("findBaseDir(text:length:) reaches the real pango_find_base_dir and reports RTL for Hebrew script")
func findBaseDirHebrewIsRTL() throws {
let text = "שלום עולם"
let direction = findBaseDir(text: text, length: -1)
#expect(direction == .rtl)
}
}

View file

@ -0,0 +1,36 @@
// PixbufSmoke.swift
// Tier-3-only runtime smoke test for GdkPixbuf (Phase E2). Proves against
// the REAL libgdk_pixbuf, linked at runtime that:
// 1. `Pixbuf(colorspace:hasAlpha:bitsPerSample:width:height:)` reaches the
// real `gdk_pixbuf_new` C call, correctly marshalling an enum param
// (`Colorspace`), a `Bool` param (`hasAlpha` `gboolean`), and `Int32`
// params (`bitsPerSample`/`width`/`height`), and wraps the returned
// `GdkPixbuf*` as a live GObject via `takingOwnership`.
// 2. `getWidth()`/`getHeight()` round-trip through `gdk_pixbuf_get_width`/
// `gdk_pixbuf_get_height`, returning the values passed to the
// constructor not stubbed/placeholder data.
//
// Not generated. `scripts/smoke-test.sh <tier>` copies every `smoke/*.swift`
// plus `smoke/tier<N>/*.swift` into the generated SmokeTests target before
// running `swift test`. Only installed for tier 3 (GdkPixbuf's module).
import Testing
import GLib
import GObject
import GdkPixbuf
@Suite("Tier 3 GdkPixbuf smoke tests")
struct PixbufSmokeTests {
@Test("Pixbuf(colorspace:hasAlpha:bitsPerSample:width:height:) constructs a real GdkPixbuf and getWidth()/getHeight() round-trip")
func pixbufConstructionAndDimensions() throws {
let pixbuf = Pixbuf(colorspace: .rgb, hasAlpha: true, bitsPerSample: 8, width: 4, height: 7)
// Dimension round-trip: these values only match if the constructor
// actually reached `gdk_pixbuf_new` with the right arguments and the
// getters call back into the real C object rather than returning
// stub data.
#expect(pixbuf.getWidth() == 4)
#expect(pixbuf.getHeight() == 7)
}
}