Skip to content

Commit a2982f9

Browse files
committed
Rework handling of returns in cdecl thunks to handle more types
Implement a separate "swift to cdecl" conversion path that turns a Swift value into cdecl-compatible return values. This includes returning class and actor references (as unsafe raw pointers), unsafe pointers (also as unsafe raw pointers), and returned tuples (which are passed indirectly).
1 parent 4c15602 commit a2982f9

File tree

8 files changed

+328
-27
lines changed

8 files changed

+328
-27
lines changed

Sources/JExtractSwift/CDeclLowering/CDeclConversions.swift

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,107 @@ extension ConversionStep {
8787
self = .tuplify(try elements.map { try ConversionStep(cdeclToSwift: $0) })
8888
}
8989
}
90+
91+
/// Produce a conversion that takes in a value that would be available in a
92+
/// Swift function and convert that to the corresponding cdecl values.
93+
///
94+
/// This conversion goes in the opposite direction of init(cdeclToSwift:), and
95+
/// is used for (e.g.) returning the Swift value from a cdecl function. When
96+
/// there are multiple cdecl values that correspond to this one Swift value,
97+
/// the result will be a tuple that can be assigned to a tuple of the cdecl
98+
/// values, e.g., (<placeholder>.baseAddress, <placeholder>.count).
99+
init(
100+
swiftToCDecl swiftType: SwiftType,
101+
stdlibTypes: SwiftStandardLibraryTypes
102+
) throws {
103+
switch swiftType {
104+
case .function, .optional:
105+
throw LoweringError.unhandledType(swiftType)
106+
107+
case .metatype:
108+
self = .unsafeCastPointer(
109+
.placeholder,
110+
swiftType: .nominal(
111+
SwiftNominalType(
112+
nominalTypeDecl: stdlibTypes[.unsafeRawPointer]
113+
)
114+
)
115+
)
116+
return
117+
118+
case .nominal(let nominal):
119+
if let knownType = nominal.nominalTypeDecl.knownStandardLibraryType {
120+
// Swift types that map to primitive types in C. These can be passed
121+
// through directly.
122+
if knownType.primitiveCType != nil {
123+
self = .placeholder
124+
return
125+
}
126+
127+
// Typed pointers
128+
if nominal.genericArguments?.first != nil {
129+
switch knownType {
130+
case .unsafePointer, .unsafeMutablePointer:
131+
let isMutable = knownType == .unsafeMutablePointer
132+
self = ConversionStep(
133+
initializeRawPointerFromTyped: .placeholder,
134+
isMutable: isMutable,
135+
isPartOfBufferPointer: false,
136+
stdlibTypes: stdlibTypes
137+
)
138+
return
139+
140+
case .unsafeBufferPointer, .unsafeMutableBufferPointer:
141+
let isMutable = knownType == .unsafeMutableBufferPointer
142+
self = .tuplify(
143+
[
144+
ConversionStep(
145+
initializeRawPointerFromTyped: .explodedComponent(
146+
.placeholder,
147+
component: "pointer"
148+
),
149+
isMutable: isMutable,
150+
isPartOfBufferPointer: true,
151+
stdlibTypes: stdlibTypes
152+
),
153+
.explodedComponent(.placeholder, component: "count")
154+
]
155+
)
156+
return
157+
158+
default:
159+
break
160+
}
161+
}
162+
}
163+
164+
// Arbitrary nominal types.
165+
switch nominal.nominalTypeDecl.kind {
166+
case .actor, .class:
167+
// For actor and class, we pass around the pointer directly. Case to
168+
// the unsafe raw pointer type we use to represent it in C.
169+
self = .unsafeCastPointer(
170+
.placeholder,
171+
swiftType: .nominal(
172+
SwiftNominalType(
173+
nominalTypeDecl: stdlibTypes[.unsafeRawPointer]
174+
)
175+
)
176+
)
177+
178+
case .enum, .struct, .protocol:
179+
// For enums, structs, and protocol types, we leave the value alone.
180+
// The indirection will be handled by the caller.
181+
self = .placeholder
182+
}
183+
184+
case .tuple(let elements):
185+
// Convert all of the elements.
186+
self = .tuplify(
187+
try elements.map { element in
188+
try ConversionStep(swiftToCDecl: element, stdlibTypes: stdlibTypes)
189+
}
190+
)
191+
}
192+
}
90193
}

Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ extension Swift2JavaTranslator {
6363
// void result type
6464
indirectResult = false
6565
} else if loweredResult.cdeclParameters.count == 1,
66-
loweredResult.cdeclParameters[0].isPrimitive {
66+
loweredResult.cdeclParameters[0].canBeDirectReturn {
6767
// Primitive result type
6868
indirectResult = false
6969
} else {
@@ -145,7 +145,8 @@ extension Swift2JavaTranslator {
145145
SwiftNominalType(
146146
nominalTypeDecl: swiftStdlibTypes[.unsafeRawPointer]
147147
)
148-
)
148+
),
149+
canBeDirectReturn: true
149150
)
150151
]
151152
)
@@ -163,7 +164,7 @@ extension Swift2JavaTranslator {
163164
convention: convention,
164165
parameterName: parameterName,
165166
type: type,
166-
isPrimitive: true
167+
canBeDirectReturn: true
167168
)
168169
]
169170
)
@@ -183,7 +184,8 @@ extension Swift2JavaTranslator {
183184
parameterName: parameterName + "_pointer",
184185
type: SwiftType.nominal(
185186
SwiftNominalType(nominalTypeDecl: cdeclPointerType)
186-
)
187+
),
188+
canBeDirectReturn: true
187189
)
188190
]
189191
)
@@ -217,6 +219,13 @@ extension Swift2JavaTranslator {
217219
}
218220
}
219221

222+
// Arbitrary types are lowered to raw pointers that either "are" the
223+
// reference (for classes and actors) or will point to it.
224+
let canBeDirectReturn = switch nominal.nominalTypeDecl.kind {
225+
case .actor, .class: true
226+
case .enum, .protocol, .struct: false
227+
}
228+
220229
let isMutable = (convention == .inout)
221230
return LoweredParameters(
222231
cdeclParameters: [
@@ -229,7 +238,8 @@ extension Swift2JavaTranslator {
229238
? swiftStdlibTypes[.unsafeMutableRawPointer]
230239
: swiftStdlibTypes[.unsafeRawPointer]
231240
)
232-
)
241+
),
242+
canBeDirectReturn: canBeDirectReturn
233243
)
234244
]
235245
)
@@ -289,7 +299,11 @@ extension LoweredFunctionSignature {
289299
/// Produce the `@_cdecl` thunk for this lowered function signature that will
290300
/// call into the original function.
291301
@_spi(Testing)
292-
public func cdeclThunk(cName: String, inputFunction: FunctionDeclSyntax) -> FunctionDeclSyntax {
302+
public func cdeclThunk(
303+
cName: String,
304+
inputFunction: FunctionDeclSyntax,
305+
stdlibTypes: SwiftStandardLibraryTypes
306+
) -> FunctionDeclSyntax {
293307
var loweredCDecl = cdecl.createFunctionDecl(cName)
294308

295309
// Add the @_cdecl attribute.
@@ -345,23 +359,70 @@ extension LoweredFunctionSignature {
345359
\(callExpression)
346360
}
347361
"""
348-
} else if cdecl.result.type.isVoid {
349-
// Indirect return. This is a regular return in Swift that turns
350-
// into an assignment via the indirect parameters.
351-
// FIXME: This should actually be a swiftToCDecl conversion!
352-
let resultConversion = try! ConversionStep(cdeclToSwift: original.result.type)
353-
loweredCDecl.body = """
354-
{
355-
\(resultConversion.asExprSyntax(isSelf: true, placeholder: "_result")) = \(callExpression)
356-
}
357-
"""
358362
} else {
359-
// Direct return.
360-
loweredCDecl.body = """
361-
{
362-
return \(callExpression)
363-
}
364-
"""
363+
// Determine the necessary conversion of the Swift return value to the
364+
// cdecl return value.
365+
let resultConversion = try! ConversionStep(
366+
swiftToCDecl: original.result.type,
367+
stdlibTypes: stdlibTypes
368+
)
369+
370+
var bodyItems: [CodeBlockItemSyntax] = []
371+
372+
// If the are multiple places in the result conversion that reference
373+
// the placeholder, capture the result of the call in a local variable.
374+
// This prevents us from calling the function multiple times.
375+
let originalResult: ExprSyntax
376+
if resultConversion.placeholderCount > 1 {
377+
bodyItems.append("""
378+
let __swift_result = \(callExpression)
379+
"""
380+
)
381+
originalResult = "__swift_result"
382+
} else {
383+
originalResult = callExpression
384+
}
385+
386+
// FIXME: Check whether there are multiple places in which we reference
387+
// the placeholder in resultConversion. If so, we should write it into a
388+
// local let "_resultValue" or similar so we don't call the underlying
389+
// function multiple times.
390+
391+
// Convert the result.
392+
let convertedResult = resultConversion.asExprSyntax(
393+
isSelf: true,
394+
placeholder: originalResult.description
395+
)
396+
397+
if cdecl.result.type.isVoid {
398+
// Indirect return. This is a regular return in Swift that turns
399+
// into an assignment via the indirect parameters. We do a cdeclToSwift
400+
// conversion on the left-hand side of the tuple to gather all of the
401+
// indirect output parameters we need to assign to, and the result
402+
// conversion is the corresponding right-hand side.
403+
let cdeclParamConversion = try! ConversionStep(
404+
cdeclToSwift: original.result.type
405+
)
406+
let indirectResults = cdeclParamConversion.asExprSyntax(
407+
isSelf: true,
408+
placeholder: "_result"
409+
)
410+
bodyItems.append("""
411+
\(indirectResults) = \(convertedResult)
412+
"""
413+
)
414+
} else {
415+
// Direct return. Just convert the expression.
416+
bodyItems.append("""
417+
return \(convertedResult)
418+
"""
419+
)
420+
}
421+
422+
loweredCDecl.body = CodeBlockSyntax(
423+
leftBrace: .leftBraceToken(trailingTrivia: .newline),
424+
statements: .init(bodyItems.map { $0.with(\.trailingTrivia, .newline) })
425+
)
365426
}
366427

367428
return loweredCDecl

Sources/JExtractSwift/ConversionStep.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,56 @@ enum ConversionStep: Equatable {
5151
/// tuples, which Swift will convert to the labeled tuple form.
5252
case tuplify([ConversionStep])
5353

54+
/// Create an initialization step that produces the raw pointer type that
55+
/// corresponds to the typed pointer.
56+
init(
57+
initializeRawPointerFromTyped typedStep: ConversionStep,
58+
isMutable: Bool,
59+
isPartOfBufferPointer: Bool,
60+
stdlibTypes: SwiftStandardLibraryTypes
61+
) {
62+
// Initialize the corresponding raw pointer type from the typed
63+
// pointer we have on the Swift side.
64+
let rawPointerType = isMutable
65+
? stdlibTypes[.unsafeMutableRawPointer]
66+
: stdlibTypes[.unsafeRawPointer]
67+
self = .initialize(
68+
.nominal(
69+
SwiftNominalType(
70+
nominalTypeDecl: rawPointerType
71+
)
72+
),
73+
arguments: [
74+
LabeledArgument(
75+
argument: isPartOfBufferPointer
76+
? .explodedComponent(
77+
typedStep,
78+
component: "pointer"
79+
)
80+
: typedStep
81+
),
82+
]
83+
)
84+
}
85+
86+
/// Count the number of times that the placeholder occurs within this
87+
/// conversion step.
88+
var placeholderCount: Int {
89+
switch self {
90+
case .explodedComponent(let inner, component: _),
91+
.passIndirectly(let inner), .pointee(let inner),
92+
.typedPointer(let inner, swiftType: _),
93+
.unsafeCastPointer(let inner, swiftType: _):
94+
inner.placeholderCount
95+
case .initialize(_, arguments: let arguments):
96+
arguments.reduce(0) { $0 + $1.argument.placeholderCount }
97+
case .placeholder:
98+
1
99+
case .tuplify(let elements):
100+
elements.reduce(0) { $0 + $1.placeholderCount }
101+
}
102+
}
103+
54104
/// Convert the conversion step into an expression with the given
55105
/// value as the placeholder value in the expression.
56106
func asExprSyntax(isSelf: Bool, placeholder: String) -> ExprSyntax {

Sources/JExtractSwift/Swift2JavaTranslator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public final class Swift2JavaTranslator {
3939
/// type representation.
4040
package var importedTypes: [String: ImportedNominalType] = [:]
4141

42-
var swiftStdlibTypes: SwiftStandardLibraryTypes
42+
public var swiftStdlibTypes: SwiftStandardLibraryTypes
4343

4444
let symbolTable: SwiftSymbolTable
4545
let nominalResolution: NominalTypeResolution = NominalTypeResolution()

Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ struct SwiftParameter: Equatable {
1919
var argumentLabel: String?
2020
var parameterName: String?
2121
var type: SwiftType
22-
var isPrimitive = false
22+
var canBeDirectReturn = false
2323
}
2424

2525
extension SwiftParameter: CustomStringConvertible {

Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ enum KnownStandardLibraryType: String, Hashable, CaseIterable {
5858

5959
/// Captures many types from the Swift standard library in their most basic
6060
/// forms, so that the translator can reason about them in source code.
61-
struct SwiftStandardLibraryTypes {
61+
public struct SwiftStandardLibraryTypes {
6262
// Swift.UnsafePointer<Element>
6363
let unsafePointerDecl: SwiftNominalTypeDeclaration
6464

Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ func assertLoweredFunction(
4747
inputFunction,
4848
enclosingType: enclosingType
4949
)
50-
let loweredCDecl = loweredFunction.cdeclThunk(cName: "c_\(inputFunction.name.text)", inputFunction: inputFunction)
50+
let loweredCDecl = loweredFunction.cdeclThunk(
51+
cName: "c_\(inputFunction.name.text)",
52+
inputFunction: inputFunction,
53+
stdlibTypes: translator.swiftStdlibTypes
54+
)
5155

5256
#expect(
5357
loweredCDecl.description == expectedCDecl.description,

0 commit comments

Comments
 (0)