Complete Phase E4 tier-5 Gtk pointer-hiding and smoke coverage
Hides pointer storage/init behind @_spi(SGTKInternal) across boxed records and interface Ref wrappers, marks interface Ref classes @MainActor, threads boxed-record hasCopyFunction through GValue ops, and qualifies re-exported dependency imports @_spi(SGTKInternal). Adds the tier-5 GtkSmoke display-free runtime smoke suite.
This commit is contained in:
parent
dac435ec89
commit
c20015be48
11 changed files with 180 additions and 12 deletions
|
|
@ -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 `<c:include name="gtk/gtk.h"/>` 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 <gtk/gtkunixprint.h>"
|
||||
}
|
||||
|
||||
// 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)\"])]"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))"))
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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<GError>)"))
|
||||
}
|
||||
|
||||
@Test("Boxed signal param wraps via retaining: when a copy function exists, takingOwnership: when it doesn't")
|
||||
func boxedSignalParamWrapperSelection() throws {
|
||||
let withCopy = Signal(
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
import Testing
|
||||
|
||||
import GLib
|
||||
import GObject
|
||||
@_spi(SGTKInternal) import GObject
|
||||
|
||||
@Suite("Runtime smoke tests")
|
||||
struct SmokeTests {
|
||||
|
|
|
|||
55
smoke/tier5/GtkSmoke.swift
Normal file
55
smoke/tier5/GtkSmoke.swift
Normal file
|
|
@ -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 <tier>` copies every `smoke/*.swift`
|
||||
// plus `smoke/tier<N>/*.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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue