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:
parent
8698d48e1c
commit
164f8b7b9a
7 changed files with 320 additions and 14 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue