1
0
Fork 0

Tier 2 compile-gate compliance: interfaces, cross-module refs, Gio

This commit is contained in:
Brendan Szymanski 2026-07-19 00:27:08 -04:00
parent 5abb48e889
commit 0504b34d7f
18 changed files with 9186 additions and 304 deletions

View file

@ -42,6 +42,9 @@ public enum MarshalIn: Equatable, Sendable {
case stringToC
/// Access the underlying pointer of an object or interface wrapper.
case objectPointer
/// Access the underlying pointer of an interface-typed wrapper (a
/// protocol existential `any Foo`).
case interfacePointer
/// Pass the `rawValue` of an enum (Int C int).
case enumRaw
/// Pass the `rawValue` of a bitfield (UInt32 C uint).
@ -89,6 +92,12 @@ public enum MarshalOut: Equatable, Sendable {
/// - 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?)
/// Wrap an object pointer as the concrete `<Name>Ref` interface wrapper,
/// returned as the protocol existential.
/// - Parameter adopt: When `true`, take ownership (`init(takingOwnership:)`,
/// transfer=full); when `false`, retain a borrowed reference
/// (`init(retaining:)`, transfer=none).
case interfaceWrap(adopt: Bool)
/// Unsupported causes the whole callable to be skipped.
/// - Parameter reason: Why the return value cannot be marshalled.
case unsupported(reason: String)
@ -178,6 +187,15 @@ public enum SkipReason: String, Codable, CaseIterable, Sendable {
case constructorOutParams
/// A signal parameter or return type could not be mapped.
case signalUnmappableParam
/// A type declaration duplicates one already declared in a dependency
/// module (e.g. GObject re-declaring GLib's `IOCondition`); the
/// downstream duplicate is skipped so unqualified cross-module
/// references stay unambiguous.
case duplicateOfDependency
/// A class member (property or method) is already declared, with an
/// identical Swift name, by an ancestor class planned in this module;
/// the subclass copy is redundant (GObject inheritance provides it).
case inheritedMember
}
/// A single skipped symbol: what was skipped, and why.
@ -426,19 +444,24 @@ public struct SignalPlan: Equatable, Sendable {
/// The return mapping, or `nil` for `void`.
public let returnMapping: Mapping?
/// The `@convention(c)` trampoline's C-level name, unique per module.
/// Format: `"_trampoline_\(namespace)_\(owningClass)_\(girName)"`.
public let trampolineCName: String
/// `true` when the owning type is a `protocol` (GObject interface)
/// rather than a concrete class the trampoline's instance parameter
/// must be wrapped via the interface's `Ref` concrete wrapper, not the
/// interface protocol itself (which has no initializers).
public let ownerIsInterface: Bool
/// Documentation from the GIR `<doc>` element.
public let doc: String?
public init(owningClassName: String, girName: String, swiftName: String,
isDetailed: Bool = false, parameters: [ParameterPlan] = [],
returnMapping: Mapping? = nil, trampolineCName: String,
doc: String? = nil) {
ownerIsInterface: Bool = false, doc: String? = nil) {
self.owningClassName = owningClassName; self.girName = girName
self.swiftName = swiftName; self.isDetailed = isDetailed
self.parameters = parameters; self.returnMapping = returnMapping
self.trampolineCName = trampolineCName; self.doc = doc
self.trampolineCName = trampolineCName; self.ownerIsInterface = ownerIsInterface
self.doc = doc
}
}
@ -540,6 +563,13 @@ public struct InterfacePlan: Equatable, Sendable {
public let name: String
public let cType: String
public let prereqs: [String]
/// The Swift name of a prerequisite that is a *class* (not another
/// interface), if any e.g. `TlsServerConnection` prerequisites
/// `TlsConnection`. The protocol inheritance clause then constrains
/// `Self: TlsConnection`, so the concrete `<Name>Ref` wrapper must
/// subclass it directly (inheriting its `pointer`/init/deinit) rather
/// than declaring its own bare ref-counted storage.
public let classPrereq: 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
@ -552,17 +582,24 @@ public struct InterfacePlan: Equatable, Sendable {
/// The module-qualified Swift name, e.g. `"GObject.TypePlugin"`.
/// Used to match against `ClassPlan.interfaces` entries.
public let qualifiedName: String
/// `true` when this interface's implementers are GObject-derived (i.e.
/// `var pointer` refers to a ref-counted `GObject*`). Gates emission of
/// the concrete `<Name>Ref` wrapper, which manages that refcount.
public let isGObject: Bool
public let doc: String?
public init(name: String, cType: String, prereqs: [String] = [],
classPrereq: String? = nil,
getTypeFunction: String? = nil, methods: [CallablePlan] = [],
properties: [PropertyPlan] = [], signals: [SignalPlan] = [],
qualifiedName: String = "",
qualifiedName: String = "", isGObject: Bool = true,
doc: String? = nil) {
self.name = name; self.cType = cType; self.prereqs = prereqs
self.classPrereq = classPrereq
self.getTypeFunction = getTypeFunction; self.methods = methods
self.properties = properties; self.signals = signals
self.qualifiedName = qualifiedName
self.isGObject = isGObject
self.doc = doc
}
}
@ -625,6 +662,9 @@ public struct PropertyPlan: Equatable, Sendable {
public struct ModulePlan: Sendable {
/// The Swift module name, e.g. `"GLib"`.
public let module: String
/// Direct dependency modules this module's files must `import`, e.g.
/// `["GLib", "GModule", "GObject"]` for `Gio`. Sorted, deduplicated.
public let dependencyModules: [String]
/// All successfully planned types.
public let types: [TypePlan]
/// Every skipped symbol with its reason.
@ -632,9 +672,9 @@ public struct ModulePlan: Sendable {
/// Coverage statistics derived from planned and skipped counts.
public let coverage: CoverageStats
public init(module: String, types: [TypePlan], skips: [SkipEntry],
public init(module: String, dependencyModules: [String] = [], types: [TypePlan], skips: [SkipEntry],
coverage: CoverageStats) {
self.module = module; self.types = types; self.skips = skips
self.module = module; self.dependencyModules = dependencyModules; self.types = types; self.skips = skips
self.coverage = coverage
}

View file

@ -33,12 +33,13 @@ import Foundation
public func renderModule(_ plan: ModulePlan) -> [String: String] {
var files: [String: String] = [:]
let depImports = plan.dependencyModules.map { "import \($0)\n" }.joined()
let header = """
// Generated by SwiftGtkGen. DO NOT EDIT.
import C\(plan.module)
import Foundation
\(depImports)
"""
var constants: [(name: String, body: String)] = []
@ -86,7 +87,7 @@ public func renderModule(_ plan: ModulePlan) -> [String: String] {
return false
}
files["Support.swift"] = renderSupport(moduleName: plan.module, hasSignals: hasSignals, hasCallbackBoxes: hasCallbacks)
files["Support.swift"] = renderSupport(moduleName: plan.module, dependencyModules: plan.dependencyModules, hasSignals: hasSignals, hasCallbackBoxes: hasCallbacks)
for filename in files.keys {
precondition(isValidGeneratedFileName(filename),
@ -143,7 +144,7 @@ private func leadingNameWord(_ name: String) -> String {
return word.isEmpty ? String(trimmed) : String(word)
}
private func renderSupport(moduleName: String, hasSignals: Bool = false, hasCallbackBoxes: Bool = false) -> String {
private func renderSupport(moduleName: String, dependencyModules: [String] = [], hasSignals: Bool = false, hasCallbackBoxes: Bool = false) -> String {
let glibError: String
if moduleName == "GLib" {
glibError = """
@ -171,28 +172,28 @@ private func renderSupport(moduleName: String, hasSignals: Bool = false, hasCall
// MARK: - Fundamental GType Constants
let G_TYPE_INVALID: UInt = 0
let G_TYPE_NONE: UInt = 4
let G_TYPE_INTERFACE: UInt = 8
let G_TYPE_CHAR: UInt = 12
let G_TYPE_BOOLEAN: UInt = 20
let G_TYPE_INT: UInt = 24
let G_TYPE_UINT: UInt = 28
let G_TYPE_LONG: UInt = 32
let G_TYPE_ULONG: UInt = 36
let G_TYPE_INT64: UInt = 40
let G_TYPE_UINT64: UInt = 44
let G_TYPE_ENUM: UInt = 48
let G_TYPE_FLAGS: UInt = 52
let G_TYPE_FLOAT: UInt = 56
let G_TYPE_DOUBLE: UInt = 60
let G_TYPE_STRING: UInt = 64
let G_TYPE_POINTER: UInt = 68
let G_TYPE_BOXED: UInt = 72
let G_TYPE_PARAM: UInt = 76
let G_TYPE_OBJECT: UInt = 80
let G_TYPE_GTYPE: UInt = 88
let G_TYPE_VARIANT: UInt = 96
public let G_TYPE_INVALID: UInt = 0
public let G_TYPE_NONE: UInt = 4
public let G_TYPE_INTERFACE: UInt = 8
public let G_TYPE_CHAR: UInt = 12
public let G_TYPE_BOOLEAN: UInt = 20
public let G_TYPE_INT: UInt = 24
public let G_TYPE_UINT: UInt = 28
public let G_TYPE_LONG: UInt = 32
public let G_TYPE_ULONG: UInt = 36
public let G_TYPE_INT64: UInt = 40
public let G_TYPE_UINT64: UInt = 44
public let G_TYPE_ENUM: UInt = 48
public let G_TYPE_FLAGS: UInt = 52
public let G_TYPE_FLOAT: UInt = 56
public let G_TYPE_DOUBLE: UInt = 60
public let G_TYPE_STRING: UInt = 64
public let G_TYPE_POINTER: UInt = 68
public let G_TYPE_BOXED: UInt = 72
public let G_TYPE_PARAM: UInt = 76
public let G_TYPE_OBJECT: UInt = 80
public let G_TYPE_GTYPE: UInt = 88
public let G_TYPE_VARIANT: UInt = 96
"""
} else {
gtypeConstants = ""
@ -277,12 +278,13 @@ private func renderSupport(moduleName: String, hasSignals: Bool = false, hasCall
primitiveShims = ""
}
let depImports = dependencyModules.map { "import \($0)\n" }.joined()
return """
// 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.
@inline(__always)
@ -516,18 +518,24 @@ private func renderInterface(_ plan: InterfacePlan) -> String {
lines.append("public protocol \(plan.name)\(protocolInherits) {")
lines.append(" var pointer: UnsafeMutableRawPointer { get }")
for method in plan.methods {
lines.append(contentsOf: renderProtocolRequirement(method))
}
for prop in plan.properties {
lines.append(contentsOf: renderPropertyRequirement(prop))
}
lines.append("}")
// Protocol extension with signal connect methods
if !plan.signals.isEmpty {
// Method/property bodies live in a protocol extension as default
// implementations (not bare requirements): every conformer gets a
// working implementation via `self.pointer` for free, and no
// requirement can go unwitnessed (a class's own member of the same
// name still wins by static dispatch specificity where it exists).
if !plan.methods.isEmpty || !plan.properties.isEmpty || !plan.signals.isEmpty {
lines.append("")
lines.append("extension \(plan.name) {")
for method in plan.methods {
lines.append(contentsOf: renderMethod(method))
lines.append("")
}
for prop in plan.properties {
lines.append(contentsOf: renderProperty(prop))
lines.append("")
}
for sig in plan.signals {
lines.append(contentsOf: renderSignalConnect(sig, className: plan.name))
lines.append("")
@ -535,31 +543,42 @@ private func renderInterface(_ plan: InterfacePlan) -> String {
lines.append("}")
}
return lines.joined(separator: "\n") + "\n"
}
/// Renders one interface method as a protocol requirement: the `func`
/// signature only no `public`, no body. The implementing class provides the
/// body via its own generated method (the C symbol lives on the class).
private func renderProtocolRequirement(_ plan: CallablePlan) -> [String] {
var lines: [String] = []
if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc).map { " \($0)" }) }
let retType = callableReturnType(plan) ?? ""
let throwsKeyword = plan.throwsError ? " throws" : ""
lines.append(" func \(plan.name)(\(swiftSignature(plan)))\(throwsKeyword)\(retType)")
return lines
}
/// Renders one interface property as a protocol requirement: `{ get }` or `{ get set }`.
private func renderPropertyRequirement(_ plan: PropertyPlan) -> [String] {
var lines: [String] = []
if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc).map { " \($0)" }) }
if plan.setter != nil {
lines.append(" var \(plan.swiftName): \(plan.swiftType) { get set }")
} else {
lines.append(" var \(plan.swiftName): \(plan.swiftType) { get }")
// Concrete wrapper: interface protocols have no initializers, so a
// value of `any Foo` cannot be constructed from a raw C pointer. Only
// emitted for GObject-derived interfaces, which is every case the
// mapper currently produces (`TypeMapper`'s `.interface` case guards
// on `registry.isGObject`).
if plan.isGObject {
lines.append("")
lines.append("/// Concrete, ref-counted storage for an `any \(plan.name)` value obtained")
lines.append("/// from a C call — interface protocols have no initializers of their own.")
if let classPrereq = plan.classPrereq {
// The protocol's own inheritance clause constrains conformers to
// `Self: \(classPrereq)` (a class-typed prerequisite), so the Ref
// must literally subclass it inheriting its pointer storage,
// inits, and deinit rather than declaring its own.
lines.append("public final class \(plan.name)Ref: \(classPrereq), \(plan.name) {")
lines.append("}")
} else {
lines.append("public final class \(plan.name)Ref: \(plan.name) {")
lines.append(" public let pointer: UnsafeMutableRawPointer")
lines.append("")
lines.append(" public init(retaining pointer: UnsafeMutableRawPointer) {")
lines.append(" g_object_ref(pointer)")
lines.append(" self.pointer = pointer")
lines.append(" }")
lines.append("")
lines.append(" public init(takingOwnership pointer: UnsafeMutableRawPointer) {")
lines.append(" self.pointer = pointer")
lines.append(" }")
lines.append("")
lines.append(" isolated deinit {")
lines.append(" g_object_unref(pointer)")
lines.append(" }")
lines.append("}")
}
}
return lines
return lines.joined(separator: "\n") + "\n"
}
// Class
@ -718,7 +737,7 @@ private func renderSignalTrampoline(_ plan: SignalPlan) -> [String] {
let rawName = i == 0 ? "instance" : "p\(i)"
let shadowName = "captured\(rawName.prefix(1).uppercased())\(rawName.dropFirst())"
shadowLines.append("nonisolated(unsafe) let \(shadowName) = \(rawName)")
wrapperLines.append("let w\(i) = \(renderWrapperExpr(for: p, rawName: shadowName))")
wrapperLines.append("let w\(i) = \(renderWrapperExpr(for: p, rawName: shadowName, ownerIsInterface: plan.ownerIsInterface))")
}
let wrapperRefs = (0..<allParams.count).map { "w\($0)" }.joined(separator: ", ")
let shadowBody = shadowLines.map { " \($0)" }.joined(separator: "\n")
@ -738,13 +757,26 @@ private func renderSignalTrampoline(_ plan: SignalPlan) -> [String] {
/// Renders a single wrapper-expression for a signal parameter: the Swift
/// expression that converts a raw C argument (managed by the trampoline)
/// into a typed Swift wrapper. Extracted from renderSignalTrampoline.
private func renderWrapperExpr(for p: ParameterPlan, rawName: String) -> String {
private func renderWrapperExpr(for p: ParameterPlan, rawName: String, ownerIsInterface: Bool = false) -> String {
if p.isInstanceParameter {
if ownerIsInterface {
return "\(p.mapping.swiftType)Ref(retaining: \(rawName))"
}
return "\(p.mapping.swiftType)(retaining: \(rawName))"
}
switch p.mapping.marshalIn {
case .boxedPointer, .objectPointer:
return "\(p.mapping.swiftType)(retaining: \(rawName))"
let isOptional = p.mapping.swiftType.hasSuffix("?")
let baseType = isOptional ? String(p.mapping.swiftType.dropLast()) : p.mapping.swiftType
return "\(baseType)(retaining: \(rawName))"
case .interfacePointer:
let isOptional = p.mapping.swiftType.hasSuffix("?")
let baseType = isOptional ? String(p.mapping.swiftType.dropLast()) : p.mapping.swiftType
return "\(baseType)Ref(retaining: \(rawName))"
case .enumRaw:
return "\(p.mapping.swiftType)(rawValue: numericCast(\(rawName).rawValue))!"
case .bitfieldRaw:
return "\(p.mapping.swiftType)(rawValue: numericCast(\(rawName).rawValue))"
case .stringToC:
return "String(cString: \(rawName))"
case .boolToGboolean:
@ -903,13 +935,18 @@ private func outParamLocalType(_ param: ParameterPlan) -> String {
}
}
/// The initial value for an out-param local variable.
/// The initial value for an out-param local variable. `.direct` marshalIn
/// covers BOTH numeric scalars (`gint*`, `gsize*`) and raw pointers
/// (`gpointer*`, `GVariant**` imported as `UnsafeMutableRawPointer?`) the
/// zero literal that numeric out-params need is not a valid pointer
/// initializer, so the local type (not the marshal category) decides.
private func outParamInitValue(_ param: ParameterPlan) -> String {
switch param.mapping.marshalIn {
case .stringToC:
return "nil"
case .boolToGboolean, .direct, .numericCast:
return "0"
let localType = outParamLocalType(param)
return localType.hasSuffix("?") ? "nil" : "0"
case .enumRaw, .bitfieldRaw:
return ".init(rawValue: 0)"
default:
@ -936,7 +973,7 @@ private func outParamValueExpr(_ param: ParameterPlan, varName: String) -> Strin
return "\(swiftType)(rawValue: numericCast(\(varName).rawValue))!"
case .bitfieldFromRaw(let swiftType):
return "\(swiftType)(rawValue: numericCast(\(varName).rawValue))"
case .objectWrap, .objectRetain, .boxedWrap, .unsupported:
case .objectWrap, .objectRetain, .boxedWrap, .interfaceWrap, .unsupported:
return "\(varName) /* unsupported out-param marshalOut */"
}
}
@ -998,7 +1035,7 @@ private func renderMultiStatementBody(_ plan: CallablePlan, indent: String) -> [
let needsUnwrap: Bool
if hasReturn {
switch plan.returnMapping!.marshalOut {
case .objectWrap, .objectRetain, .boxedWrap, .stringCopy:
case .objectWrap, .objectRetain, .boxedWrap, .interfaceWrap, .stringCopy:
needsUnwrap = !plan.returnMapping!.swiftType.hasSuffix("?")
default:
needsUnwrap = false
@ -1146,7 +1183,7 @@ private func renderThrowingBody(_ plan: CallablePlan, indent: String) -> [String
let needsUnwrap: Bool
if hasReturn {
switch plan.returnMapping!.marshalOut {
case .objectWrap, .objectRetain, .boxedWrap, .stringCopy:
case .objectWrap, .objectRetain, .boxedWrap, .interfaceWrap, .stringCopy:
needsUnwrap = !plan.returnMapping!.swiftType.hasSuffix("?")
default:
needsUnwrap = false
@ -1281,25 +1318,31 @@ private func renderConstructor(_ plan: CallablePlan) -> [String] {
lines.append("\(indent)var error: UnsafeMutablePointer<GError>? = nil")
lines.append("\(indent)let __ptr = \(cCall)")
lines.append(contentsOf: errorCheck)
lines.append("\(indent)self.init(takingOwnership: __ptr)")
lines.append("\(indent)self.init(takingOwnership: _rawPointer(__ptr!))")
} else {
lines.append("\(indent)var error: UnsafeMutablePointer<GError>? = nil")
var scope = indent
for i in stringParams.indices {
let sp = stringParams[i]
lines.append("\(scope)let __ptr = \(sp.swiftName).withCString { \(sp.cName) in")
let prefix = i == 0 ? "let __result = " : ""
lines.append("\(scope)\(prefix)\(sp.swiftName).withCString { \(sp.cName) in")
scope += " "
}
lines.append("\(scope)let __result = \(cCall)")
for eLine in errorCheck {
let trimmed = eLine.hasPrefix(indent) ? String(eLine.dropFirst(indent.count)) : eLine
lines.append("\(scope)\(trimmed)")
}
lines.append("\(scope)self.init(takingOwnership: __result)")
// `self.init` (a delegating initializer call) cannot be nested
// inside a closure even a non-escaping one. So the C call's
// result is threaded back OUT through each `withCString`
// closure's implicit return (a nested closure body must be a
// single, un-bound expression for this to propagate an inner
// `let` declaration would make the outer closure infer `Void`),
// and `self.init` runs at the outer scope once every closure
// has returned.
lines.append("\(scope)\(cCall)")
for _ in stringParams {
scope = String(scope.dropLast(4))
lines.append("\(scope)}")
}
lines += errorCheck
lines.append("\(indent)self.init(takingOwnership: _rawPointer(__result!))")
}
} else {
let expr = renderCallExpression(plan)
@ -1332,7 +1375,7 @@ private func marshalCallArg(_ param: ParameterPlan) -> String {
return "\(param.swiftName) ? 1 : 0"
case .stringToC:
return param.swiftName
case .objectPointer:
case .objectPointer, .interfacePointer:
return pointerArg(param)
case .boxedPointer:
return pointerArg(param)
@ -1382,15 +1425,27 @@ private func marshalReturn(_ cCall: String, mapping: Mapping) -> String {
let wrapExpr = "\(baseType)(takingOwnership: _rawPointer(\(cCall)))"
if isOptional { return "\(cCall).map { \(baseType)(takingOwnership: _rawPointer($0)) }" }
return wrapExpr
case .interfaceWrap(let adopt):
let isOptional = mapping.swiftType.hasSuffix("?")
let baseType = isOptional ? String(mapping.swiftType.dropLast()) : mapping.swiftType
let ctor = adopt ? "takingOwnership" : "retaining"
if isOptional { return "\(cCall).map { \(baseType)Ref(\(ctor): _rawPointer($0)) }" }
return "\(baseType)Ref(\(ctor): _rawPointer(\(cCall)))"
case .boxedWrap(let copy, let copyFunction):
let isOptional = mapping.swiftType.hasSuffix("?")
let baseType = isOptional ? String(mapping.swiftType.dropLast()) : mapping.swiftType
let arg: String
if copy, let copyFn = copyFunction {
arg = isOptional ? "\(copyFn)($0)" : "\(copyFn)(\(cCall))"
} else {
arg = isOptional ? "$0" : cCall
if copy, copyFunction != nil {
// transfer=none: the C getter may return a `const` pointer, which
// a mutable copy/ref function cannot accept directly. Route
// through the record's `init(retaining:)` (emitted whenever a
// copy function exists see `renderRecord`), which itself calls
// the copy function on the mutable-cast instance pointer.
// `_rawPointer` accepts both `UnsafePointer<T>` (const) and
// `OpaquePointer` overloads, so this never mismatches constness.
if isOptional { return "\(cCall).map { \(baseType)(retaining: _rawPointer($0)) }" }
return "\(baseType)(retaining: _rawPointer(\(cCall)))"
}
let arg = isOptional ? "$0" : cCall
let wrapExpr = "\(baseType)(takingOwnership: _rawPointer(\(arg)))"
if isOptional { return "\(cCall).map { \(wrapExpr) }" }
return "\(baseType)(takingOwnership: _rawPointer(\(arg)))"

View file

@ -43,7 +43,8 @@ public func planModules(
let context = MapContext(
registry: registry,
currentModule: moduleName,
currentNamespace: namespace.name
currentNamespace: namespace.name,
dependencyModules: (analysis.directDependencies[moduleName] ?? []).sorted()
)
let plan = planNamespace(namespace, context: context)
@ -67,14 +68,41 @@ private func planNamespace(_ ns: Namespace, context: MapContext) -> ModulePlan {
var totalCallbacks = 0
var boundSignals = 0
var totalSignals = 0
// A type declared here is skipped if a type of the same GIR name is
// already declared in a dependency module e.g. GObject re-declaring
// GLib's `IOCondition`. Keeping both would make Step 1's unqualified
// cross-module names ambiguous. Applies to type declarations only
// (enums, bitfields, records, classes, interfaces, aliases), never to
// constants/functions/callbacks.
func duplicateDependencyModule(_ girSimpleName: String) -> String? {
for dep in context.dependencyModules {
if context.registry.resolve(name: girSimpleName, namespace: dep) != nil {
return dep
}
}
return nil
}
func skipIfDuplicate(_ girSimpleName: String) -> Bool {
guard let dep = duplicateDependencyModule(girSimpleName) else { return false }
skips.append(SkipEntry(symbol: "\(ns.name).\(girSimpleName)",
cIdentifier: girSimpleName,
reason: .duplicateOfDependency,
detail: "'\(girSimpleName)' is already declared in dependency module '\(dep)'"))
return true
}
// Enumerations
for enumeration in ns.enumerations {
totalTypes += 1; boundTypes += planEnum(into: &types, skips: &skips, enumeration: enumeration, context: context)
totalTypes += 1
if skipIfDuplicate(enumeration.name) { continue }
boundTypes += planEnum(into: &types, skips: &skips, enumeration: enumeration, context: context)
}
// Bitfields
for bitfield in ns.bitfields {
totalTypes += 1; boundTypes += planBit(into: &types, skips: &skips, bitfield: bitfield, context: context)
totalTypes += 1
if skipIfDuplicate(bitfield.name) { continue }
boundTypes += planBit(into: &types, skips: &skips, bitfield: bitfield, context: context)
}
// Constants
@ -90,21 +118,28 @@ private func planNamespace(_ ns: Namespace, context: MapContext) -> ModulePlan {
// Aliases
for alias in ns.aliases {
totalTypes += 1; boundTypes += planAliasType(into: &types, skips: &skips, alias: alias, context: context)
totalTypes += 1
if skipIfDuplicate(alias.name) { continue }
boundTypes += planAliasType(into: &types, skips: &skips, alias: alias, context: context)
}
for klass in ns.classes {
totalTypes += 1
if skipIfDuplicate(klass.name) { continue }
boundTypes += skipClass(into: &skips, into: &types,
boundCallables: &boundCallables, totalCallables: &totalCallables,
klass: klass, namespace: ns.name, context: context)
if case .class(let cp) = types.last { totalSignals += klass.signals.filter(\.symbolInfo.isBindable).count; boundSignals += cp.signals.count }
}
for iface in ns.interfaces {
totalTypes += 1; boundTypes += skipInterface(into: &skips, into: &types, iface: iface, namespace: ns.name, context: context)
totalTypes += 1
if skipIfDuplicate(iface.name) { continue }
boundTypes += skipInterface(into: &skips, into: &types, iface: iface, namespace: ns.name, context: context)
if case .interface(let ip) = types.last { totalSignals += iface.signals.filter(\.symbolInfo.isBindable).count; boundSignals += ip.signals.count }
}
for record in ns.records {
totalTypes += 1; boundTypes += skipRecord(into: &skips, into: &types, record: record, namespace: ns.name, context: context)
totalTypes += 1
if skipIfDuplicate(record.name) { continue }
boundTypes += skipRecord(into: &skips, into: &types, record: record, namespace: ns.name, context: context)
}
for callback in ns.callbacks {
totalCallbacks += 1
@ -178,10 +213,64 @@ private func planNamespace(_ ns: Namespace, context: MapContext) -> ModulePlan {
boundCallables -= lostCallables
}
// Post-pass: filter each interface's protocol requirements to only those
// whose Swift signature is witnessable by ALL implementing classes.
types = filterInterfaceRequirements(types, skips: &skips)
// 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,
@ -189,13 +278,13 @@ private func planNamespace(_ ns: Namespace, context: MapContext) -> ModulePlan {
boundSignals: boundSignals, totalSignals: totalSignals
)
return ModulePlan(module: context.currentModule, types: types, skips: skips, coverage: coverage)
return ModulePlan(module: context.currentModule, dependencyModules: context.dependencyModules, types: types, skips: skips, coverage: coverage)
}
// MARK: - Interface conformance reconciliation
// MARK: - Signature deduplication
/// Canonical key for comparing Swift functional signatures.
/// Excludes instance parameters and out-params, matching `swiftSignature`.
/// Canonical key for comparing Swift functional signatures (parameter labels
/// + types, throwing-ness, and return type). Excludes instance/out params.
private func signatureKey(_ plan: CallablePlan) -> String {
let retType = plan.returnMapping?.swiftType ?? "Void"
let params = plan.parameters
@ -205,68 +294,32 @@ private func signatureKey(_ plan: CallablePlan) -> String {
return "throws:\(plan.throwsError)|ret:\(retType)|(\(params))"
}
/// Post-pass: drops interface protocol requirements whose Swift signature is
/// NOT witnessable by every implementing class (where the class planned a method
/// with the same GIR name but a different Swift return/parameter type).
/// Dropped requirements are recorded as skip entries.
func filterInterfaceRequirements(
_ types: [TypePlan], skips: inout [SkipEntry]
) -> [TypePlan] {
// Build interface name implementer ClassPlans
var implementersByInterface: [String: [ClassPlan]] = [:]
for case .class(let plan) in types {
for iface in plan.interfaces {
implementersByInterface[iface, default: []].append(plan)
}
}
var result: [TypePlan] = []
for typePlan in types {
guard case .interface(let plan) = typePlan else {
result.append(typePlan)
continue
}
let implementers = implementersByInterface[plan.qualifiedName] ?? []
guard !implementers.isEmpty else {
// Orphan interface: no class implements it; keep all requirements.
result.append(.interface(plan))
continue
}
var filteredMethods: [CallablePlan] = []
for req in plan.methods {
let reqKey = signatureKey(req)
let witnessedByAll = implementers.allSatisfy { implPlan in
implPlan.methods.contains { candidate in
candidate.name == req.name && signatureKey(candidate) == reqKey
}
}
if witnessedByAll {
filteredMethods.append(req)
} else {
skips.append(SkipEntry(
symbol: "\(plan.qualifiedName).\(req.name)",
cIdentifier: req.cIdentifier,
reason: .interfaceMethodSignatureDrift,
detail: "Method '\(req.name)' on interface '\(plan.qualifiedName)' has no consistently-witnessed implementation across all implementing classes; dropped from protocol requirement"
))
}
}
if filteredMethods.count == plan.methods.count {
result.append(.interface(plan))
/// Drops callables whose full Swift signature collides with an earlier one
/// e.g. async `_finish` constructors that all map to the SAME rendered
/// `init(res: AsyncResult) throws` regardless of their distinct Swift names
/// (`newFinish`, `newForAddressFinish`, ): constructors render as bare
/// `init(...)`, so `plan.name` never appears in the emitted signature and
/// MUST be excluded from the dedup key. Methods, by contrast, keep their own
/// name in the emitted signature, so it stays part of their key.
/// The first occurrence wins; later collisions are recorded as skips.
private func dedupBySignature(
_ plans: [CallablePlan], isConstructor: Bool, symbolPrefix: String, skips: inout [SkipEntry]
) -> [CallablePlan] {
var seen: Set<String> = []
var result: [CallablePlan] = []
for plan in plans {
let key = isConstructor ? signatureKey(plan) : "\(plan.name)|\(signatureKey(plan))"
if seen.contains(key) {
let renderedName = isConstructor ? "init" : plan.name
skips.append(SkipEntry(
symbol: "\(symbolPrefix).\(plan.cIdentifier)",
cIdentifier: plan.cIdentifier,
reason: .nameCollision,
detail: "Swift signature '\(renderedName)(\(signatureKey(plan)))' already emitted by an earlier symbol"
))
} else {
let newPlan = InterfacePlan(
name: plan.name, cType: plan.cType,
prereqs: plan.prereqs,
getTypeFunction: plan.getTypeFunction,
methods: filteredMethods,
properties: plan.properties,
qualifiedName: plan.qualifiedName,
doc: plan.doc
)
result.append(.interface(newPlan))
seen.insert(key)
result.append(plan)
}
}
return result
@ -485,6 +538,15 @@ private let knownMissingCFunctions: Set<String> = [
// Removed from modern GLib (present in the GIR, absent from the shared
// object) these fail only at link time, not compile time.
"g_thread_init", "g_thread_init_with_errorcheck_mutexes",
// Declared in <gio/gsettingsbackend.h>, which the public umbrella
// (<gio/gio.h>) deliberately does not include (implementor-only API)
// exported by the .so but invisible to the Clang importer.
"g_settings_backend_changed", "g_settings_backend_changed_tree",
"g_settings_backend_get_default", "g_settings_backend_path_changed",
"g_settings_backend_path_writable_changed", "g_settings_backend_writable_changed",
"g_null_settings_backend_new", "g_memory_settings_backend_new",
// Declared in <gio/gnetworking.h>, likewise excluded from <gio/gio.h>.
"g_networking_init",
]
let knownMisleadingCFunctions: Set<String> = [
@ -598,6 +660,18 @@ func planInterface(_ iface: Interface, context: MapContext) -> (plan: InterfaceP
guard let resolved = registry.resolve(girName: qualified) else { return nil }
return registry.swiftTypeName(for: resolved, in: context.currentModule)
}
// A prerequisite that resolves to a *class* (not another interface) means
// every conformer must inherit from that class (Swift: `Self: SomeClass`
// constraint from the protocol's own inheritance clause). The concrete
// `<Name>Ref` wrapper must then subclass it directly, rather than
// declaring its own bare `pointer`/ref-count storage, to satisfy that
// constraint (e.g. `TlsServerConnection` requires `Self: TlsConnection`).
let classPrereq: String? = iface.prereqs.compactMap { prereq -> String? in
let qualified = prereq.contains(".") ? prereq : "\(context.currentNamespace).\(prereq)"
guard let resolved = registry.resolve(girName: qualified) else { return nil }
guard case .object = resolved.category else { return nil }
return registry.swiftTypeName(for: resolved, in: context.currentModule)
}.first
// Instance methods become protocol requirements. Unplannable ones are
// recorded as skips, matching class member behaviour.
@ -626,7 +700,7 @@ func planInterface(_ iface: Interface, context: MapContext) -> (plan: InterfaceP
// Signals
var signalPlans: [SignalPlan] = []
for signal in iface.signals where signal.symbolInfo.isBindable {
let result = planSignal(signal, onClass: iface.name, namespace: context.currentNamespace, context: context)
let result = planSignal(signal, onClass: iface.name, namespace: context.currentNamespace, ownerIsInterface: true, context: context)
if let plan = result.plan {
signalPlans.append(plan)
} else if let skip = result.skip {
@ -645,6 +719,7 @@ func planInterface(_ iface: Interface, context: MapContext) -> (plan: InterfaceP
let plan = InterfacePlan(
name: iface.name, cType: iface.cType,
prereqs: prereqSwiftNames,
classPrereq: classPrereq,
getTypeFunction: iface.getTypeFunction,
methods: methodPlans,
properties: propertyPlans,
@ -798,9 +873,17 @@ func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberS
let isOpen = registry.subclassedTypes().contains(girName)
let descendsIU = registry.descendsFromInitiallyUnowned(girName)
// Resolve implemented interfaces to their Swift names
// Resolve implemented interfaces to their Swift names, excluding any
// already provided by an ancestor class (redundant conformance is a
// Swift error, e.g. `DataInputStream: , Seekable` when its ancestor
// `BufferedInputStream` already conforms).
let ancestorInterfaceGirNames: Set<String> = Set(registry.ancestry(of: girName).flatMap { ancestor -> [String] in
if case .object(_, _, _, let ifaces) = ancestor.category { return ifaces }
return []
})
let interfaceNames: [String] = klass.implements.compactMap { ifaceName in
let qualified = ifaceName.contains(".") ? ifaceName : "\(context.currentNamespace).\(ifaceName)"
guard !ancestorInterfaceGirNames.contains(qualified) else { return nil }
guard let resolved = registry.resolve(girName: qualified) else { return nil }
return registry.swiftTypeName(for: resolved, in: context.currentModule)
}
@ -825,11 +908,13 @@ func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberS
into: &constructorPlans)
}
}
constructorPlans = dedupBySignature(constructorPlans, isConstructor: true, symbolPrefix: girName, skips: &memberSkips)
var methodPlans: [CallablePlan] = []
for method in klass.methods where method.symbolInfo.isBindable {
collect(planMethod(method, context: context), into: &methodPlans)
}
methodPlans = dedupBySignature(methodPlans, isConstructor: false, symbolPrefix: girName, skips: &memberSkips)
var functionPlans: [CallablePlan] = []
for fn in klass.functions where fn.symbolInfo.isBindable {
@ -986,6 +1071,17 @@ private func planCallable(
parameters: [Parameter], returnValue: ReturnValue, throwsGError: Bool,
doc: String?, isStatic: Bool, context: MapContext
) -> CallablePlanResult {
// Applies to methods too, not just free functions: some GIR-declared C
// symbols (e.g. GSettingsBackend's `g_settings_backend_*` implementor
// API) are exported by the .so but declared only in a header the public
// umbrella (`<gio/gio.h>`) does not include, so Clang never sees the
// prototype and the generated call is `cannot find in scope`.
if knownMissingCFunctions.contains(cIdentifier) {
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
reason: .unknownType,
detail: "C symbol '\(cIdentifier)' is not visible through the public umbrella header"))
}
// Check parameters (GError** is implicit in throws="1", not in the
// parameter list the renderer synthesizes the &error arg via
// cArguments(error: true). Do NOT strip the last parameter, it's real.)
@ -1074,7 +1170,8 @@ func planMethod(_ method: Method, context: MapContext) -> CallablePlanResult {
/// - context: The resolution context.
/// - Returns: A tuple of optional plan and optional skip entry (exactly one is non-nil).
private func planSignal(_ signal: Signal, onClass className: String,
namespace: String, context: MapContext) -> (plan: SignalPlan?, skip: SkipEntry?) {
namespace: String, ownerIsInterface: Bool = false,
context: MapContext) -> (plan: SignalPlan?, skip: SkipEntry?) {
let girName = "\(context.currentNamespace).\(className).\(signal.name)"
var signalParams: [ParameterPlan] = []
@ -1141,7 +1238,13 @@ private func planSignal(_ signal: Signal, onClass className: String,
}
let swiftName = swiftFunctionName(signal.name)
let trampolineCName = "_trampoline_\(namespace)_\(className)_\(signal.name)"
// The C/Swift identifier segment must be a valid identifier; GIR signal
// names may contain hyphens (`"drive-changed"`). GObject treats `-` and
// `_` as equivalent in signal names, so this cannot collide with a
// distinct signal. The *string* passed to `g_signal_connect_data` keeps
// the original hyphenated `girName` below.
let trampolineSignalSegment = signal.name.replacingOccurrences(of: "-", with: "_")
let trampolineCName = "_trampoline_\(namespace)_\(className)_\(trampolineSignalSegment)"
let plan = SignalPlan(
owningClassName: className, girName: signal.name,
@ -1150,6 +1253,7 @@ private func planSignal(_ signal: Signal, onClass className: String,
parameters: signalParams,
returnMapping: returnMapping,
trampolineCName: trampolineCName,
ownerIsInterface: ownerIsInterface,
doc: signal.doc
)
return (plan, nil)

View file

@ -101,10 +101,16 @@ public struct MapContext: Sendable {
public let registry: TypeRegistry
public let currentModule: String
public let currentNamespace: String
/// Direct dependency modules of `currentModule` (e.g. `["GLib", "GModule", "GObject"]`
/// for `Gio`), used to emit per-file imports and detect types duplicated
/// from an upstream dependency.
public let dependencyModules: [String]
public init(registry: TypeRegistry, currentModule: String, currentNamespace: String) {
public init(registry: TypeRegistry, currentModule: String, currentNamespace: String,
dependencyModules: [String] = []) {
self.registry = registry; self.currentModule = currentModule
self.currentNamespace = currentNamespace
self.dependencyModules = dependencyModules
}
}
@ -242,13 +248,15 @@ private func mapTypeRef(
category: .needsClass)
case .interface:
// Interfaces become protocols params work (.objectPointer accesses
// the `pointer` requirement), but returns are unsupported (protocols
// can't be constructed). The planner skips callables whose return
// mapping has an unsupported marshalOut.
// Interfaces become protocols with `var pointer` plus extension
// default implementations. Params pass through `.interfacePointer`
// (accesses the `pointer` requirement); returns/signal values
// construct the concrete `<Name>Ref` wrapper via `.interfaceWrap`
// (see `renderInterface`) since the protocol itself has no
// initializer to construct from a raw pointer.
let swiftType = context.registry.swiftTypeName(for: resolved, in: context.currentModule)
return Mapping(swiftType: swiftType, cSwiftType: "UnsafeMutableRawPointer?",
marshalIn: .objectPointer, marshalOut: .unsupported(reason: "interface return"),
marshalIn: .interfacePointer, marshalOut: .interfaceWrap(adopt: transfer == .full),
category: .needsClass)
case .enumeration:

View file

@ -372,7 +372,7 @@ public struct TypeRegistry: Sendable {
/// - module: The Swift module the reference is being written in.
/// - Returns: The Swift type name to emit.
public func swiftTypeName(for type: ResolvedType, in module: String) -> String {
type.swiftModule == module ? type.swiftName : "\(type.swiftModule).\(type.swiftName)"
type.swiftName
}
/// Every class that is subclassed by some other loaded class.

View file

@ -1,9 +1,10 @@
// InterfaceConformanceTests.swift
// Covers Phase C5 conformance: classes emit `: Parent, Interface` headers and
// protocol requirements are filtered to those every implementing class can
// witness. Methods whose Swift signature drifts between the interface node and
// a class node are dropped from the protocol (recorded as a skip) so the
// conformance stays compilable.
// Covers interface conformance under the extension-default model (Phase E1):
// classes emit `: Parent, Interface` headers; the interface's methods/properties
// render as protocol-extension DEFAULT implementations (not bare requirements),
// so a class's own method of the same name coexists without redeclaration
// errors, and a concrete `<Name>Ref` wrapper is emitted so `any Interface`
// values can be constructed from a raw C pointer.
import Testing
@ -29,11 +30,13 @@ struct InterfaceConformanceTests {
return MapContext(registry: registry, currentModule: "GObject", currentNamespace: "GObject")
}
@Test("Class implementing interface emits conformance clause")
@Test("Class implementing interface emits conformance clause; interface method becomes an extension default")
func conformanceClauseEmitted() throws {
// Interface: TypePlugin.use returns Void
// Class: TypeModule implements TypePlugin, has its own `use` returning Bool
// (signature drifts requirement dropped, but conformance still emitted)
// Class: TypeModule implements TypePlugin, has its own `use` returning Bool.
// Under the extension-default model both coexist: the class's own
// method is a distinct overload from the protocol extension default,
// so no redeclaration/witnessing conflict is possible.
let iface = Interface(
name: "TypePlugin", cType: "GTypePlugin",
methods: [
@ -62,17 +65,12 @@ struct InterfaceConformanceTests {
]
)
// Plan both, build type plan array, then filter
let ctx = makeContext()
let (ifacePlan, _) = planInterface(iface, context: ctx)
let (classPlan, _) = planClass(klass, context: ctx)
var skips: [SkipEntry] = []
let types: [TypePlan] = [.class(classPlan), .interface(ifacePlan)]
let filteredTypes = filterInterfaceRequirements(types, skips: &skips)
// Render the filtered module
let module = ModulePlan(module: "GObject", types: filteredTypes, skips: skips,
let module = ModulePlan(module: "GObject", types: types, skips: [],
coverage: CoverageStats())
let files = renderModule(module)
let classSrc = files["TypeModule.swift"] ?? ""
@ -80,67 +78,21 @@ struct InterfaceConformanceTests {
#expect(classSrc.contains("class TypeModule: Object, TypePlugin {"))
// Drifted method `use` dropped from protocol
#expect(!ifaceSrc.contains("func use"))
// Protocol body is bare only the `pointer` requirement.
#expect(ifaceSrc.contains("public protocol TypePlugin {"))
#expect(ifaceSrc.contains("var pointer: UnsafeMutableRawPointer { get }"))
// Skip recorded for the drift
let driftSkips = skips.filter { $0.reason == .interfaceMethodSignatureDrift }
#expect(driftSkips[0].symbol == "TypePlugin.use")
}
// The interface method is a protocol EXTENSION default, not a
// requirement it keeps its own (Void) signature regardless of the
// implementing class's differently-signatured method.
#expect(ifaceSrc.contains("extension TypePlugin {"))
#expect(ifaceSrc.contains("public func use("))
@Test("Consistent interface/class signatures keep the requirement in the protocol")
func consistentSignaturesKeepRequirement() throws {
// Regression guard: when the interface and implementing class agree on
// a method's signature, the requirement MUST remain in the rendered
// protocol. Without this test the filter could over-drop every
// requirement and the suite would still pass (the drift test only
// asserts dropping; the no-interfaces test never exercises the keep
// branch at all).
let iface = Interface(
name: "TypePlugin", cType: "GTypePlugin",
methods: [
Method(name: "use", cIdentifier: "g_type_plugin_use",
parameters: [
Parameter(name: "self", type: .pointer,
cType: "GTypePlugin*",
isInstanceParameter: true)
],
returnValue: ReturnValue(type: .boolean)),
],
getTypeFunction: "g_type_plugin_get_type"
)
let klass = Class(
name: "TypeModule", cType: "GTypeModule", parent: "Object",
getTypeFunction: "g_type_module_get_type",
implements: ["TypePlugin"],
methods: [
Method(name: "use", cIdentifier: "g_type_module_use",
parameters: [
Parameter(name: "self", type: .pointer,
cType: "GTypeModule*",
isInstanceParameter: true)
],
returnValue: ReturnValue(type: .boolean)),
]
)
let ctx = makeContext()
let (ifacePlan, _) = planInterface(iface, context: ctx)
let (classPlan, _) = planClass(klass, context: ctx)
var skips: [SkipEntry] = []
let types: [TypePlan] = [.class(classPlan), .interface(ifacePlan)]
let filteredTypes = filterInterfaceRequirements(types, skips: &skips)
let module = ModulePlan(module: "GObject", types: filteredTypes, skips: skips,
coverage: CoverageStats())
let files = renderModule(module)
let ifaceSrc = files["TypePlugin.swift"] ?? ""
// Requirement kept: `func use` appears in the protocol with a Bool return.
#expect(ifaceSrc.contains("func use"))
// No drift skip was recorded.
#expect(skips.filter { $0.reason == .interfaceMethodSignatureDrift }.isEmpty)
// Concrete Ref wrapper is emitted so `any TypePlugin` is constructible.
#expect(ifaceSrc.contains("public final class TypePluginRef: TypePlugin {"))
#expect(ifaceSrc.contains("init(retaining pointer: UnsafeMutableRawPointer)"))
#expect(ifaceSrc.contains("init(takingOwnership pointer: UnsafeMutableRawPointer)"))
#expect(ifaceSrc.contains("isolated deinit"))
}
@Test("Class with no interfaces keeps existing header unchanged")

View file

@ -1,10 +1,11 @@
// InterfaceGenerationTests.swift
// Covers Phase C5: GObject interface method planning and rendering. An
// interface method becomes a protocol *requirement* a `func` signature with
// no `public` modifier and no body because the C symbol that implements it
// lives on the conforming class, not the interface. Unplannable methods
// (unsupported parameter types) are recorded as skips, mirroring class
// members. These tests lock in that surface so a regression surfaces here.
// Covers interface method planning and rendering under the extension-default
// model (Phase E1): an interface method's body is rendered as a `public`
// protocol-extension DEFAULT implementation (`extension Ping { public func }`)
// calling the C symbol via `self.pointer`, since interface protocols have no
// members of their own beyond `pointer`. Unplannable methods (unsupported
// parameter types) are recorded as skips, mirroring class members. These
// tests lock in that surface so a regression surfaces here.
import Testing
@ -32,8 +33,8 @@ struct InterfaceGenerationTests {
return renderModule(module)["\(plan.name).swift"] ?? ""
}
@Test("Interface instance methods become protocol requirements")
func methodsBecomeRequirements() throws {
@Test("Interface instance methods become protocol extension default implementations")
func methodsBecomeExtensionDefaults() throws {
let iface = Interface(
name: "Ping", cType: "GPing",
methods: [
@ -51,10 +52,13 @@ struct InterfaceGenerationTests {
#expect(plan.methods[0].name == "getId")
let source = render(plan)
// A requirement no `public`, no body.
#expect(source.contains("func getId() -> Int"))
#expect(!source.contains("public func getId"))
#expect(!source.contains("g_ping_get_id")) // no body means no C call
// Protocol body is bare only the `pointer` requirement.
#expect(source.contains("public protocol Ping {"))
#expect(source.contains("var pointer: UnsafeMutableRawPointer { get }"))
// A default implementation `public`, with a body calling the C symbol.
#expect(source.contains("extension Ping {"))
#expect(source.contains("public func getId() -> Int"))
#expect(source.contains("g_ping_get_id"))
}
@Test("Unplannable interface methods are recorded as skips, not requirements")

View file

@ -1,8 +1,8 @@
// PropertyGenerationTests.swift
// Covers Phase C6: GObject property planning and rendering. A property becomes
// a Swift computed `var` backed by the GValue machinery
// (`g_value_init` / `g_object_get_property` / `g_object_set_property`), or for
// interfaces a `{ get }` / `{ get set }` protocol requirement. Properties
// (`g_value_init` / `g_object_get_property` / `g_object_set_property`), rendered
// as a `public var` for interfaces, as a protocol-extension default. Properties
// whose type has no `GValueOps` are recorded as skips.
//
// These tests also lock in the numeric bridging of enum/flags getters: a
@ -235,10 +235,10 @@ struct PropertyGenerationTests {
#expect(!source.contains("internalThing"))
}
// Interface property requirements
// Interface property extension defaults
@Test("A readable-only interface property becomes a { get } requirement")
func interfaceReadOnlyPropertyIsGetRequirement() throws {
@Test("A readable-only interface property becomes a GValue-backed extension default getter")
func interfaceReadOnlyPropertyIsExtensionDefault() throws {
let iface = Interface(
name: "Orientable", cType: "GtkOrientable",
properties: [
@ -253,14 +253,16 @@ struct PropertyGenerationTests {
let module = ModulePlan(module: "GObject", types: [.interface(plan)], skips: [],
coverage: CoverageStats())
let source = renderModule(module)["Orientable.swift"] ?? ""
#expect(source.contains("var orientation: Orientation { get }"))
// A requirement no body, no GValue machinery.
#expect(!source.contains("g_object_get_property"))
#expect(!source.contains("public var orientation"))
// Protocol body is bare only the `pointer` requirement.
#expect(!source.contains("var orientation: Orientation { get }"))
// A default implementation, in a protocol extension, with a real body.
#expect(source.contains("extension Orientable {"))
#expect(source.contains("public var orientation: Orientation"))
#expect(source.contains("g_object_get_property"))
}
@Test("A writable interface property becomes a { get set } requirement")
func interfaceWritablePropertyIsGetSetRequirement() throws {
@Test("A writable interface property becomes a GValue-backed extension default getter+setter")
func interfaceWritablePropertyIsExtensionDefault() throws {
let iface = Interface(
name: "Editable", cType: "GtkEditable",
properties: [
@ -271,7 +273,10 @@ struct PropertyGenerationTests {
let module = ModulePlan(module: "GObject", types: [.interface(plan)], skips: [],
coverage: CoverageStats())
let source = renderModule(module)["Editable.swift"] ?? ""
#expect(source.contains("var text: String { get set }"))
#expect(!source.contains("var text: String { get set }"))
#expect(source.contains("extension Editable {"))
#expect(source.contains("public var text: String"))
#expect(source.contains("g_object_set_property"))
}
// getter=/setter= method delegation

View file

@ -185,4 +185,114 @@ struct RendererCallableTests {
let outSkip = skips.first { $0.reason == .constructorOutParams }
#expect(outSkip != nil)
}
// MARK: - E1: throwing constructor with string params doesn't nest self.init
@Test("Throwing constructor with a String param threads the result out of withCString instead of nesting self.init")
func throwingConstructorWithStringParamAvoidsNestedSelfInit() throws {
// `self.init` (a delegating initializer call) is illegal inside any
// closure, including a non-escaping `withCString` trailing closure.
// Regression guard for the tier-2 DBusConnection/DBusProxy bug where
// string-parameterised throwing constructors called self.init from
// inside the withCString closure.
let klass = Class(
name: "Proxy", cType: "GProxy", parent: "Object",
getTypeFunction: "g_proxy_get_type",
constructors: [
Constructor(name: "new_for_bus_sync", cIdentifier: "g_proxy_new_for_bus_sync",
parameters: [
Parameter(name: "name", type: .string, cType: "const char*"),
],
returnValue: ReturnValue(transferOwnership: .full),
throwsGError: true),
]
)
let (plan, _) = planClass(klass, context: makeContext())
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
coverage: CoverageStats())
let src = renderModule(module)["Proxy.swift"] ?? ""
// The C call (returning the result) lives INSIDE the withCString
// closure as its trailing/implicit-return expression...
#expect(src.contains("g_proxy_new_for_bus_sync(cString0, &error)"))
// ...while `self.init` runs OUTSIDE every closure, never nested.
#expect(!src.contains("cString0, &error)\n self.init"))
let lines = src.components(separatedBy: "\n")
guard let selfInitLine = lines.first(where: { $0.contains("self.init(takingOwnership:") }) else {
Issue.record("expected a self.init(takingOwnership:) line"); return
}
// Indentation of 8 spaces == top-level init body scope, not nested
// one level inside the withCString closure (12 spaces).
#expect(selfInitLine.hasPrefix(" self.init"))
}
// MARK: - E1: constructor signature deduplication
@Test("Two constructors with the same rendered init signature but different Swift names dedup to one, keeping the first")
func constructorsWithIdenticalSignatureDedup() throws {
// `init(...)` never carries the constructor's Swift name (it always
// renders as bare `init`), so two GIR constructors mapping to
// DIFFERENT Swift names (`newFinish` vs `newForAddressFinish`) but
// the IDENTICAL parameter/throws/return shape still collide at
// `init(res:)`. Regression guard for the tier-2 DBusConnection bug.
let resParam = Parameter(name: "res", type: .typeRef("AsyncResult", namespace: "GObject"),
cType: "GAsyncResult*")
let klass = Class(
name: "Connection", cType: "GConnection", parent: "Object",
getTypeFunction: "g_connection_get_type",
constructors: [
Constructor(name: "new_finish", cIdentifier: "g_connection_new_finish",
parameters: [resParam],
returnValue: ReturnValue(transferOwnership: .full),
throwsGError: true),
Constructor(name: "new_for_address_finish", cIdentifier: "g_connection_new_for_address_finish",
parameters: [resParam],
returnValue: ReturnValue(transferOwnership: .full),
throwsGError: true),
]
)
let ctx = MapContext(
registry: TypeRegistry(repositories: ["GObject": Repository(namespaces: [
Namespace(name: "GObject", version: "2.0", classes: [
Class(name: "Object", cType: "GObject", parent: nil, getTypeFunction: "g_object_get_type"),
Class(name: "AsyncResult", cType: "GAsyncResult", parent: "Object",
getTypeFunction: "g_async_result_get_type"),
])
])]),
currentModule: "GObject", currentNamespace: "GObject"
)
let (plan, skips) = planClass(klass, context: ctx)
#expect(plan.constructors.count == 1)
#expect(plan.constructors[0].cIdentifier == "g_connection_new_finish")
let dedupSkip = skips.first { $0.reason == .nameCollision }
#expect(dedupSkip?.cIdentifier == "g_connection_new_for_address_finish")
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
coverage: CoverageStats())
let src = renderModule(module)["Connection.swift"] ?? ""
let occurrences = src.components(separatedBy: "convenience init(res: AsyncResult) throws").count - 1
#expect(occurrences == 1)
}
// MARK: - E1: pointer out-param zero-initializes to nil, not 0
@Test("A gpointer out-param local variable initializes to nil, not the integer 0")
func pointerOutParamInitializesToNil() throws {
// `.direct` marshalIn covers both numeric out-params (`gint*` `0`)
// and raw-pointer out-params (`gpointer*` `UnsafeMutableRawPointer?`,
// which `= 0` cannot initialize). Regression guard for the tier-2
// FileInfo.getAttributeData bug.
let fn = GlobalFunction(
name: "get_attribute_data", cIdentifier: "g_file_info_get_attribute_data",
parameters: [
Parameter(name: "attribute", type: .string, cType: "const char*"),
Parameter(name: "value_pp", type: .pointer, cType: "gpointer*", direction: .out),
],
returnValue: ReturnValue(type: .void)
)
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
Issue.record("expected get_attribute_data to plan successfully"); return
}
let body = renderCallable(plan)
#expect(body.contains("var out0: UnsafeMutableRawPointer? = nil"))
#expect(!body.contains("var out0: UnsafeMutableRawPointer? = 0"))
}
}

View file

@ -282,7 +282,7 @@ struct TypeMapperTests {
let ctx = makeContext()
let mapping = try map(.typeRef("Object", namespace: "GObject"),
nullable: false, transfer: .none, context: ctx)
#expect(mapping.swiftType == "GObject.Object")
#expect(mapping.swiftType == "Object")
#expect(mapping.cSwiftType == "UnsafeMutableRawPointer?")
#expect(mapping.marshalIn == .objectPointer)
#expect(mapping.marshalOut == .objectWrap(sink: false))
@ -301,8 +301,9 @@ struct TypeMapperTests {
let ctx = makeContext()
let mapping = try map(.typeRef("TypePlugin", namespace: "GObject"),
nullable: false, transfer: .none, context: ctx)
#expect(mapping.swiftType == "GObject.TypePlugin")
#expect(mapping.marshalIn == .objectPointer)
#expect(mapping.swiftType == "TypePlugin")
#expect(mapping.marshalIn == .interfacePointer)
#expect(mapping.marshalOut == .interfaceWrap(adopt: false))
}
@Test("Enum typeRef maps to enum raw-value wrapping")
@ -310,10 +311,10 @@ struct TypeMapperTests {
let ctx = makeContext()
let mapping = try map(.typeRef("ParamFlags", namespace: "GObject"),
nullable: false, transfer: .none, context: ctx)
#expect(mapping.swiftType == "GObject.ParamFlags")
#expect(mapping.swiftType == "ParamFlags")
#expect(mapping.cSwiftType == "GParamFlags")
#expect(mapping.marshalIn == .enumRaw)
#expect(mapping.marshalOut == .enumFromRaw(swiftType: "GObject.ParamFlags"))
#expect(mapping.marshalOut == .enumFromRaw(swiftType: "ParamFlags"))
#expect(mapping.gvalue?.typeMacro == "G_TYPE_ENUM")
}
@ -322,10 +323,10 @@ struct TypeMapperTests {
let ctx = makeContext()
let mapping = try map(.typeRef("SignalFlags", namespace: "GObject"),
nullable: false, transfer: .none, context: ctx)
#expect(mapping.swiftType == "GObject.SignalFlags")
#expect(mapping.swiftType == "SignalFlags")
#expect(mapping.cSwiftType == "GSignalFlags")
#expect(mapping.marshalIn == .bitfieldRaw)
#expect(mapping.marshalOut == .bitfieldFromRaw(swiftType: "GObject.SignalFlags"))
#expect(mapping.marshalOut == .bitfieldFromRaw(swiftType: "SignalFlags"))
#expect(mapping.gvalue?.typeMacro == "G_TYPE_FLAGS")
}
@ -334,7 +335,7 @@ struct TypeMapperTests {
let ctx = makeContext()
let mapping = try map(.typeRef("Value", namespace: "GObject"),
nullable: false, transfer: .none, context: ctx)
#expect(mapping.swiftType == "GObject.Value")
#expect(mapping.swiftType == "Value")
#expect(mapping.cSwiftType == "UnsafeMutableRawPointer?")
#expect(mapping.marshalIn == .boxedPointer)
#expect(mapping.marshalOut == .boxedWrap(copy: true, copyFunction: "g_value_copy")) // transfer != .full copy
@ -524,7 +525,7 @@ struct TypeMapperTests {
// Should succeed without throwing.
let mapping = try map(.typeRef("Value", namespace: "GObject"),
nullable: false, transfer: .none, context: ctx)
#expect(mapping.swiftType == "GObject.Value")
#expect(mapping.swiftType == "Value")
#expect(mapping.marshalOut == .boxedWrap(copy: true, copyFunction: "g_value_copy"))
}
}

View file

@ -159,12 +159,15 @@ struct TypeRegistryTests {
#expect(!subclassed.contains("Gtk.Label"))
}
@Test("Swift spelling qualifies only cross-module references")
@Test("Swift spelling is always unqualified — per-file dependency imports resolve cross-module references")
func spellsSwiftTypeNames() throws {
let registry = makeRegistry()
let object = try #require(registry.resolve(girName: "GObject.Object"))
let widget = try #require(registry.resolve(girName: "Gtk.Widget"))
#expect(registry.swiftTypeName(for: object, in: "Gtk") == "GObject.Object")
// Cross-module references are unqualified (`Object`, not `GObject.Object`):
// qualifying is impossible when the C typedef of the same name (from
// the generated file's `import CGtk`) shadows the Swift module.
#expect(registry.swiftTypeName(for: object, in: "Gtk") == "Object")
#expect(registry.swiftTypeName(for: object, in: "GObject") == "Object")
#expect(registry.swiftTypeName(for: widget, in: "Gtk") == "Widget")
}

View file

@ -66,6 +66,12 @@
"reason" : "plainRecord",
"symbol" : "GObject.FlagsValue"
},
{
"cIdentifier" : "IOCondition",
"detail" : "'IOCondition' is already declared in dependency module 'GLib'",
"reason" : "duplicateOfDependency",
"symbol" : "GObject.IOCondition"
},
{
"cIdentifier" : "GInitiallyUnownedClass",
"detail" : "GObject class struct for 'InitiallyUnowned'",
@ -990,12 +996,6 @@
"reason" : "plainRecord",
"symbol" : "GObject.type_free_instance"
},
{
"cIdentifier" : "g_type_get_plugin",
"detail" : "return type: interface return",
"reason" : "unknownType",
"symbol" : "GObject.type_get_plugin"
},
{
"cIdentifier" : "g_type_interface_add_prerequisite",
"detail" : "deprecatedRemoved",
@ -1109,20 +1109,14 @@
"detail" : "C symbol 'g_variant_get_gtype' is not exported by the system library",
"reason" : "unknownType",
"symbol" : "GObject.variant_get_gtype"
},
{
"cIdentifier" : "g_type_plugin_use",
"detail" : "Method 'use' on interface 'TypePlugin' has no consistently-witnessed implementation across all implementing classes; dropped from protocol requirement",
"reason" : "interfaceMethodSignatureDrift",
"symbol" : "TypePlugin.use"
}
],
"module" : "GObject",
"stats" : {
"boundCallables" : 112,
"boundCallables" : 113,
"boundCallbacks" : 32,
"boundSignals" : 3,
"boundTypes" : 77,
"boundTypes" : 76,
"totalCallables" : 284,
"totalCallbacks" : 34,
"totalSignals" : 3,

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,57 @@
{
"entries" : [
{
"cIdentifier" : "GModule",
"detail" : "no GType registration",
"reason" : "plainRecord",
"symbol" : "GModule.Module"
},
{
"cIdentifier" : "GModuleCheckInit",
"detail" : "callback 'ModuleCheckInit' has unmappable param/return: callback 'ModuleCheckInit' param 'module' unmappable: 'GModule.Module' has no GType registration or lifetime functions",
"reason" : "callbackWithoutUserData",
"symbol" : "GModule.ModuleCheckInit"
},
{
"cIdentifier" : "GModuleUnload",
"detail" : "callback 'ModuleUnload' has unmappable param/return: callback 'ModuleUnload' param 'module' unmappable: 'GModule.Module' has no GType registration or lifetime functions",
"reason" : "callbackWithoutUserData",
"symbol" : "GModule.ModuleUnload"
},
{
"cIdentifier" : "g_module_build_path",
"detail" : "deprecatedRemoved",
"reason" : "deprecatedRemoved",
"symbol" : "GModule.module_build_path"
},
{
"cIdentifier" : "g_module_error",
"detail" : "deprecatedRemoved",
"reason" : "deprecatedRemoved",
"symbol" : "GModule.module_error"
},
{
"cIdentifier" : "g_module_error_quark",
"detail" : "deprecatedRemoved",
"reason" : "deprecatedRemoved",
"symbol" : "GModule.module_error_quark"
},
{
"cIdentifier" : "g_module_supported",
"detail" : "deprecatedRemoved",
"reason" : "deprecatedRemoved",
"symbol" : "GModule.module_supported"
}
],
"module" : "GModule",
"stats" : {
"boundCallables" : 0,
"boundCallbacks" : 0,
"boundSignals" : 0,
"boundTypes" : 6,
"totalCallables" : 4,
"totalCallbacks" : 2,
"totalSignals" : 0,
"totalTypes" : 9
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -2,30 +2,49 @@
# smoke-test.sh — runtime smoke tests for the generated bindings.
#
# The compile gate proves the generated Swift type-checks. This goes further:
# it generates the tier-1 bindings, links them against the REAL C libraries via
# pkg-config, and runs hand-written swift-testing cases (smoke/*.swift) that
# invoke real GLib/GObject functionality through the wrappers and check the
# results at runtime.
# it generates a tier's bindings, links them against the REAL C libraries via
# pkg-config, and runs hand-written swift-testing cases (smoke/*.swift, plus
# smoke/tier<N>/*.swift for tier N ≥ 2) that invoke real GLib/GObject/Gio
# functionality through the wrappers and check the results at runtime.
#
# Usage: scripts/smoke-test.sh [--fresh]
# Usage: scripts/smoke-test.sh [tier] [--fresh]
#
# Output goes to ${TMPDIR:-/tmp}/swift-gtk-gen-smoke/tier-1 (a stable path so
# tier 1..6 (default 1). tier1 = GLib+GObject, tier2 = +GModule+Gio, …
# --fresh wipe the tier's output directory before generating
#
# Output goes to ${TMPDIR:-/tmp}/swift-gtk-gen-smoke/tier-<N> (a stable path so
# incremental swift builds stay fast between runs).
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUT="${TMPDIR:-/tmp}/swift-gtk-gen-smoke/tier-1"
CONFIG="$ROOT/configs/tier1.toml"
[[ "${1:-}" == "--fresh" ]] && rm -rf "$OUT"
TIER="1"
FRESH=0
for arg in "$@"; do
if [[ "$arg" == "--fresh" ]]; then
FRESH=1
else
TIER="$arg"
fi
done
CONFIG="$ROOT/configs/tier${TIER}.toml"
OUT="${TMPDIR:-/tmp}/swift-gtk-gen-smoke/tier-${TIER}"
if [[ ! -f "$CONFIG" ]]; then
echo "!! missing $CONFIG" >&2
exit 2
fi
[[ "$FRESH" == 1 ]] && rm -rf "$OUT"
mkdir -p "$OUT"
echo "==> Building generator"
swift build --package-path "$ROOT"
BIN="$(swift build --package-path "$ROOT" --show-bin-path)/swift-gtk-gen"
echo "==> Generating bindings (with SmokeTests target) into $OUT"
echo "==> Generating tier $TIER bindings (with SmokeTests target) into $OUT"
"$BIN" --monorepo-config "$CONFIG" --output "$OUT" --smoke-target >"$OUT/generate.log" 2>&1
echo "==> Installing smoke tests"
@ -33,6 +52,11 @@ mkdir -p "$OUT/Tests/SmokeTests"
# Refresh so edits to smoke/*.swift always take effect.
rm -f "$OUT/Tests/SmokeTests/"*.swift
cp "$ROOT/smoke/"*.swift "$OUT/Tests/SmokeTests/"
# Tier-specific smoke cases (e.g. smoke/tier2/*.swift exercises Gio) are only
# installed — and only need to compile — for tiers that generate their deps.
if [[ -d "$ROOT/smoke/tier${TIER}" ]]; then
cp "$ROOT/smoke/tier${TIER}/"*.swift "$OUT/Tests/SmokeTests/"
fi
echo "==> Running smoke tests against the real C libraries"
swift test --package-path "$OUT"

View file

@ -0,0 +1,51 @@
// InterfaceSmoke.swift
// Tier-2-only runtime smoke tests for Gio interface value construction
// (Phase E1). Proves against the REAL libgio, linked at runtime that:
// 1. a C call returning an interface-typed value (`GFile*` via
// `g_vfs_get_file_for_path`) is bound as `any File`, backed by the
// concrete `FileRef` wrapper (`init(retaining:)` / `init(takingOwnership:)`
// / `isolated deinit` around `g_object_ref`/`g_object_unref`);
// 2. calling an interface method through that value (`File.getPath()`, a
// protocol-extension DEFAULT implementation dispatching through
// `self.pointer`) reaches the real C symbol and returns the correct
// result.
//
// 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 2 (Gio's module).
import Testing
import GLib
import GObject
import Gio
@Suite("Tier 2 interface smoke tests")
struct InterfaceSmokeTests {
@Test("Vfs.getFileForPath returns an any File backed by FileRef, and getPath() round-trips through the real C call")
func fileInterfaceValueConstructionAndDispatch() throws {
let vfs = Vfs.getDefault()
let file: File = vfs.getFileForPath(path: "/tmp/x")
// Interface value construction: `any File` was constructed from a raw
// C pointer via the concrete `FileRef` wrapper this compiles and
// runs only because Phase E1 gives interfaces a constructible type.
#expect(file is FileRef)
// Interface method dispatch: `getPath()` is a protocol-extension
// default implementation, not a witness on a concrete class it
// must still reach `g_file_get_path` through `self.pointer` and
// return the real path.
#expect(file.getPath() == "/tmp/x")
}
@Test("File.getUri() also dispatches through the protocol extension default")
func fileInterfaceSecondMethodDispatch() throws {
let vfs = Vfs.getDefault()
let file = vfs.getFileForPath(path: "/tmp/y")
// Any URI Gio assigns for a local path always carries the path
// itself, proving the call reached the real GVfs/GFile machinery
// rather than returning a stub/placeholder.
#expect(file.getUri().contains("/tmp/y"))
}
}