1
0
Fork 0

Return signal handler values across the C ABI

This commit is contained in:
Brendan Szymanski 2026-08-02 01:48:47 -04:00
parent a172d07ab1
commit 858544cdec

View file

@ -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<CChar>?", 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<T>; 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..<allParams.count).map { "w\($0)" }.joined(separator: ", ")
let shadowBody = shadowLines.map { " \($0)" }.joined(separator: "\n")
let wrapperBody = (wrapperLines + ["box.closure(\(wrapperRefs))"]).map { " \($0)" }.joined(separator: "\n")
let bridge = plan.returnMapping.flatMap(signalReturnBridge)
let call = "box.closure(\(wrapperRefs))"
let tail = bridge.map { $0.emit(call) } ?? [call]
let wrapperBody = (wrapperLines + tail).map { " \($0)" }.joined(separator: "\n")
lines.append("@_cdecl(\"\(plan.trampolineCName)\")")
lines.append("nonisolated func \(plan.trampolineCName)(\(cDecl)) {")
lines.append(" guard let data else { return }")
lines.append("nonisolated func \(plan.trampolineCName)(\(cDecl))\(bridge.map { " -> \($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(" }")