1
0
Fork 0

Generate boxed-record memory management and interface requirements

Boxed records (C4): resolve each record's copy/free function from the GIR
copy-function/free-function attribute, else its own ref/copy and unref/free
method, and render init(retaining:) plus an isolated deinit. The deinit is
isolated because the package's default MainActor isolation makes accessing the
non-Sendable pointer from a nonisolated deinit a hard error. The registry uses
the same resolution so transfer=none returns copy instead of adopting a
borrowed pointer. This supersedes the earlier deferral: the hypothesised
OpaquePointer/Sendable barriers either did not hold or were solvable.

Interfaces (C5): plan each interface method through planMethod and render it as
a protocol requirement (no body); the conforming class supplies the C call.
This commit is contained in:
Brendan Szymanski 2026-07-17 21:04:37 -04:00
parent 8698d48e1c
commit 164f8b7b9a
7 changed files with 320 additions and 14 deletions

View file

@ -443,12 +443,17 @@ public struct InterfacePlan: Equatable, Sendable {
public let cType: String
public let prereqs: [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
/// the class, not the interface see `planInterface`).
public let methods: [CallablePlan]
public let doc: String?
public init(name: String, cType: String, prereqs: [String] = [],
getTypeFunction: String? = nil, doc: String? = nil) {
getTypeFunction: String? = nil, methods: [CallablePlan] = [],
doc: String? = nil) {
self.name = name; self.cType = cType; self.prereqs = prereqs
self.getTypeFunction = getTypeFunction; self.doc = doc
self.getTypeFunction = getTypeFunction; self.methods = methods; self.doc = doc
}
}

View file

@ -224,13 +224,38 @@ private func renderRecord(_ plan: RecordPlan) -> String {
if let doc = plan.doc {
lines.append(contentsOf: renderDocComment(doc))
}
lines.append("public final class \(plan.name) {")
lines.append(" public let pointer: UnsafeMutableRawPointer")
lines.append("")
lines.append(" /// Adopts an owned boxed pointer; the wrapper takes responsibility")
lines.append(" /// for freeing it. Use for `transfer-ownership=\"full\"` returns.")
lines.append(" public init(takingOwnership pointer: UnsafeMutableRawPointer) {")
lines.append(" self.pointer = pointer")
lines.append(" }")
// init(retaining:) copies a borrowed pointer so the wrapper owns its own
// instance. Only emitted when a copy/ref function is known.
if let copy = plan.copyFunction {
lines.append("")
lines.append(" /// Copies a borrowed boxed pointer so the wrapper owns an independent")
lines.append(" /// instance. Use for `transfer-ownership=\"none\"` returns.")
lines.append(" public init(retaining pointer: UnsafeMutableRawPointer) {")
lines.append(" self.pointer = _rawPointer(\(copy)(_instancePointer(pointer)))")
lines.append(" }")
}
// deinit frees the owned pointer. `isolated` runs the deinit on the
// type's default actor (MainActor for this package), which is required to
// touch the non-Sendable `pointer` from a deinitialiser under strict
// concurrency. Only emitted when a free/unref function is known; without
// one the wrapper leaks rather than risk freeing with the wrong function.
if let free = plan.freeFunction {
lines.append("")
lines.append(" isolated deinit {")
lines.append(" \(free)(_instancePointer(pointer))")
lines.append(" }")
}
lines.append("}")
return lines.joined(separator: "\n") + "\n"
}
@ -251,10 +276,25 @@ 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))
}
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
}
// Class
private func renderClass(_ plan: ClassPlan) -> String {

View file

@ -194,7 +194,9 @@ private func skipInterface(into skips: inout [SkipEntry], into types: inout [Typ
reason: .notIntrospectable, detail: "non-introspectable interface"))
return 0
}
types.append(.interface(planInterface(iface, context: context)))
let (plan, memberSkips) = planInterface(iface, context: context)
skips.append(contentsOf: memberSkips)
types.append(.interface(plan))
return 1
}
@ -287,35 +289,102 @@ let reservedSwiftTypes: Set<String> = [
"Object", "Type", "Protocol",
]
/// Plans a boxed record as an opaque pointer wrapper.
/// Plans a boxed record as an opaque pointer wrapper, resolving the copy and
/// free functions that give it correct memory management (C4).
func planRecord(_ record: Record, context: MapContext) -> RecordPlan {
return RecordPlan(
name: record.name, cType: record.cType,
getTypeFunction: record.getTypeFunction,
copyFunction: record.copyFunction,
freeFunction: record.freeFunction,
copyFunction: resolvedCopyFunction(record),
freeFunction: resolvedFreeFunction(record),
doc: record.doc
)
}
/// Plans an interface as a Swift protocol stub.
func planInterface(_ iface: Interface, context: MapContext) -> InterfacePlan {
/// Resolves a boxed record's destructor C identifier.
///
/// Prefers the explicit GIR `free-function` attribute; otherwise falls back to
/// the record's own parameterless `unref` (preferred, ref-counted) or `free`
/// method. `g_boxed_free` is deliberately *not* used as a fallback: it lives in
/// GObject, which the GLib module cannot link against (GObject depends on GLib,
/// not the reverse). Returns `nil` when no safe destructor exists the wrapper
/// then renders without a `deinit` (documented leak) rather than risk a wrong
/// free.
func resolvedFreeFunction(_ record: Record) -> String? {
if let explicit = record.freeFunction { return explicit }
return record.instanceReleaseMethod(named: ["unref", "free"])
}
/// Resolves a boxed record's copy/ref C identifier.
///
/// Prefers the explicit GIR `copy-function` attribute; otherwise falls back to
/// the record's own parameterless `ref` (preferred, ref-counted) or `copy`
/// method. Returns `nil` when none exists the wrapper then renders without
/// `init(retaining:)`, and borrowed (`transfer-ownership="none"`) returns of
/// this type must not be adopted as owned.
func resolvedCopyFunction(_ record: Record) -> String? {
if let explicit = record.copyFunction { return explicit }
return record.instanceReleaseMethod(named: ["ref", "copy"])
}
extension Record {
/// The C identifier of the first bindable instance method matching one of
/// `names` (in priority order) that takes no arguments beyond the instance
/// itself the shape every canonical `ref`/`unref`/`copy`/`free` shares.
func instanceReleaseMethod(named names: [String]) -> String? {
for wanted in names {
if let method = methods.first(where: {
$0.name == wanted && $0.symbolInfo.isBindable
&& $0.parameters.allSatisfy(\.isInstanceParameter)
}) {
return method.cIdentifier
}
}
return nil
}
}
/// Plans an interface as a Swift protocol, planning its instance methods as
/// protocol requirements.
///
/// A GObject interface method is dispatched through the implementing class's
/// instance pointer the concrete C symbol (e.g. `gtk_widget_get_buildable_id`)
/// lives on the class, not the interface. So each interface method becomes a
/// protocol *requirement* only; the implementing class supplies the body via
/// its own `planClass` methods. Methods that cannot be planned (unsupported
/// types, varargs, ) are returned as skip entries, mirroring `planClass`.
///
/// - Returns: The interface plan and the skip entries for its unplannable
/// methods.
func planInterface(_ iface: Interface, context: MapContext) -> (plan: InterfacePlan, memberSkips: [SkipEntry]) {
let registry = context.registry
let girName = "\(context.currentNamespace).\(iface.name)"
// Resolve prerequisite types through registry
let prereqSwiftNames: [String] = iface.prereqs.compactMap { prereq in
let qualified = prereq.contains(".") ? prereq : "\(context.currentNamespace).\(prereq)"
guard let resolved = registry.resolve(girName: qualified) else { return nil }
return registry.swiftTypeName(for: resolved, in: context.currentModule)
}
return InterfacePlan(
// Instance methods become protocol requirements. Unplannable ones are
// recorded as skips, matching class member behaviour.
var memberSkips: [SkipEntry] = []
var methodPlans: [CallablePlan] = []
for method in iface.methods where method.symbolInfo.isBindable {
switch planMethod(method, context: context) {
case .success(let plan): methodPlans.append(plan)
case .skip(let entry): memberSkips.append(entry)
}
}
let plan = InterfacePlan(
name: iface.name, cType: iface.cType,
prereqs: prereqSwiftNames,
getTypeFunction: iface.getTypeFunction,
methods: methodPlans,
doc: iface.doc
)
return (plan, memberSkips)
}
/// Plans a class as a GObject wrapper with cross-module inheritance, planning

View file

@ -227,7 +227,11 @@ public struct TypeRegistry: Sendable {
if let forType = rec.isGTypeStructFor {
category = .gtypeStruct(forType: qualify(forType))
} else if rec.isBoxed {
category = .boxedRecord(copyFunction: rec.copyFunction, freeFunction: rec.freeFunction)
// Resolve copy/free the same way the planner does (explicit GIR
// attribute, else the record's own ref/copy/unref/free method)
// so borrowed (transfer="none") returns copy correctly instead
// of adopting a pointer they don't own.
category = .boxedRecord(copyFunction: resolvedCopyFunction(rec), freeFunction: resolvedFreeFunction(rec))
} else {
category = .plainRecord
}