diff --git a/Sources/SwiftGtkGenCore/CodeGen+Scaffolding.swift b/Sources/SwiftGtkGenCore/CodeGen+Scaffolding.swift index b57e785..4c392fc 100644 --- a/Sources/SwiftGtkGenCore/CodeGen+Scaffolding.swift +++ b/Sources/SwiftGtkGenCore/CodeGen+Scaffolding.swift @@ -136,9 +136,18 @@ extension CodeGenerator { let cName = "C\(moduleName)" let lowerModName = moduleName.lowercased() let cHeader = repo.cHeaderPath - let umbrellaHeader = cHeader.isEmpty + var umbrellaHeader = cHeader.isEmpty ? "#include <\(lowerModName)/\(lowerModName).h>" : "#include <\(cHeader)>" + // GTK4's own `` does not cover the + // `gtk/gtkunixprint.h` header that must be included + // explicitly, even though the GIR declares them as ordinary Gtk + // namespace members. Root-cause fix, not a skip: the C symbols + // are real and exported by libgtk-4.so; only the umbrella header + // GTK's own GIR points at omits them. + if moduleName == "Gtk" { + umbrellaHeader += "\n#include " + } // No `link` directives: the `.systemLibrary` target carries // `pkgConfig:`, so pkg-config `--libs` supplies the exact linker @@ -184,7 +193,11 @@ extension CodeGenerator { var cTargets = "" for name in moduleNames { let cName = "C\(name)" - let pkgConfigName = analysis.repositories[name]?.packageName ?? "" + // Gtk needs `gtk4-unix-print`'s cflags (a strict superset of `gtk4`'s, + // adding only the `-I.../unix-print` search path the umbrella + // header above pulls `gtk/gtkunixprint.h` from) — same libs, same .so. + var pkgConfigName = analysis.repositories[name]?.packageName ?? "" + if name == "Gtk" && pkgConfigName == "gtk4" { pkgConfigName = "gtk4-unix-print" } let pkgConfigArg = pkgConfigName.isEmpty ? "" : ", pkgConfig: \"\(pkgConfigName)\", providers: [.apt([\"\(pkgConfigName)\"]), .brew([\"\(pkgConfigName)\"])]" diff --git a/Sources/SwiftGtkGenCore/CodeGen.swift b/Sources/SwiftGtkGenCore/CodeGen.swift index b2c51e3..e1ace47 100644 --- a/Sources/SwiftGtkGenCore/CodeGen.swift +++ b/Sources/SwiftGtkGenCore/CodeGen.swift @@ -17,7 +17,7 @@ public struct CodeGenerator { guard !transitive.isEmpty else { continue } var content = header for dep in transitive.sorted() { - content += "@_exported import \(dep)\n" + content += "@_spi(SGTKInternal) @_exported import \(dep)\n" } content += "\n" files["Sources/\(moduleName)/\(moduleName).swift"] = content diff --git a/Sources/SwiftGtkGenCore/TypeMapper.swift b/Sources/SwiftGtkGenCore/TypeMapper.swift index 0ea5a9d..7c40640 100644 --- a/Sources/SwiftGtkGenCore/TypeMapper.swift +++ b/Sources/SwiftGtkGenCore/TypeMapper.swift @@ -330,7 +330,7 @@ private func mapTypeRef( swiftType: swiftType, cSwiftType: "UnsafeMutableRawPointer?", marshalIn: .boxedPointer, marshalOut: .boxedWrap(copy: transfer != .full, copyFunction: copyFn), gvalue: GValueOps(typeMacro: "G_TYPE_BOXED", - getterSuffix: "boxed", setterSuffix: "boxed"), + getterSuffix: "boxed", setterSuffix: "boxed", hasCopyFunction: copyFn != nil), category: .needsRecord) case .gtypeStruct: diff --git a/Tests/SwiftGtkGenCoreTests/CallbackGenerationTests.swift b/Tests/SwiftGtkGenCoreTests/CallbackGenerationTests.swift index 239cfb2..8d2f5c2 100644 --- a/Tests/SwiftGtkGenCoreTests/CallbackGenerationTests.swift +++ b/Tests/SwiftGtkGenCoreTests/CallbackGenerationTests.swift @@ -42,8 +42,26 @@ struct CallbackGenerationTests { coverage: CoverageStats()) let files = renderModule(module) let source = files["Callbacks.swift"] ?? "" - #expect(source.contains("public typealias CompareFunc = @convention(c) (UnsafeRawPointer?, UnsafeRawPointer?) -> Int32")) - #expect(source.contains("public typealias CompareFuncSwift = (UnsafeRawPointer?, UnsafeRawPointer?) -> Int32")) + #expect(source.contains("@_spi(SGTKInternal) public typealias CompareFunc = @convention(c) (UnsafeRawPointer?, UnsafeRawPointer?) -> Int32")) + // The Swift-closure form still carries raw UnsafeRawPointer args here, + // so it is hidden behind @_spi too (Phase E4 gpointer policy). + #expect(source.contains("@_spi(SGTKInternal) public typealias CompareFuncSwift = (UnsafeRawPointer?, UnsafeRawPointer?) -> Int32")) + } + + @Test("A pointer-free Swift-closure form stays plain public; only the @convention(c) form is @_spi") + func callbackTypePointerFreeSwiftFormStaysPublic() throws { + let plan = CallbackTypePlan( + name: "NotifyFunc", + swiftType: "(String) -> Void", + cSwiftType: "@convention(c) (UnsafeMutableRawPointer?) -> Void" + ) + let module = ModulePlan(module: "GLib", types: [.callback(plan)], skips: [], + coverage: CoverageStats()) + let files = renderModule(module) + let source = files["Callbacks.swift"] ?? "" + #expect(source.contains("@_spi(SGTKInternal) public typealias NotifyFunc = @convention(c) (UnsafeMutableRawPointer?) -> Void")) + #expect(source.contains("public typealias NotifyFuncSwift = (String) -> Void")) + #expect(!source.contains("@_spi(SGTKInternal) public typealias NotifyFuncSwift")) } // MARK: - Planner baseline (unchanged pending D4.3) diff --git a/Tests/SwiftGtkGenCoreTests/InterfaceConformanceTests.swift b/Tests/SwiftGtkGenCoreTests/InterfaceConformanceTests.swift index d26b895..40aea6e 100644 --- a/Tests/SwiftGtkGenCoreTests/InterfaceConformanceTests.swift +++ b/Tests/SwiftGtkGenCoreTests/InterfaceConformanceTests.swift @@ -80,7 +80,7 @@ struct InterfaceConformanceTests { // Protocol body is bare — only the `pointer` requirement. #expect(ifaceSrc.contains("public protocol TypePlugin {")) - #expect(ifaceSrc.contains("var pointer: UnsafeMutableRawPointer { get }")) + #expect(ifaceSrc.contains("@_spi(SGTKInternal) var pointer: UnsafeMutableRawPointer { get }")) // The interface method is a protocol EXTENSION default, not a // requirement — it keeps its own (Void) signature regardless of the @@ -89,9 +89,10 @@ struct InterfaceConformanceTests { #expect(ifaceSrc.contains("public func use(")) // 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("public final class TypePluginRef: @MainActor TypePlugin {")) + #expect(ifaceSrc.contains("@_spi(SGTKInternal) public let pointer: UnsafeMutableRawPointer")) + #expect(ifaceSrc.contains("@_spi(SGTKInternal) public init(retaining pointer: UnsafeMutableRawPointer)")) + #expect(ifaceSrc.contains("@_spi(SGTKInternal) public init(takingOwnership pointer: UnsafeMutableRawPointer)")) #expect(ifaceSrc.contains("isolated deinit")) } diff --git a/Tests/SwiftGtkGenCoreTests/PropertyGenerationTests.swift b/Tests/SwiftGtkGenCoreTests/PropertyGenerationTests.swift index 24ae06e..fc9ffd3 100644 --- a/Tests/SwiftGtkGenCoreTests/PropertyGenerationTests.swift +++ b/Tests/SwiftGtkGenCoreTests/PropertyGenerationTests.swift @@ -115,6 +115,20 @@ struct PropertyGenerationTests { #expect(!source.contains("g_object_set_property")) } + // MARK: - Phase E4: no-unsafe-pointer public API policy + + @Test("A root class's pointer storage and both instance inits are @_spi(SGTKInternal), the class itself stays public") + func classPointerSurfaceIsSPIGated() throws { + let prop = Property(name: "length", type: .int32, isReadable: true, isWritable: false) + let (source, _, _) = renderClass(named: "Buffer", properties: [prop]) + #expect(source.contains("public class Buffer {")) + #expect(source.contains("@_spi(SGTKInternal) public let pointer: UnsafeMutableRawPointer")) + #expect(source.contains("@_spi(SGTKInternal) public required init(takingOwnership pointer: UnsafeMutableRawPointer)")) + #expect(source.contains("@_spi(SGTKInternal) public init(retaining pointer: UnsafeMutableRawPointer)")) + // The plain-public getter is unaffected — Int32 carries no raw pointer. + #expect(source.contains(" public var length: Int32")) + } + @Test("Writable string property generates both a getter and a setter") func writableStringProperty() throws { let prop = Property(name: "label", type: .string, isReadable: true, isWritable: true) diff --git a/Tests/SwiftGtkGenCoreTests/RecordGenerationTests.swift b/Tests/SwiftGtkGenCoreTests/RecordGenerationTests.swift index 6624bcd..f762358 100644 --- a/Tests/SwiftGtkGenCoreTests/RecordGenerationTests.swift +++ b/Tests/SwiftGtkGenCoreTests/RecordGenerationTests.swift @@ -77,7 +77,9 @@ struct RecordGenerationTests { copyFunction: "g_variant_type_copy", freeFunction: "g_variant_type_free" ) let source = render(plan) - #expect(source.contains("public init(retaining pointer: UnsafeMutableRawPointer)")) + #expect(source.contains("@_spi(SGTKInternal) public let pointer: UnsafeMutableRawPointer")) + #expect(source.contains("@_spi(SGTKInternal) public init(takingOwnership pointer: UnsafeMutableRawPointer)")) + #expect(source.contains("@_spi(SGTKInternal) public init(retaining pointer: UnsafeMutableRawPointer)")) #expect(source.contains("g_variant_type_copy(_instancePointer(pointer))")) #expect(source.contains("isolated deinit {")) #expect(source.contains("g_variant_type_free(_instancePointer(pointer))")) diff --git a/Tests/SwiftGtkGenCoreTests/RendererCallableTests.swift b/Tests/SwiftGtkGenCoreTests/RendererCallableTests.swift index d120dfd..58fb808 100644 --- a/Tests/SwiftGtkGenCoreTests/RendererCallableTests.swift +++ b/Tests/SwiftGtkGenCoreTests/RendererCallableTests.swift @@ -57,6 +57,37 @@ struct RendererCallableTests { #expect(body.contains("result!") || body.contains("marshalReturn")) } + // MARK: - Phase E4: no-unsafe-pointer public API policy + + @Test("A function with a gpointer parameter renders @_spi(SGTKInternal) public func, hiding the pointer") + func gpointerParameterHidesFunctionBehindSPI() throws { + let fn = GlobalFunction( + name: "set_user_data", cIdentifier: "g_set_user_data", + parameters: [Parameter(name: "data", type: .pointer, cType: "gpointer")], + returnValue: ReturnValue(type: .void) + ) + guard case .success(let plan) = planFunction(fn, context: makeContext()) else { + Issue.record("expected set_user_data to plan successfully"); return + } + let body = renderCallable(plan) + #expect(body.contains("@_spi(SGTKInternal) public func setUserData")) + } + + @Test("A function returning a plain wrapper type (no raw pointer in its signature) stays plain public") + func wrapperReturnStaysPlainPublic() throws { + let fn = GlobalFunction( + name: "get_default_object", cIdentifier: "g_get_default_object", + parameters: [], + returnValue: ReturnValue(type: .typeRef("Object", namespace: "GObject")) + ) + guard case .success(let plan) = planFunction(fn, context: makeContext()) else { + Issue.record("expected get_default_object to plan successfully"); return + } + let body = renderCallable(plan) + #expect(body.contains("public func getDefaultObject")) + #expect(!body.contains("@_spi(SGTKInternal) public func getDefaultObject")) + } + // MARK: - C3: Out-param tuples @Test("Single out-param with no Swift return becomes the out-param's type") diff --git a/Tests/SwiftGtkGenCoreTests/SignalGenerationTests.swift b/Tests/SwiftGtkGenCoreTests/SignalGenerationTests.swift index 9f3b2a3..604af3a 100644 --- a/Tests/SwiftGtkGenCoreTests/SignalGenerationTests.swift +++ b/Tests/SwiftGtkGenCoreTests/SignalGenerationTests.swift @@ -130,6 +130,40 @@ struct SignalGenerationTests { #expect(source.contains("_sgtk_signal_connect_data(")) #expect(!source.contains("_sgtk_signal_connect_data(ptr, cName, unsafeBitCast(\\(plan.trampolineCName)")) } + + @Test("Support.swift hides GLibError.init(consuming:), SignalHandle.instance/init, and _sgtk_* helpers behind @_spi(SGTKInternal)") + func supportPointerSurfaceIsSPIGated() throws { + let notify = Signal(name: "notify", isDetailed: true) + let klass = Class(name: "Object", cType: "GObject", parent: nil, + getTypeFunction: "g_object_get_type", signals: [notify]) + let (plan, skips) = planClass(klass, context: makeContext()) + #expect(skips.isEmpty) + let module = ModulePlan(module: "GObject", dependencyModules: ["GLib"], + types: [.class(plan)], skips: [], coverage: CoverageStats()) + let support = renderModule(module)["Support.swift"] ?? "" + #expect(support.contains("@_spi(SGTKInternal) public let instance: UnsafeMutableRawPointer")) + #expect(support.contains("@_spi(SGTKInternal) public init(id: UInt, instance: UnsafeMutableRawPointer)")) + #expect(support.contains("@_spi(SGTKInternal) public nonisolated func _sgtk_destroy_notify_impl(")) + #expect(support.contains("@_spi(SGTKInternal) public nonisolated func _sgtk_signal_connect_data(")) + #expect(support.contains("@_spi(SGTKInternal) public nonisolated func _sgtk_signal_handler_disconnect(")) + // SignalHandle itself and its id/disconnect() stay plain public — only + // the raw-pointer members are hidden. + #expect(support.contains("public struct SignalHandle {")) + #expect(support.contains("public let id: UInt")) + #expect(support.contains("public mutating func disconnect()")) + // Dependency import chokepoint is SPI too. + #expect(support.contains("@_spi(SGTKInternal) import GLib")) + } + + @Test("Support.swift hides GLibError.init(consuming:) behind @_spi(SGTKInternal), struct/fields stay plain public") + func glibErrorConsumingInitIsSPIGated() throws { + let module = ModulePlan(module: "GLib", types: [], skips: [], coverage: CoverageStats()) + let support = renderModule(module)["Support.swift"] ?? "" + #expect(support.contains("public struct GLibError: Swift.Error {")) + #expect(support.contains("public let domain: UInt32")) + #expect(support.contains("@_spi(SGTKInternal) public init(consuming error: UnsafeMutablePointer)")) + } + @Test("Boxed signal param wraps via retaining: when a copy function exists, takingOwnership: when it doesn't") func boxedSignalParamWrapperSelection() throws { let withCopy = Signal( diff --git a/smoke/SmokeTests.swift b/smoke/SmokeTests.swift index b49876b..ec9f769 100644 --- a/smoke/SmokeTests.swift +++ b/smoke/SmokeTests.swift @@ -13,7 +13,7 @@ import Testing import GLib -import GObject +@_spi(SGTKInternal) import GObject @Suite("Runtime smoke tests") struct SmokeTests { diff --git a/smoke/tier5/GtkSmoke.swift b/smoke/tier5/GtkSmoke.swift new file mode 100644 index 0000000..f1bdda1 --- /dev/null +++ b/smoke/tier5/GtkSmoke.swift @@ -0,0 +1,55 @@ +// GtkSmoke.swift +// Tier-5-only runtime smoke test for Gtk (Phase E4). Proves — against the +// REAL libgtk-4, linked at runtime — that the generated bindings reach real +// GTK C symbols and that the no-unsafe-pointer public API policy still +// produces a fully usable, pointer-free surface. +// +// Both cases are deliberately display-free (no `gtk_init`, no +// `realize()`/`present()`, no display/backend connection): +// 1. `getMajorVersion()`/`getMinorVersion()` are plain free-function calls +// into `gtk_get_major_version`/`gtk_get_minor_version` — guaranteed +// display-free, proves Gtk links and the friendly public API is +// callable. +// 2. `Adjustment(value:lower:upper:stepIncrement:pageIncrement:pageSize:)` +// reaches the real `gtk_adjustment_new` C constructor and adopts the +// returned pointer through `init(takingOwnership:)`. `GtkAdjustment` is +// a plain GObject (not a widget — no realize/display dependency) with a +// value round-trip through `getValue()`/`setValue(value:)`. Every call +// in this test uses ONLY plain-public API — no `@_spi` import — which +// is itself part of the proof that the pointer-free surface is complete +// enough for real, non-toy usage. +// +// Not generated. `scripts/smoke-test.sh ` copies every `smoke/*.swift` +// plus `smoke/tier/*.swift` into the generated SmokeTests target before +// running `swift test`. Only installed for tier >= 5 (Gtk's module). + +import Testing + +import GLib +import GObject +import Gtk + +@Suite("Tier 5 Gtk smoke tests") +struct GtkSmokeTests { + @Test("getMajorVersion()/getMinorVersion() reach the real gtk_get_major_version/gtk_get_minor_version and report GTK 4") + func versionFunctionsReachRealGtk() throws { + #expect(getMajorVersion() == 4) + // GTK 4's minor version is a real, non-placeholder value reported by + // the linked library, not a stubbed default. + #expect(getMinorVersion() >= 0) + } + + @Test("Adjustment(...) reaches the real gtk_adjustment_new and constructs a live, display-free GObject") + func adjustmentConstructsAndRoundTrips() throws { + let adjustment = Adjustment(value: 5, lower: 0, upper: 10, stepIncrement: 1, pageIncrement: 2, pageSize: 0) + #expect(adjustment.getValue() == 5) + #expect(adjustment.getLower() == 0) + #expect(adjustment.getUpper() == 10) + + // Property computed-var path (GValue-free — delegates to the real + // getter/setter methods above) round-trips through the real C call. + adjustment.value = 7 + #expect(adjustment.value == 7) + #expect(adjustment.getValue() == 7) + } +}