From 4f1016f682f876e945124653a18429b606d03511 Mon Sep 17 00:00:00 2001 From: Brendan Szymanski Date: Tue, 21 Jul 2026 00:24:22 -0400 Subject: [PATCH] Emit isolated deinit on root class wrappers, fix five review findings --- Sources/SwiftGtkGenCore/BindingPlan.swift | 12 ++ Sources/SwiftGtkGenCore/PlanRenderer.swift | 15 +- Sources/SwiftGtkGenCore/Planner.swift | 4 +- Sources/SwiftGtkGenCore/TypeMapper.swift | 34 +++-- Sources/SwiftGtkGenCore/XMLParser.swift | 3 +- .../CallbackGenerationTests.swift | 42 ++++++ .../ParserSemanticsTests.swift | 15 ++ .../PropertyGenerationTests.swift | 23 +++ .../TypeMapperTests.swift | 41 +++++- docs/skip-baseline/tier1/GLib.json | 138 +++++++++--------- docs/skip-baseline/tier1/GObject.json | 26 ++-- smoke/SmokeTests.swift | 46 +++++- 12 files changed, 288 insertions(+), 111 deletions(-) diff --git a/Sources/SwiftGtkGenCore/BindingPlan.swift b/Sources/SwiftGtkGenCore/BindingPlan.swift index e6e7066..02c9738 100644 --- a/Sources/SwiftGtkGenCore/BindingPlan.swift +++ b/Sources/SwiftGtkGenCore/BindingPlan.swift @@ -156,6 +156,9 @@ public enum SkipReason: String, Codable, CaseIterable, Sendable { case inoutParameter /// A C array parameter or return has no length annotation. case arrayWithoutLength + /// A C array has a usable length annotation, but array bridging is + /// not yet implemented. + case arrayBridgingUnimplemented /// The type involves a container (GList, GSList, GHashTable, …) that is /// not yet bridged. case containerType @@ -529,6 +532,13 @@ public struct ClassPlan: Equatable, Sendable { /// non-`GObject` fundamental type hierarchy (e.g. `GParamSpec`'s /// `g_param_spec_ref_sink` — see `TypeRegistry.refUnrefFunctions`). public let refFunc: String + /// The C function that releases a reference on this class's underlying + /// pointer, called from the root class's `isolated deinit`. + /// `"g_object_unref"` for ordinary `GObject`-derived classes; + /// overridden for classes that root their own non-`GObject` fundamental + /// type hierarchy (e.g. `GParamSpec`'s `g_param_spec_unref` — see + /// `TypeRegistry.refUnrefFunctions`). + public let unrefFunc: String /// Planned constructors (empty for abstract classes). public let constructors: [CallablePlan] /// Planned instance methods. @@ -550,6 +560,7 @@ public struct ClassPlan: Equatable, Sendable { getTypeFunction: String? = nil, descendsFromInitiallyUnowned: Bool = false, refFunc: String = "g_object_ref", + unrefFunc: String = "g_object_unref", interfaces: [String] = [], constructors: [CallablePlan] = [], methods: [CallablePlan] = [], functions: [CallablePlan] = [], properties: [PropertyPlan] = [], @@ -561,6 +572,7 @@ public struct ClassPlan: Equatable, Sendable { self.getTypeFunction = getTypeFunction self.descendsFromInitiallyUnowned = descendsFromInitiallyUnowned self.refFunc = refFunc + self.unrefFunc = unrefFunc self.interfaces = interfaces self.constructors = constructors; self.methods = methods self.functions = functions; self.properties = properties diff --git a/Sources/SwiftGtkGenCore/PlanRenderer.swift b/Sources/SwiftGtkGenCore/PlanRenderer.swift index e0bd011..542a181 100644 --- a/Sources/SwiftGtkGenCore/PlanRenderer.swift +++ b/Sources/SwiftGtkGenCore/PlanRenderer.swift @@ -740,10 +740,17 @@ private func renderClass(_ plan: ClassPlan) -> String { lines.append("") } - // deinit — skipped until Phase C (ownership semantics). - // A naive g_object_unref here would double-free if the C layer - // already released its reference. Proper ref-counting requires - // tracking transfer ownership from every C call path. + // deinit — releases the ref taken/adopted in the inits above. `isolated` + // runs on the package default actor (MainActor), required to touch the + // non-Sendable `pointer` under strict concurrency. Emitted only on the + // root (which owns `pointer`); subclasses inherit it. + if isRoot { + let unrefArg = plan.unrefFunc == "g_object_unref" ? "pointer" : "_instancePointer(pointer)" + lines.append(" isolated deinit {") + lines.append(" \(plan.unrefFunc)(\(unrefArg))") + lines.append(" }") + lines.append("") + } // ── Members: constructors, methods, static functions ── for ctor in plan.constructors { diff --git a/Sources/SwiftGtkGenCore/Planner.swift b/Sources/SwiftGtkGenCore/Planner.swift index 3b2d225..afbcfc9 100644 --- a/Sources/SwiftGtkGenCore/Planner.swift +++ b/Sources/SwiftGtkGenCore/Planner.swift @@ -161,6 +161,7 @@ public func planModules( getTypeFunction: plan.getTypeFunction, descendsFromInitiallyUnowned: plan.descendsFromInitiallyUnowned, refFunc: plan.refFunc, + unrefFunc: plan.unrefFunc, interfaces: plan.interfaces, constructors: plan.constructors, methods: filteredMethods, functions: plan.functions, properties: filteredProps, @@ -953,7 +954,7 @@ func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberS let isOpen = registry.subclassedTypes().contains(girName) let descendsIU = registry.descendsFromInitiallyUnowned(girName) - let refFunc = registry.refUnrefFunctions(for: girName).ref + let (refFunc, unrefFunc) = registry.refUnrefFunctions(for: girName) // Resolve implemented interfaces to their Swift names, excluding any // already provided by an ancestor class (redundant conformance is a @@ -1048,6 +1049,7 @@ func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberS getTypeFunction: klass.getTypeFunction, descendsFromInitiallyUnowned: descendsIU, refFunc: refFunc, + unrefFunc: unrefFunc, interfaces: interfaceNames, constructors: constructorPlans, methods: methodPlans, diff --git a/Sources/SwiftGtkGenCore/TypeMapper.swift b/Sources/SwiftGtkGenCore/TypeMapper.swift index 7c40640..21d762b 100644 --- a/Sources/SwiftGtkGenCore/TypeMapper.swift +++ b/Sources/SwiftGtkGenCore/TypeMapper.swift @@ -153,7 +153,8 @@ private func _mapValue( type: GIRType, transfer: TransferOwnership, context: MapContext, - cType: String = "" + cType: String = "", + seen: Set = [] ) throws(MapError) -> Mapping { switch type { @@ -189,7 +190,7 @@ private func _mapValue( detail: "va_list parameters are permanently unbridgeable") case .typeRef(let name, let namespace): return try mapTypeRef(name: name, namespace: namespace ?? context.currentNamespace, - transfer: transfer, context: context) + transfer: transfer, context: context, seen: seen) case .container(let kind, let elements): throw MapError(reason: .containerType, detail: "\(kind.rawValue) container with \(elements.count) element(s) not yet bridged") @@ -198,10 +199,10 @@ private func _mapValue( throw MapError(reason: .arrayWithoutLength, detail: "C array has no length annotation") } - throw MapError(reason: .arrayWithoutLength, + throw MapError(reason: .arrayBridgingUnimplemented, detail: "C array bridging not yet implemented") case .optional(let inner): - let innerMap = try _mapValue(type: inner, transfer: transfer, context: context) + let innerMap = try _mapValue(type: inner, transfer: transfer, context: context, seen: seen) return innerMap.optionalised(nullable: true) } } @@ -237,7 +238,8 @@ private func mapTypeRef( name: String, namespace: String, transfer: TransferOwnership, - context: MapContext + context: MapContext, + seen: Set = [] ) throws(MapError) -> Mapping { guard let resolved = context.registry.resolve(name: name, namespace: namespace) else { throw MapError(reason: .unknownType, @@ -361,23 +363,30 @@ private func mapTypeRef( for param in realParams { let pm: Mapping do { - pm = try _mapValue(type: param.type, transfer: param.transferOwnership, context: context) + pm = try _mapValue(type: param.type, transfer: param.transferOwnership, context: context, seen: seen) } catch { throw MapError(reason: .callbackWithoutUserData, detail: "callback '\(name)' param '\(param.name)' unmappable: \(error.detail)") } - swiftParamTypes.append(pm.swiftType) + // Nullability only widens the Swift-facing closure signature. + // The raw @convention(c) signature must stay C-ABI-representable + // (Optional etc. is not), so cSwiftType is left as-is. + let swiftT = param.isNullable && !pm.swiftType.hasSuffix("?") ? "\(pm.swiftType)?" : pm.swiftType + swiftParamTypes.append(swiftT) cSwiftParamTypes.append(pm.cSwiftType) } // Map return value let retMapping: Mapping + var retSwiftType = "Void" if cb.returnValue.type != .void { do { - retMapping = try _mapValue(type: cb.returnValue.type, transfer: cb.returnValue.transferOwnership, context: context) + retMapping = try _mapValue(type: cb.returnValue.type, transfer: cb.returnValue.transferOwnership, context: context, seen: seen) } catch { throw MapError(reason: .callbackWithoutUserData, detail: "callback '\(name)' return unmappable: \(error.detail)") } + retSwiftType = cb.returnValue.isNullable && !retMapping.swiftType.hasSuffix("?") + ? "\(retMapping.swiftType)?" : retMapping.swiftType } else { retMapping = .voidMapping } @@ -389,7 +398,7 @@ private func mapTypeRef( // Build closure type strings — avoid double-parens when params are empty let swiftParamsStr = swiftParamTypes.isEmpty ? "" : swiftParamTypes.joined(separator: ", ") let cSwiftParamsStr = cSwiftParamTypes.isEmpty ? "" : cSwiftParamTypes.joined(separator: ", ") - let swiftRet = retMapping.swiftType == "Void" ? "Void" : retMapping.swiftType + let swiftRet = retSwiftType == "Void" ? "Void" : retSwiftType let cSwiftRet = retMapping.cSwiftType == "Void" ? "Void" : retMapping.cSwiftType let swiftType = "(\(swiftParamsStr)) -> \(swiftRet)" let cSwiftType = "@convention(c) (\(cSwiftParamsStr)) -> \(cSwiftRet)" @@ -401,7 +410,12 @@ private func mapTypeRef( return mapped case .alias(let target): - return try _mapValue(type: target, transfer: transfer, context: context) + guard !seen.contains(resolved.girName) else { + throw MapError(reason: .unknownType, + detail: "cyclic type alias '\(resolved.girName)'") + } + return try _mapValue(type: target, transfer: transfer, context: context, + seen: seen.union([resolved.girName])) } } diff --git a/Sources/SwiftGtkGenCore/XMLParser.swift b/Sources/SwiftGtkGenCore/XMLParser.swift index 7c553c4..e75a28f 100644 --- a/Sources/SwiftGtkGenCore/XMLParser.swift +++ b/Sources/SwiftGtkGenCore/XMLParser.swift @@ -378,7 +378,8 @@ final class GIRXMLDelegate: NSObject, XMLParserDelegate { info: ArrayInfo( lengthParameterIndex: attributeDict["length"].flatMap(Int.init), fixedSize: attributeDict["fixed-size"].flatMap(Int.init), - isZeroTerminated: attributeDict["zero-terminated"] == "1", + isZeroTerminated: attributeDict["zero-terminated"].map { $0 == "1" } + ?? (attributeDict["length"] == nil && attributeDict["fixed-size"] == nil), cType: attributeDict["c:type"] ?? "" ) ))) diff --git a/Tests/SwiftGtkGenCoreTests/CallbackGenerationTests.swift b/Tests/SwiftGtkGenCoreTests/CallbackGenerationTests.swift index 8d2f5c2..e6e2c4e 100644 --- a/Tests/SwiftGtkGenCoreTests/CallbackGenerationTests.swift +++ b/Tests/SwiftGtkGenCoreTests/CallbackGenerationTests.swift @@ -81,6 +81,48 @@ struct CallbackGenerationTests { #expect(entry.reason == .callbackWithoutUserData) } + // MARK: - Parameter/return nullability (review finding #2) + + @Test("Callback param and return nullability propagate into the mapped closure type") + func callbackNullabilityPropagates() throws { + let glib = Repository(namespaces: [ + Namespace(name: "GLib", version: "2.0", + callbacks: [Callback( + name: "NullableFunc", cType: "GNullableFunc", + parameters: [ + Parameter(name: "value", type: .string, cType: "const char*", isNullable: true), + ], + returnValue: ReturnValue(type: .string, isNullable: true) + )]) + ]) + let registry = TypeRegistry(repositories: ["GLib": glib]) + let ctx = MapContext(registry: registry, currentModule: "GLib", currentNamespace: "GLib") + let mapping = try map(.typeRef("NullableFunc", namespace: "GLib"), + nullable: false, transfer: .none, context: ctx) + #expect(mapping.swiftType.contains("String?")) + #expect(mapping.swiftType.hasSuffix("-> String?")) + } + + @Test("A non-nullable callback param/return stays non-optional") + func callbackNonNullableStaysNonOptional() throws { + let glib = Repository(namespaces: [ + Namespace(name: "GLib", version: "2.0", + callbacks: [Callback( + name: "PlainFunc", cType: "GPlainFunc", + parameters: [ + Parameter(name: "value", type: .string, cType: "const char*", isNullable: false), + ], + returnValue: ReturnValue(type: .string, isNullable: false) + )]) + ]) + let registry = TypeRegistry(repositories: ["GLib": glib]) + let ctx = MapContext(registry: registry, currentModule: "GLib", currentNamespace: "GLib") + let mapping = try map(.typeRef("PlainFunc", namespace: "GLib"), + nullable: false, transfer: .none, context: ctx) + #expect(!mapping.swiftType.contains("String?")) + #expect(mapping.swiftType.hasSuffix("-> String")) + } + // MARK: - Renderer infrastructure (constructed directly — D4.3 render-side proof) /// A `.callbackBox(scope: .call, …)` parameter that doubles as its own diff --git a/Tests/SwiftGtkGenCoreTests/ParserSemanticsTests.swift b/Tests/SwiftGtkGenCoreTests/ParserSemanticsTests.swift index 45c2c0b..80ab929 100644 --- a/Tests/SwiftGtkGenCoreTests/ParserSemanticsTests.swift +++ b/Tests/SwiftGtkGenCoreTests/ParserSemanticsTests.swift @@ -255,6 +255,11 @@ struct ParserSemanticsTests { + + + + + @@ -282,6 +287,16 @@ struct ParserSemanticsTests { return } #expect(fixedInfo.fixedSize == 4) + + // No zero-terminated/length/fixed-size attribute at all: GIR omits + // zero-terminated for a plain NULL-terminated array (e.g. GStrv), so + // absence must default to true — not false. + guard case .cArray(_, let absentInfo) = params[4].type else { + Issue.record("expected a C array for 'argv'") + return + } + #expect(absentInfo.isZeroTerminated) + #expect(absentInfo.hasKnownLength) } @Test("Container element types are captured") diff --git a/Tests/SwiftGtkGenCoreTests/PropertyGenerationTests.swift b/Tests/SwiftGtkGenCoreTests/PropertyGenerationTests.swift index fc9ffd3..bd6bd0e 100644 --- a/Tests/SwiftGtkGenCoreTests/PropertyGenerationTests.swift +++ b/Tests/SwiftGtkGenCoreTests/PropertyGenerationTests.swift @@ -129,6 +129,29 @@ struct PropertyGenerationTests { #expect(source.contains(" public var length: Int32")) } + @Test("A root class's isolated deinit unrefs its pointer via g_object_unref") + func rootClassEmitsUnrefDeinit() throws { + let prop = Property(name: "length", type: .int32, isReadable: true, isWritable: false) + let (source, plan, _) = renderClass(named: "Buffer", properties: [prop]) + #expect(plan.unrefFunc == "g_object_unref") + #expect(source.contains("isolated deinit {")) + #expect(source.contains("g_object_unref(pointer)")) + } + + @Test("A class rooting a non-GObject fundamental unrefs through _instancePointer") + func fundamentalRootClassUnrefsTypedPointer() throws { + let plan = ClassPlan( + name: "ParamSpec", girName: "GObject.ParamSpec", cType: "GParamSpec", + getTypeFunction: "g_param_spec_get_type", + refFunc: "g_param_spec_ref", unrefFunc: "g_param_spec_unref" + ) + let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [], + coverage: CoverageStats()) + let source = renderModule(module)["ParamSpec.swift"] ?? "" + #expect(source.contains("isolated deinit {")) + #expect(source.contains("g_param_spec_unref(_instancePointer(pointer))")) + } + @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/TypeMapperTests.swift b/Tests/SwiftGtkGenCoreTests/TypeMapperTests.swift index b91734b..9427e30 100644 --- a/Tests/SwiftGtkGenCoreTests/TypeMapperTests.swift +++ b/Tests/SwiftGtkGenCoreTests/TypeMapperTests.swift @@ -376,15 +376,40 @@ struct TypeMapperTests { @Test("Alias follows to underlying type") func aliasFollowsToUnderlying() throws { let ctx = makeContext() - // GLib.Strv is an alias for a zero-terminated string array. - // Without array bridging, this throws .arrayWithoutLength — but the - // alias is *followed*, which is what this test asserts. + // GLib.Strv is an alias for a zero-terminated string array, which has + // a known length (zero-termination) but array bridging itself isn't + // implemented yet — .arrayBridgingUnimplemented, not + // .arrayWithoutLength. The alias is *followed*, which is what this + // test asserts. do { _ = try map(.typeRef("Strv", namespace: "GLib"), nullable: false, transfer: .none, context: ctx) - Issue.record("Expected arrayWithoutLength error") + Issue.record("Expected arrayBridgingUnimplemented error") } catch { - #expect(error.reason == .arrayWithoutLength) + #expect(error.reason == .arrayBridgingUnimplemented) + } + } + + @Test("Cyclic type alias throws instead of overflowing the stack") + func cyclicAliasThrows() throws { + let cyclic = Repository(namespaces: [ + Namespace( + name: "Cyclic", version: "1.0", + aliases: [ + Alias(name: "A", cType: "CyclicA", target: .typeRef("B", namespace: "Cyclic")), + Alias(name: "B", cType: "CyclicB", target: .typeRef("A", namespace: "Cyclic")), + ] + ) + ]) + let registry = TypeRegistry(repositories: ["Cyclic": cyclic]) + let ctx = MapContext(registry: registry, currentModule: "Cyclic", currentNamespace: "Cyclic") + do { + _ = try map(.typeRef("A", namespace: "Cyclic"), + nullable: false, transfer: .none, context: ctx) + Issue.record("Expected unknownType error from cyclic alias detection") + } catch { + #expect(error.reason == .unknownType) + #expect(error.detail.contains("cyclic")) } } @@ -425,15 +450,15 @@ struct TypeMapperTests { } } - @Test("cArray with length throws arrayWithoutLength (not yet bridged)") + @Test("cArray with length throws arrayBridgingUnimplemented (not yet bridged)") func cArrayWithLengthNotYetBridged() { let ctx = makeContext() do { _ = try map(.cArray(.int32, ArrayInfo(lengthParameterIndex: 0)), nullable: false, transfer: .none, context: ctx) - Issue.record("Expected arrayWithoutLength error") + Issue.record("Expected arrayBridgingUnimplemented error") } catch { - #expect(error.reason == .arrayWithoutLength) + #expect(error.reason == .arrayBridgingUnimplemented) } } diff --git a/docs/skip-baseline/tier1/GLib.json b/docs/skip-baseline/tier1/GLib.json index 057835f..dff5ec7 100644 --- a/docs/skip-baseline/tier1/GLib.json +++ b/docs/skip-baseline/tier1/GLib.json @@ -705,7 +705,7 @@ { "cIdentifier" : "g_base64_decode", "detail" : "C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.base64_decode" }, { @@ -723,7 +723,7 @@ { "cIdentifier" : "g_base64_encode", "detail" : "parameter 'data': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.base64_encode" }, { @@ -735,7 +735,7 @@ { "cIdentifier" : "g_base64_encode_step", "detail" : "parameter 'in': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.base64_encode_step" }, { @@ -788,8 +788,8 @@ }, { "cIdentifier" : "g_build_filenamev", - "detail" : "parameter 'args': C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "parameter 'args': C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.build_filenamev" }, { @@ -800,8 +800,8 @@ }, { "cIdentifier" : "g_build_pathv", - "detail" : "parameter 'args': C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "parameter 'args': C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.build_pathv" }, { @@ -969,19 +969,19 @@ { "cIdentifier" : "g_compute_checksum_for_data", "detail" : "parameter 'data': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.compute_checksum_for_data" }, { "cIdentifier" : "g_compute_hmac_for_data", "detail" : "parameter 'key': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.compute_hmac_for_data" }, { "cIdentifier" : "g_compute_hmac_for_string", "detail" : "parameter 'key': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.compute_hmac_for_string" }, { @@ -993,13 +993,13 @@ { "cIdentifier" : "g_convert", "detail" : "parameter 'str': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.convert" }, { "cIdentifier" : "g_convert_with_fallback", "detail" : "parameter 'str': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.convert_with_fallback" }, { @@ -1220,20 +1220,20 @@ }, { "cIdentifier" : "g_environ_getenv", - "detail" : "parameter 'envp': C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "parameter 'envp': C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.environ_getenv" }, { "cIdentifier" : "g_environ_setenv", - "detail" : "parameter 'envp': C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "parameter 'envp': C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.environ_setenv" }, { "cIdentifier" : "g_environ_unsetenv", - "detail" : "parameter 'envp': C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "parameter 'envp': C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.environ_unsetenv" }, { @@ -1251,7 +1251,7 @@ { "cIdentifier" : "g_file_get_contents", "detail" : "out-param 'contents': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.file_get_contents" }, { @@ -1263,13 +1263,13 @@ { "cIdentifier" : "g_file_set_contents", "detail" : "parameter 'contents': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.file_set_contents" }, { "cIdentifier" : "g_file_set_contents_full", "detail" : "parameter 'contents': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.file_set_contents_full" }, { @@ -1322,32 +1322,32 @@ }, { "cIdentifier" : "g_get_environ", - "detail" : "C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.get_environ" }, { "cIdentifier" : "g_get_filename_charsets", - "detail" : "out-param 'filename_charsets': C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "out-param 'filename_charsets': C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.get_filename_charsets" }, { "cIdentifier" : "g_get_language_names", - "detail" : "C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.get_language_names" }, { "cIdentifier" : "g_get_language_names_with_category", - "detail" : "C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.get_language_names_with_category" }, { "cIdentifier" : "g_get_locale_variants", - "detail" : "C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.get_locale_variants" }, { @@ -1358,14 +1358,14 @@ }, { "cIdentifier" : "g_get_system_config_dirs", - "detail" : "C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.get_system_config_dirs" }, { "cIdentifier" : "g_get_system_data_dirs", - "detail" : "C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.get_system_data_dirs" }, { @@ -1640,20 +1640,20 @@ }, { "cIdentifier" : "g_listenv", - "detail" : "C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.listenv" }, { "cIdentifier" : "g_locale_from_utf8", "detail" : "C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.locale_from_utf8" }, { "cIdentifier" : "g_locale_to_utf8", "detail" : "parameter 'opsysstring': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.locale_to_utf8" }, { @@ -1701,7 +1701,7 @@ { "cIdentifier" : "g_log_structured_array", "detail" : "parameter 'fields': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.log_structured_array" }, { @@ -1719,7 +1719,7 @@ { "cIdentifier" : "g_log_writer_default", "detail" : "parameter 'fields': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.log_writer_default" }, { @@ -1737,25 +1737,25 @@ { "cIdentifier" : "g_log_writer_format_fields", "detail" : "parameter 'fields': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.log_writer_format_fields" }, { "cIdentifier" : "g_log_writer_journald", "detail" : "parameter 'fields': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.log_writer_journald" }, { "cIdentifier" : "g_log_writer_standard_streams", "detail" : "parameter 'fields': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.log_writer_standard_streams" }, { "cIdentifier" : "g_log_writer_syslog", "detail" : "parameter 'fields': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.log_writer_syslog" }, { @@ -2277,7 +2277,7 @@ { "cIdentifier" : "g_shell_parse_argv", "detail" : "out-param 'argvp': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.shell_parse_argv" }, { @@ -2360,8 +2360,8 @@ }, { "cIdentifier" : "g_spawn_command_line_sync", - "detail" : "out-param 'standard_output': C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "out-param 'standard_output': C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.spawn_command_line_sync" }, { @@ -2462,8 +2462,8 @@ }, { "cIdentifier" : "g_strdupv", - "detail" : "parameter 'str_array': C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "parameter 'str_array': C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.strdupv" }, { @@ -2474,8 +2474,8 @@ }, { "cIdentifier" : "g_strfreev", - "detail" : "parameter 'str_array': C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "parameter 'str_array': C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.strfreev" }, { @@ -2516,14 +2516,14 @@ }, { "cIdentifier" : "g_strsplit", - "detail" : "C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.strsplit" }, { "cIdentifier" : "g_strsplit_set", - "detail" : "parameter 'delimiters': C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "parameter 'delimiters': C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.strsplit_set" }, { @@ -2534,14 +2534,14 @@ }, { "cIdentifier" : "g_strv_contains", - "detail" : "parameter 'strv': C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "parameter 'strv': C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.strv_contains" }, { "cIdentifier" : "g_strv_equal", - "detail" : "parameter 'strv1': C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "parameter 'strv1': C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.strv_equal" }, { @@ -2552,8 +2552,8 @@ }, { "cIdentifier" : "g_strv_length", - "detail" : "parameter 'str_array': C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "parameter 'str_array': C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.strv_length" }, { @@ -2865,13 +2865,13 @@ { "cIdentifier" : "g_ucs4_to_utf16", "detail" : "parameter 'str': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.ucs4_to_utf16" }, { "cIdentifier" : "g_ucs4_to_utf8", "detail" : "parameter 'str': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.ucs4_to_utf8" }, { @@ -2895,7 +2895,7 @@ { "cIdentifier" : "g_unicode_canonical_ordering", "detail" : "parameter 'string': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.unicode_canonical_ordering" }, { @@ -3039,13 +3039,13 @@ { "cIdentifier" : "g_utf16_to_ucs4", "detail" : "parameter 'str': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.utf16_to_ucs4" }, { "cIdentifier" : "g_utf16_to_utf8", "detail" : "parameter 'str': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.utf16_to_utf8" }, { @@ -3081,13 +3081,13 @@ { "cIdentifier" : "g_utf8_validate", "detail" : "parameter 'str': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.utf8_validate" }, { "cIdentifier" : "g_utf8_validate_len", "detail" : "parameter 'str': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GLib.utf8_validate_len" }, { diff --git a/docs/skip-baseline/tier1/GObject.json b/docs/skip-baseline/tier1/GObject.json index 2413249..98664ed 100644 --- a/docs/skip-baseline/tier1/GObject.json +++ b/docs/skip-baseline/tier1/GObject.json @@ -105,7 +105,7 @@ { "cIdentifier" : "g_object_newv", "detail" : "parameter 'parameters': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GObject.Object.newv" }, { @@ -518,8 +518,8 @@ }, { "cIdentifier" : "g_enum_register_static", - "detail" : "parameter 'const_static_values': C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "parameter 'const_static_values': C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GObject.enum_register_static" }, { @@ -548,14 +548,14 @@ }, { "cIdentifier" : "g_flags_register_static", - "detail" : "parameter 'const_static_values': C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "parameter 'const_static_values': C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GObject.flags_register_static" }, { "cIdentifier" : "g_object_getv", "detail" : "parameter 'names': C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GObject.getv" }, { @@ -722,14 +722,14 @@ }, { "cIdentifier" : "g_type_module_register_enum", - "detail" : "parameter 'const_static_values': C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "parameter 'const_static_values': C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GObject.register_enum" }, { "cIdentifier" : "g_type_module_register_flags", - "detail" : "parameter 'const_static_values': C array has no length annotation", - "reason" : "arrayWithoutLength", + "detail" : "parameter 'const_static_values': C array bridging not yet implemented", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GObject.register_flags" }, { @@ -813,7 +813,7 @@ { "cIdentifier" : "g_signal_list_ids", "detail" : "C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GObject.signal_list_ids" }, { @@ -927,7 +927,7 @@ { "cIdentifier" : "g_type_children", "detail" : "C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GObject.type_children" }, { @@ -1029,7 +1029,7 @@ { "cIdentifier" : "g_type_interfaces", "detail" : "C array bridging not yet implemented", - "reason" : "arrayWithoutLength", + "reason" : "arrayBridgingUnimplemented", "symbol" : "GObject.type_interfaces" }, { diff --git a/smoke/SmokeTests.swift b/smoke/SmokeTests.swift index c95a6e6..7c5c735 100644 --- a/smoke/SmokeTests.swift +++ b/smoke/SmokeTests.swift @@ -52,13 +52,49 @@ struct SmokeTests { #expect(group.getData(key: "smoke") == marker) } + // MARK: - Object ownership (review finding #1: isolated deinit balances + // init(retaining:)/init(takingOwnership:)) + // + // An over-release (double-free) from an unbalanced deinit aborts the + // process deterministically inside a large alloc/drop loop; a correctly + // balanced release completes cleanly. This is the runtime proof that the + // generated `isolated deinit` unrefs exactly once per construction. + + @Test("GObject wrapper deinit balances its ref across many alloc/drop cycles") + func objectOwnershipLoop() { + for _ in 0..<200_000 { + let group = BindingGroup() + _ = group.isFloating() + } + } + // MARK: - Boxed record memory management (C4: init(retaining:) + isolated deinit) // - // These prove the copy/free functions the planner resolves are correct at - // runtime: a wrong copy (aliasing instead of duplicating) or a wrong free - // (double-free / freeing a borrowed pointer) corrupts the heap and aborts - // the process once enough alloc/free cycles run. The loops make such a bug - // deterministic rather than intermittent. + // NOTE: no tier-1 boxed record (VariantType, Bytes, MainLoop, …) currently + // has a bound "new"-style constructor — every GIR `` for a + // boxed record is presently unbound (a pre-existing gap, separate from + // this plan's scope: TypeMapper handles typeRefs/callbacks/arrays/aliases, + // not why boxed-record constructors are skipped by the planner). Without a + // real pointer there is no way to exercise copy/free at runtime without + // fabricating one, which would test nothing. `RecordGenerationTests` + // (`rendersMemoryManagement`) already locks in the render-time contract + // (copy/free function resolution + `isolated deinit` emission); a runtime + // copy/free loop belongs here once a boxed-record constructor is bound. + + // MARK: - Signal box release (review finding #5: destroy-notify balance) + // + // A wrong Unmanaged retain/release balance in the generated destroy-notify + // trampoline double-frees or leaks the closure box; a double-free aborts + // inside this loop. + + @Test("Signal connect/disconnect box release balances across many cycles") + func signalBoxReleaseLoop() { + for _ in 0..<100_000 { + let group = BindingGroup() + var handler = group.connectNotify(detail: "source") { _, _ in } + handler.disconnect() + } + } @Test("Signal roundtrip: notify::source fires on property change, disconnect prevents re-fire") func notifySignalRoundtrip() {