Skip to content

Commit 464475e

Browse files
committed
Implement function bodies for lowered @_cdecl thunks
Improve the handling of lowering Swift declarations down to C thunks to more clearly model the mapping between the C parameters and the Swift parameters, and start generating code within the body of these C thunks. Provide tests and fixes for lowering of methods of structs and classes, including mutating methods, as well as inout parameters, direct and indirect returns, and so on.
1 parent 831397d commit 464475e

File tree

6 files changed

+303
-38
lines changed

6 files changed

+303
-38
lines changed

Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift

Lines changed: 174 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import JavaTypes
1616
import SwiftSyntax
1717

1818
extension Swift2JavaTranslator {
19+
/// Lower the given function declaration to a C-compatible entrypoint,
20+
/// providing all of the mappings between the parameter and result types
21+
/// of the original function and its `@_cdecl` counterpart.
1922
@_spi(Testing)
2023
public func lowerFunctionSignature(
2124
_ decl: FunctionDeclSyntax,
@@ -145,8 +148,12 @@ extension Swift2JavaTranslator {
145148
let mutable = (convention == .inout)
146149
let loweringStep: LoweringStep
147150
switch nominal.nominalTypeDecl.kind {
148-
case .actor, .class: loweringStep = .passDirectly(parameterName)
149-
case .enum, .struct, .protocol: loweringStep = .passIndirectly(parameterName)
151+
case .actor, .class:
152+
loweringStep =
153+
.unsafeCastPointer(.passDirectly(parameterName), swiftType: type)
154+
case .enum, .struct, .protocol:
155+
loweringStep =
156+
.passIndirectly(.pointee( .typedPointer(.passDirectly(parameterName), swiftType: type)))
150157
}
151158

152159
return LoweredParameters(
@@ -173,7 +180,7 @@ extension Swift2JavaTranslator {
173180
try lowerParameter(element, convention: convention, parameterName: name)
174181
}
175182
return LoweredParameters(
176-
cdeclToOriginal: .tuplify(parameterNames.map { .passDirectly($0) }),
183+
cdeclToOriginal: .tuplify(loweredElements.map { $0.cdeclToOriginal }),
177184
cdeclParameters: loweredElements.flatMap { $0.cdeclParameters },
178185
javaFFMParameters: loweredElements.flatMap { $0.javaFFMParameters }
179186
)
@@ -258,10 +265,9 @@ extension Swift2JavaTranslator {
258265
cdeclToOriginal = .passDirectly(parameterName)
259266

260267
case (true, false):
261-
// FIXME: Generic arguments, ugh
262-
cdeclToOriginal = .suffixed(
263-
.passDirectly(parameterName),
264-
".assumingMemoryBound(to: \(nominal.genericArguments![0]).self)"
268+
cdeclToOriginal = .typedPointer(
269+
.passDirectly(parameterName + "_pointer"),
270+
swiftType: nominal.genericArguments![0]
265271
)
266272

267273
case (false, true):
@@ -275,9 +281,9 @@ extension Swift2JavaTranslator {
275281
type,
276282
arguments: [
277283
LabeledArgument(label: "start",
278-
argument: .suffixed(
284+
argument: .typedPointer(
279285
.passDirectly(parameterName + "_pointer"),
280-
".assumingMemoryBound(to: \(nominal.genericArguments![0]).self")),
286+
swiftType: nominal.genericArguments![0])),
281287
LabeledArgument(label: "count",
282288
argument: .passDirectly(parameterName + "_count"))
283289
]
@@ -338,30 +344,113 @@ struct LabeledArgument<Element> {
338344

339345
extension LabeledArgument: Equatable where Element: Equatable { }
340346

341-
/// How to lower the Swift parameter
347+
/// Describes the transformation needed to take the parameters of a thunk
348+
/// and map them to the corresponding parameter (or result value) of the
349+
/// original function.
342350
enum LoweringStep: Equatable {
351+
/// A direct reference to a parameter of the thunk.
343352
case passDirectly(String)
344-
case passIndirectly(String)
345-
indirect case suffixed(LoweringStep, String)
353+
354+
/// Cast the pointer described by the lowering step to the given
355+
/// Swift type using `unsafeBitCast(_:to:)`.
356+
indirect case unsafeCastPointer(LoweringStep, swiftType: SwiftType)
357+
358+
/// Assume at the untyped pointer described by the lowering step to the
359+
/// given type, using `assumingMemoryBound(to:).`
360+
indirect case typedPointer(LoweringStep, swiftType: SwiftType)
361+
362+
/// The thing to which the pointer typed, which is the `pointee` property
363+
/// of the `Unsafe(Mutable)Pointer` types in Swift.
364+
indirect case pointee(LoweringStep)
365+
366+
/// Pass this value indirectly, via & for explicit `inout` parameters.
367+
indirect case passIndirectly(LoweringStep)
368+
369+
/// Initialize a value of the given Swift type with the set of labeled
370+
/// arguments.
346371
case initialize(SwiftType, arguments: [LabeledArgument<LoweringStep>])
372+
373+
/// Produce a tuple with the given elements.
374+
///
375+
/// This is used for exploding Swift tuple arguments into multiple
376+
/// elements, recursively. Note that this always produces unlabeled
377+
/// tuples, which Swift will convert to the labeled tuple form.
347378
case tuplify([LoweringStep])
348379
}
349380

350381
struct LoweredParameters: Equatable {
351-
/// The steps needed to get from the @_cdecl parameter to the original function
382+
/// The steps needed to get from the @_cdecl parameters to the original function
352383
/// parameter.
353384
var cdeclToOriginal: LoweringStep
354385

355386
/// The lowering of the parameters at the C level in Swift.
356387
var cdeclParameters: [SwiftParameter]
357388

358-
/// The lowerung of the parmaeters at the C level as expressed for Java's
389+
/// The lowering of the parameters at the C level as expressed for Java's
359390
/// foreign function and memory interface.
360391
///
361392
/// The elements in this array match up with those of 'cdeclParameters'.
362393
var javaFFMParameters: [ForeignValueLayout]
363394
}
364395

396+
extension LoweredParameters {
397+
/// Produce an expression that computes the argument for this parameter
398+
/// when calling the original function from the cdecl entrypoint.
399+
func cdeclToOriginalArgumentExpr(isSelf: Bool)-> ExprSyntax {
400+
cdeclToOriginal.asExprSyntax(isSelf: isSelf)
401+
}
402+
}
403+
404+
extension LoweringStep {
405+
func asExprSyntax(isSelf: Bool) -> ExprSyntax {
406+
switch self {
407+
case .passDirectly(let rawArgument):
408+
return "\(raw: rawArgument)"
409+
410+
case .unsafeCastPointer(let step, swiftType: let swiftType):
411+
let untypedExpr = step.asExprSyntax(isSelf: false)
412+
return "unsafeBitCast(\(untypedExpr), to: \(swiftType.metatypeReferenceExprSyntax))"
413+
414+
case .typedPointer(let step, swiftType: let type):
415+
let untypedExpr = step.asExprSyntax(isSelf: isSelf)
416+
return "\(untypedExpr).assumingMemoryBound(to: \(type.metatypeReferenceExprSyntax))"
417+
418+
case .pointee(let step):
419+
let untypedExpr = step.asExprSyntax(isSelf: isSelf)
420+
return "\(untypedExpr).pointee"
421+
422+
case .passIndirectly(let step):
423+
let innerExpr = step.asExprSyntax(isSelf: false)
424+
return isSelf ? innerExpr : "&\(innerExpr)"
425+
426+
case .initialize(let type, arguments: let arguments):
427+
let renderedArguments: [String] = arguments.map { labeledArgument in
428+
let renderedArg = labeledArgument.argument.asExprSyntax(isSelf: false)
429+
if let argmentLabel = labeledArgument.label {
430+
return "\(argmentLabel): \(renderedArg.description)"
431+
} else {
432+
return renderedArg.description
433+
}
434+
}
435+
436+
// FIXME: Should be able to use structured initializers here instead
437+
// of splatting out text.
438+
let renderedArgumentList = renderedArguments.joined(separator: ", ")
439+
return "\(raw: type.description)(\(raw: renderedArgumentList))"
440+
441+
case .tuplify(let elements):
442+
let renderedElements: [String] = elements.map { element in
443+
element.asExprSyntax(isSelf: false).description
444+
}
445+
446+
// FIXME: Should be able to use structured initializers here instead
447+
// of splatting out text.
448+
let renderedElementList = renderedElements.joined(separator: ", ")
449+
return "(\(raw: renderedElementList))"
450+
}
451+
}
452+
}
453+
365454
enum LoweringError: Error {
366455
case inoutNotSupported(SwiftType)
367456
case unhandledType(SwiftType)
@@ -375,3 +464,74 @@ public struct LoweredFunctionSignature: Equatable {
375464
var parameters: [LoweredParameters]
376465
var result: LoweredParameters
377466
}
467+
468+
extension LoweredFunctionSignature {
469+
/// Produce the `@_cdecl` thunk for this lowered function signature that will
470+
/// call into the original function.
471+
@_spi(Testing)
472+
public func cdeclThunk(cName: String, inputFunction: FunctionDeclSyntax) -> FunctionDeclSyntax {
473+
var loweredCDecl = cdecl.createFunctionDecl(cName)
474+
475+
// Add the @_cdecl attribute.
476+
let cdeclAttribute: AttributeSyntax = "@_cdecl(\(literal: cName))\n"
477+
loweredCDecl.attributes.append(.attribute(cdeclAttribute))
478+
479+
// Create the body.
480+
481+
// Lower "self", if there is one.
482+
let parametersToLower: ArraySlice<LoweredParameters>
483+
let cdeclToOriginalSelf: ExprSyntax?
484+
if original.selfParameter != nil {
485+
cdeclToOriginalSelf = parameters[0].cdeclToOriginalArgumentExpr(isSelf: true)
486+
parametersToLower = parameters[1...]
487+
} else {
488+
cdeclToOriginalSelf = nil
489+
parametersToLower = parameters[...]
490+
}
491+
492+
// Lower the remaining arguments.
493+
// FIXME: Should be able to use structured initializers here instead
494+
// of splatting out text.
495+
let cdeclToOriginalArguments = zip(parametersToLower, original.parameters).map { lowering, originalParam in
496+
let cdeclToOriginalArg = lowering.cdeclToOriginalArgumentExpr(isSelf: false)
497+
if let argumentLabel = originalParam.argumentLabel {
498+
return "\(argumentLabel): \(cdeclToOriginalArg.description)"
499+
} else {
500+
return cdeclToOriginalArg.description
501+
}
502+
}
503+
504+
// Form the call expression.
505+
var callExpression: ExprSyntax = "\(inputFunction.name)(\(raw: cdeclToOriginalArguments.joined(separator: ", ")))"
506+
if let cdeclToOriginalSelf {
507+
callExpression = "\(cdeclToOriginalSelf).\(callExpression)"
508+
}
509+
510+
// Handle the return.
511+
if cdecl.result.type.isVoid && original.result.type.isVoid {
512+
// Nothing to return.
513+
loweredCDecl.body = """
514+
{
515+
\(callExpression)
516+
}
517+
"""
518+
} else if cdecl.result.type.isVoid {
519+
// Indirect return. This is a regular return in Swift that turns
520+
// into a
521+
loweredCDecl.body = """
522+
{
523+
\(result.cdeclToOriginalArgumentExpr(isSelf: true)) = \(callExpression)
524+
}
525+
"""
526+
} else {
527+
// Direct return.
528+
loweredCDecl.body = """
529+
{
530+
return \(callExpression)
531+
}
532+
"""
533+
}
534+
535+
return loweredCDecl
536+
}
537+
}

Sources/JExtractSwift/SwiftTypes/SwiftFunctionSignature.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import SwiftSyntaxBuilder
1919
/// parameters and return type.
2020
@_spi(Testing)
2121
public struct SwiftFunctionSignature: Equatable {
22+
// FIXME: isStaticOrClass probably shouldn't be here?
2223
var isStaticOrClass: Bool
2324
var selfParameter: SwiftParameter?
2425
var parameters: [SwiftParameter]
@@ -30,9 +31,16 @@ extension SwiftFunctionSignature {
3031
/// signature.
3132
package func createFunctionDecl(_ name: String) -> FunctionDeclSyntax {
3233
let parametersStr = parameters.map(\.description).joined(separator: ", ")
33-
let resultStr = result.type.description
34+
35+
let resultWithArrow: String
36+
if result.type.isVoid {
37+
resultWithArrow = ""
38+
} else {
39+
resultWithArrow = " -> \(result.type.description)"
40+
}
41+
3442
let decl: DeclSyntax = """
35-
func \(raw: name)(\(raw: parametersStr)) -> \(raw: resultStr) {
43+
func \(raw: name)(\(raw: parametersStr))\(raw: resultWithArrow) {
3644
// implementation
3745
}
3846
"""
@@ -53,7 +61,7 @@ extension SwiftFunctionSignature {
5361
var isConsuming = false
5462
var isStaticOrClass = false
5563
for modifier in node.modifiers {
56-
switch modifier.name {
64+
switch modifier.name.tokenKind {
5765
case .keyword(.mutating): isMutating = true
5866
case .keyword(.static), .keyword(.class): isStaticOrClass = true
5967
case .keyword(.consuming): isConsuming = true

Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,23 +58,34 @@ enum SwiftParameterConvention: Equatable {
5858

5959
extension SwiftParameter {
6060
init(_ node: FunctionParameterSyntax, symbolTable: SwiftSymbolTable) throws {
61-
// Determine the convention. The default is by-value, but modifiers can alter
62-
// this.
61+
// Determine the convention. The default is by-value, but there are
62+
// specifiers on the type for other conventions (like `inout`).
63+
var type = node.type
6364
var convention = SwiftParameterConvention.byValue
64-
for modifier in node.modifiers {
65-
switch modifier.name {
66-
case .keyword(.consuming), .keyword(.__consuming), .keyword(.__owned):
67-
convention = .consuming
68-
case .keyword(.inout):
69-
convention = .inout
70-
default:
71-
break
65+
if let attributedType = type.as(AttributedTypeSyntax.self) {
66+
for specifier in attributedType.specifiers {
67+
guard case .simpleTypeSpecifier(let simple) = specifier else {
68+
continue
69+
}
70+
71+
switch simple.specifier.tokenKind {
72+
case .keyword(.consuming), .keyword(.__consuming), .keyword(.__owned):
73+
convention = .consuming
74+
case .keyword(.inout):
75+
convention = .inout
76+
default:
77+
break
78+
}
7279
}
80+
81+
// Ignore anything else in the attributed type.
82+
// FIXME: We might want to check for these and ignore them.
83+
type = attributedType.baseType
7384
}
7485
self.convention = convention
7586

7687
// Determine the type.
77-
self.type = try SwiftType(node.type, symbolTable: symbolTable)
88+
self.type = try SwiftType(type, symbolTable: symbolTable)
7889

7990
// FIXME: swift-syntax itself should have these utilities based on identifiers.
8091
if let secondName = node.secondName {

Sources/JExtractSwift/SwiftTypes/SwiftType.swift

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,34 @@ enum SwiftType: Equatable {
3333
var asNominalTypeDeclaration: SwiftNominalTypeDeclaration? {
3434
asNominalType?.nominalTypeDecl
3535
}
36+
37+
/// Whether this is the "Void" type, which is actually an empty
38+
/// tuple.
39+
var isVoid: Bool {
40+
return self == .tuple([])
41+
}
3642
}
3743

3844
extension SwiftType: CustomStringConvertible {
45+
/// Whether forming a postfix type or expression to this Swift type
46+
/// requires parentheses.
47+
private var postfixRequiresParentheses: Bool {
48+
switch self {
49+
case .function: true
50+
case .metatype, .nominal, .optional, .tuple: false
51+
}
52+
}
53+
3954
var description: String {
4055
switch self {
4156
case .nominal(let nominal): return nominal.description
4257
case .function(let functionType): return functionType.description
4358
case .metatype(let instanceType):
44-
return "(\(instanceType.description)).Type"
59+
var instanceTypeStr = instanceType.description
60+
if instanceType.postfixRequiresParentheses {
61+
instanceTypeStr = "(\(instanceTypeStr))"
62+
}
63+
return "\(instanceTypeStr).Type"
4564
case .optional(let wrappedType):
4665
return "\(wrappedType.description)?"
4766
case .tuple(let elements):
@@ -213,4 +232,14 @@ extension SwiftType {
213232
)
214233
)
215234
}
235+
236+
/// Produce an expression that creates the metatype for this type in
237+
/// Swift source code.
238+
var metatypeReferenceExprSyntax: ExprSyntax {
239+
let type: ExprSyntax = "\(raw: description)"
240+
if postfixRequiresParentheses {
241+
return "(\(type)).self"
242+
}
243+
return "\(type).self"
244+
}
216245
}

Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ func assertLoweredFunction(
4646
inputFunction,
4747
enclosingType: enclosingType
4848
)
49-
let loweredCDecl = loweredFunction.cdecl.createFunctionDecl(inputFunction.name.text)
49+
let loweredCDecl = loweredFunction.cdeclThunk(cName: "c_\(inputFunction.name.text)", inputFunction: inputFunction)
50+
5051
#expect(
5152
loweredCDecl.description == expectedCDecl.description,
5253
sourceLocation: Testing.SourceLocation(

0 commit comments

Comments
 (0)