Tier 2 compile-gate compliance: interfaces, cross-module refs, Gio
This commit is contained in:
parent
5abb48e889
commit
0504b34d7f
18 changed files with 9186 additions and 304 deletions
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }")
|
||||
// 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(" var \(plan.swiftName): \(plan.swiftType) { get }")
|
||||
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)))"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
/// 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: "\(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"
|
||||
symbol: "\(symbolPrefix).\(plan.cIdentifier)",
|
||||
cIdentifier: plan.cIdentifier,
|
||||
reason: .nameCollision,
|
||||
detail: "Swift signature '\(renderedName)(\(signatureKey(plan)))' already emitted by an earlier symbol"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
if filteredMethods.count == plan.methods.count {
|
||||
result.append(.interface(plan))
|
||||
} 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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 ──
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
3213
docs/skip-baseline/tier2/GLib.json
Normal file
3213
docs/skip-baseline/tier2/GLib.json
Normal file
File diff suppressed because it is too large
Load diff
57
docs/skip-baseline/tier2/GModule.json
Normal file
57
docs/skip-baseline/tier2/GModule.json
Normal 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
|
||||
}
|
||||
}
|
||||
1125
docs/skip-baseline/tier2/GObject.json
Normal file
1125
docs/skip-baseline/tier2/GObject.json
Normal file
File diff suppressed because it is too large
Load diff
4136
docs/skip-baseline/tier2/Gio.json
Normal file
4136
docs/skip-baseline/tier2/Gio.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
|
|
|
|||
51
smoke/tier2/InterfaceSmoke.swift
Normal file
51
smoke/tier2/InterfaceSmoke.swift
Normal 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"))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue