From 858544cdecb507a913b45b03cde8b2630aea6afa Mon Sep 17 00:00:00 2001 From: Brendan Szymanski Date: Sun, 2 Aug 2026 01:48:47 -0400 Subject: [PATCH] Return signal handler values across the C ABI --- .../GObjectGeneratorCore/PlanRenderer.swift | 88 +++++++++++++++++-- 1 file changed, 83 insertions(+), 5 deletions(-) diff --git a/Sources/GObjectGeneratorCore/PlanRenderer.swift b/Sources/GObjectGeneratorCore/PlanRenderer.swift index fad0a0c..4202a9f 100644 --- a/Sources/GObjectGeneratorCore/PlanRenderer.swift +++ b/Sources/GObjectGeneratorCore/PlanRenderer.swift @@ -867,6 +867,75 @@ private func renderClass(_ plan: ClassPlan) -> String { // ── Signal rendering ── +/// How a signal handler's Swift return value is handed back across the C ABI. +/// +/// `nil` for a void signal. Switches on `marshalIn` - the mapping's +/// Swift-into-C direction, which is exactly the direction a signal return +/// travels - so the spellings match the parameter path in `renderCallable`. +private struct SignalReturnBridge { + /// The `@_cdecl` function's return type. + let cType: String + /// Returned when `data` is nil and the closure never runs. + let zero: String + /// Statements that evaluate `call` and `return` its C form. Emitted + /// inside `MainActor.assumeIsolated`. + let emit: (_ call: String) -> [String] +} + +private func signalReturnBridge(_ m: Mapping) -> SignalReturnBridge? { + let isOptional = m.swiftType.hasSuffix("?") + switch m.marshalIn { + case .boolToGboolean: + return .init(cType: "Int32", zero: "0", emit: { ["return (\($0)) ? 1 : 0"] }) + case .direct: + return .init(cType: m.cSwiftType, zero: "0", emit: { ["return \($0)"] }) + case .numericCast: + return .init(cType: m.cSwiftType, zero: "0", emit: { ["return numericCast(\($0))"] }) + case .enumRaw, .bitfieldRaw: + return .init( + cType: m.cSwiftType, zero: ".init(rawValue: 0)", + emit: { ["return \(m.cSwiftType)(rawValue: numericCast((\($0)).rawValue))"] }) + case .stringToC: + // The signal's return is an owned `char*`; duplicate so the Swift + // String's buffer is not handed to C. + return .init(cType: "UnsafeMutablePointer?", zero: "nil", + emit: { ["return g_strdup(\($0))"] }) + case .objectPointer, .interfacePointer: + // transfer=full (`.objectWrap`/`.interfaceWrap(adopt:)`) means C takes + // ownership of a reference, so add one; transfer=none + // (`.objectRetain`) hands over a borrowed pointer unchanged. + let transfersFull: Bool + switch m.marshalOut { + case .objectRetain: transfersFull = false + default: transfersFull = true + } + let refLine = transfersFull ? ["_ = g_object_ref(r.pointer)"] : [] + // _instancePointer overloads return OpaquePointer or + // UnsafeMutablePointer; neither matches UnsafeMutableRawPointer. + if m.cSwiftType.replacingOccurrences(of: "?", with: "") == "UnsafeMutableRawPointer" { + if isOptional { + return .init(cType: m.cSwiftType, zero: "nil", emit: { call in + ["guard let r = \(call) else { return nil }"] + refLine + + ["return r.pointer"] + }) + } + return .init(cType: m.cSwiftType, zero: "nil", emit: { call in + ["let r = \(call)"] + refLine + ["return r.pointer"] + }) + } + if isOptional { + return .init(cType: m.cSwiftType, zero: "nil", emit: { call in + ["guard let r = \(call) else { return nil }"] + refLine + + ["return _instancePointer(r.pointer)"] + }) + } + return .init(cType: m.cSwiftType, zero: "nil", emit: { call in + ["let r = \(call)"] + refLine + ["return _instancePointer(r.pointer)"] + }) + default: + return nil + } +} /// Renders a `@_cdecl nonisolated` trampoline for a GObject signal. /// /// The `@_cdecl` ABI is required because C calls this function via a raw @@ -914,13 +983,21 @@ private func renderSignalTrampoline(_ plan: SignalPlan) -> [String] { } let wrapperRefs = (0.. \($0.cType)" } ?? "") {") + lines.append(" guard let data else { return \(bridge?.zero ?? "") }") lines.append(" let box = Unmanaged<_ClosureBox<\(closureType)>>.fromOpaque(data).takeUnretainedValue()") lines.append(shadowBody) - lines.append(" MainActor.assumeIsolated {") + if let bridge { + lines.append(" return MainActor.assumeIsolated { () -> \(bridge.cType) in") + } else { + lines.append(" MainActor.assumeIsolated {") + } lines.append(wrapperBody) lines.append(" }") lines.append("}") @@ -987,6 +1064,7 @@ private func renderSignalConnect(_ plan: SignalPlan, className: String) -> [Stri let closureParams = closureParamTypes.isEmpty ? "" : closureParamTypes.joined(separator: ", ") let closureType = "(\(closureParams)) -> \(closureRet)" + let cRet = plan.returnMapping.flatMap(signalReturnBridge)?.cType ?? "Void" // Build @convention(c) type for the trampoline's unsafeBitCast let cTypes = ["UnsafeMutableRawPointer"] + realParams.map { p in p.mapping.cSwiftType.replacingOccurrences(of: "?", with: "") @@ -1014,7 +1092,7 @@ private func renderSignalConnect(_ plan: SignalPlan, className: String) -> [Stri lines.append(" let ptr = self.pointer") lines.append(" \(detailBody)") lines.append(" return signalName.withCString { cName in") - lines.append(" let id = _sgtkSignalConnectData(ptr, cName, unsafeBitCast(\(plan.trampolineCName) as (@convention(c) (\(cTypeStr)) -> Void), to: UnsafeRawPointer.self), dataPtr, unsafeBitCast(destroyFn as (@convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> Void), to: UnsafeRawPointer.self), 0)") + lines.append(" let id = _sgtkSignalConnectData(ptr, cName, unsafeBitCast(\(plan.trampolineCName) as (@convention(c) (\(cTypeStr)) -> \(cRet)), to: UnsafeRawPointer.self), dataPtr, unsafeBitCast(destroyFn as (@convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> Void), to: UnsafeRawPointer.self), 0)") lines.append(" return SignalHandle(id: id, instance: ptr)") lines.append(" }") lines.append(" }")