Skip to content

Commit

Permalink
Implement function bodies for lowered @_cdecl thunks
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
DougGregor committed Jan 30, 2025
1 parent 831397d commit 464475e
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 38 deletions.
188 changes: 174 additions & 14 deletions Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import JavaTypes
import SwiftSyntax

extension Swift2JavaTranslator {
/// Lower the given function declaration to a C-compatible entrypoint,
/// providing all of the mappings between the parameter and result types
/// of the original function and its `@_cdecl` counterpart.
@_spi(Testing)
public func lowerFunctionSignature(
_ decl: FunctionDeclSyntax,
Expand Down Expand Up @@ -145,8 +148,12 @@ extension Swift2JavaTranslator {
let mutable = (convention == .inout)
let loweringStep: LoweringStep
switch nominal.nominalTypeDecl.kind {
case .actor, .class: loweringStep = .passDirectly(parameterName)
case .enum, .struct, .protocol: loweringStep = .passIndirectly(parameterName)
case .actor, .class:
loweringStep =
.unsafeCastPointer(.passDirectly(parameterName), swiftType: type)
case .enum, .struct, .protocol:
loweringStep =
.passIndirectly(.pointee( .typedPointer(.passDirectly(parameterName), swiftType: type)))
}

return LoweredParameters(
Expand All @@ -173,7 +180,7 @@ extension Swift2JavaTranslator {
try lowerParameter(element, convention: convention, parameterName: name)
}
return LoweredParameters(
cdeclToOriginal: .tuplify(parameterNames.map { .passDirectly($0) }),
cdeclToOriginal: .tuplify(loweredElements.map { $0.cdeclToOriginal }),
cdeclParameters: loweredElements.flatMap { $0.cdeclParameters },
javaFFMParameters: loweredElements.flatMap { $0.javaFFMParameters }
)
Expand Down Expand Up @@ -258,10 +265,9 @@ extension Swift2JavaTranslator {
cdeclToOriginal = .passDirectly(parameterName)

case (true, false):
// FIXME: Generic arguments, ugh
cdeclToOriginal = .suffixed(
.passDirectly(parameterName),
".assumingMemoryBound(to: \(nominal.genericArguments![0]).self)"
cdeclToOriginal = .typedPointer(
.passDirectly(parameterName + "_pointer"),
swiftType: nominal.genericArguments![0]
)

case (false, true):
Expand All @@ -275,9 +281,9 @@ extension Swift2JavaTranslator {
type,
arguments: [
LabeledArgument(label: "start",
argument: .suffixed(
argument: .typedPointer(
.passDirectly(parameterName + "_pointer"),
".assumingMemoryBound(to: \(nominal.genericArguments![0]).self")),
swiftType: nominal.genericArguments![0])),
LabeledArgument(label: "count",
argument: .passDirectly(parameterName + "_count"))
]
Expand Down Expand Up @@ -338,30 +344,113 @@ struct LabeledArgument<Element> {

extension LabeledArgument: Equatable where Element: Equatable { }

/// How to lower the Swift parameter
/// Describes the transformation needed to take the parameters of a thunk
/// and map them to the corresponding parameter (or result value) of the
/// original function.
enum LoweringStep: Equatable {
/// A direct reference to a parameter of the thunk.
case passDirectly(String)
case passIndirectly(String)
indirect case suffixed(LoweringStep, String)

/// Cast the pointer described by the lowering step to the given
/// Swift type using `unsafeBitCast(_:to:)`.
indirect case unsafeCastPointer(LoweringStep, swiftType: SwiftType)

/// Assume at the untyped pointer described by the lowering step to the
/// given type, using `assumingMemoryBound(to:).`
indirect case typedPointer(LoweringStep, swiftType: SwiftType)

/// The thing to which the pointer typed, which is the `pointee` property
/// of the `Unsafe(Mutable)Pointer` types in Swift.
indirect case pointee(LoweringStep)

/// Pass this value indirectly, via & for explicit `inout` parameters.
indirect case passIndirectly(LoweringStep)

/// Initialize a value of the given Swift type with the set of labeled
/// arguments.
case initialize(SwiftType, arguments: [LabeledArgument<LoweringStep>])

/// Produce a tuple with the given elements.
///
/// This is used for exploding Swift tuple arguments into multiple
/// elements, recursively. Note that this always produces unlabeled
/// tuples, which Swift will convert to the labeled tuple form.
case tuplify([LoweringStep])
}

struct LoweredParameters: Equatable {
/// The steps needed to get from the @_cdecl parameter to the original function
/// The steps needed to get from the @_cdecl parameters to the original function
/// parameter.
var cdeclToOriginal: LoweringStep

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

/// The lowerung of the parmaeters at the C level as expressed for Java's
/// The lowering of the parameters at the C level as expressed for Java's
/// foreign function and memory interface.
///
/// The elements in this array match up with those of 'cdeclParameters'.
var javaFFMParameters: [ForeignValueLayout]
}

extension LoweredParameters {
/// Produce an expression that computes the argument for this parameter
/// when calling the original function from the cdecl entrypoint.
func cdeclToOriginalArgumentExpr(isSelf: Bool)-> ExprSyntax {
cdeclToOriginal.asExprSyntax(isSelf: isSelf)
}
}

extension LoweringStep {
func asExprSyntax(isSelf: Bool) -> ExprSyntax {
switch self {
case .passDirectly(let rawArgument):
return "\(raw: rawArgument)"

case .unsafeCastPointer(let step, swiftType: let swiftType):
let untypedExpr = step.asExprSyntax(isSelf: false)
return "unsafeBitCast(\(untypedExpr), to: \(swiftType.metatypeReferenceExprSyntax))"

case .typedPointer(let step, swiftType: let type):
let untypedExpr = step.asExprSyntax(isSelf: isSelf)
return "\(untypedExpr).assumingMemoryBound(to: \(type.metatypeReferenceExprSyntax))"

case .pointee(let step):
let untypedExpr = step.asExprSyntax(isSelf: isSelf)
return "\(untypedExpr).pointee"

case .passIndirectly(let step):
let innerExpr = step.asExprSyntax(isSelf: false)
return isSelf ? innerExpr : "&\(innerExpr)"

case .initialize(let type, arguments: let arguments):
let renderedArguments: [String] = arguments.map { labeledArgument in
let renderedArg = labeledArgument.argument.asExprSyntax(isSelf: false)
if let argmentLabel = labeledArgument.label {
return "\(argmentLabel): \(renderedArg.description)"
} else {
return renderedArg.description
}
}

// FIXME: Should be able to use structured initializers here instead
// of splatting out text.
let renderedArgumentList = renderedArguments.joined(separator: ", ")
return "\(raw: type.description)(\(raw: renderedArgumentList))"

case .tuplify(let elements):
let renderedElements: [String] = elements.map { element in
element.asExprSyntax(isSelf: false).description
}

// FIXME: Should be able to use structured initializers here instead
// of splatting out text.
let renderedElementList = renderedElements.joined(separator: ", ")
return "(\(raw: renderedElementList))"
}
}
}

enum LoweringError: Error {
case inoutNotSupported(SwiftType)
case unhandledType(SwiftType)
Expand All @@ -375,3 +464,74 @@ public struct LoweredFunctionSignature: Equatable {
var parameters: [LoweredParameters]
var result: LoweredParameters
}

extension LoweredFunctionSignature {
/// Produce the `@_cdecl` thunk for this lowered function signature that will
/// call into the original function.
@_spi(Testing)
public func cdeclThunk(cName: String, inputFunction: FunctionDeclSyntax) -> FunctionDeclSyntax {
var loweredCDecl = cdecl.createFunctionDecl(cName)

// Add the @_cdecl attribute.
let cdeclAttribute: AttributeSyntax = "@_cdecl(\(literal: cName))\n"
loweredCDecl.attributes.append(.attribute(cdeclAttribute))

// Create the body.

// Lower "self", if there is one.
let parametersToLower: ArraySlice<LoweredParameters>
let cdeclToOriginalSelf: ExprSyntax?
if original.selfParameter != nil {
cdeclToOriginalSelf = parameters[0].cdeclToOriginalArgumentExpr(isSelf: true)
parametersToLower = parameters[1...]
} else {
cdeclToOriginalSelf = nil
parametersToLower = parameters[...]
}

// Lower the remaining arguments.
// FIXME: Should be able to use structured initializers here instead
// of splatting out text.
let cdeclToOriginalArguments = zip(parametersToLower, original.parameters).map { lowering, originalParam in
let cdeclToOriginalArg = lowering.cdeclToOriginalArgumentExpr(isSelf: false)
if let argumentLabel = originalParam.argumentLabel {
return "\(argumentLabel): \(cdeclToOriginalArg.description)"
} else {
return cdeclToOriginalArg.description
}
}

// Form the call expression.
var callExpression: ExprSyntax = "\(inputFunction.name)(\(raw: cdeclToOriginalArguments.joined(separator: ", ")))"
if let cdeclToOriginalSelf {
callExpression = "\(cdeclToOriginalSelf).\(callExpression)"
}

// Handle the return.
if cdecl.result.type.isVoid && original.result.type.isVoid {
// Nothing to return.
loweredCDecl.body = """
{
\(callExpression)
}
"""
} else if cdecl.result.type.isVoid {
// Indirect return. This is a regular return in Swift that turns
// into a
loweredCDecl.body = """
{
\(result.cdeclToOriginalArgumentExpr(isSelf: true)) = \(callExpression)
}
"""
} else {
// Direct return.
loweredCDecl.body = """
{
return \(callExpression)
}
"""
}

return loweredCDecl
}
}
14 changes: 11 additions & 3 deletions Sources/JExtractSwift/SwiftTypes/SwiftFunctionSignature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import SwiftSyntaxBuilder
/// parameters and return type.
@_spi(Testing)
public struct SwiftFunctionSignature: Equatable {
// FIXME: isStaticOrClass probably shouldn't be here?
var isStaticOrClass: Bool
var selfParameter: SwiftParameter?
var parameters: [SwiftParameter]
Expand All @@ -30,9 +31,16 @@ extension SwiftFunctionSignature {
/// signature.
package func createFunctionDecl(_ name: String) -> FunctionDeclSyntax {
let parametersStr = parameters.map(\.description).joined(separator: ", ")
let resultStr = result.type.description

let resultWithArrow: String
if result.type.isVoid {
resultWithArrow = ""
} else {
resultWithArrow = " -> \(result.type.description)"
}

let decl: DeclSyntax = """
func \(raw: name)(\(raw: parametersStr)) -> \(raw: resultStr) {
func \(raw: name)(\(raw: parametersStr))\(raw: resultWithArrow) {
// implementation
}
"""
Expand All @@ -53,7 +61,7 @@ extension SwiftFunctionSignature {
var isConsuming = false
var isStaticOrClass = false
for modifier in node.modifiers {
switch modifier.name {
switch modifier.name.tokenKind {
case .keyword(.mutating): isMutating = true
case .keyword(.static), .keyword(.class): isStaticOrClass = true
case .keyword(.consuming): isConsuming = true
Expand Down
33 changes: 22 additions & 11 deletions Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,34 @@ enum SwiftParameterConvention: Equatable {

extension SwiftParameter {
init(_ node: FunctionParameterSyntax, symbolTable: SwiftSymbolTable) throws {
// Determine the convention. The default is by-value, but modifiers can alter
// this.
// Determine the convention. The default is by-value, but there are
// specifiers on the type for other conventions (like `inout`).
var type = node.type
var convention = SwiftParameterConvention.byValue
for modifier in node.modifiers {
switch modifier.name {
case .keyword(.consuming), .keyword(.__consuming), .keyword(.__owned):
convention = .consuming
case .keyword(.inout):
convention = .inout
default:
break
if let attributedType = type.as(AttributedTypeSyntax.self) {
for specifier in attributedType.specifiers {
guard case .simpleTypeSpecifier(let simple) = specifier else {
continue
}

switch simple.specifier.tokenKind {
case .keyword(.consuming), .keyword(.__consuming), .keyword(.__owned):
convention = .consuming
case .keyword(.inout):
convention = .inout
default:
break
}
}

// Ignore anything else in the attributed type.
// FIXME: We might want to check for these and ignore them.
type = attributedType.baseType
}
self.convention = convention

// Determine the type.
self.type = try SwiftType(node.type, symbolTable: symbolTable)
self.type = try SwiftType(type, symbolTable: symbolTable)

// FIXME: swift-syntax itself should have these utilities based on identifiers.
if let secondName = node.secondName {
Expand Down
31 changes: 30 additions & 1 deletion Sources/JExtractSwift/SwiftTypes/SwiftType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,34 @@ enum SwiftType: Equatable {
var asNominalTypeDeclaration: SwiftNominalTypeDeclaration? {
asNominalType?.nominalTypeDecl
}

/// Whether this is the "Void" type, which is actually an empty
/// tuple.
var isVoid: Bool {
return self == .tuple([])
}
}

extension SwiftType: CustomStringConvertible {
/// Whether forming a postfix type or expression to this Swift type
/// requires parentheses.
private var postfixRequiresParentheses: Bool {
switch self {
case .function: true
case .metatype, .nominal, .optional, .tuple: false
}
}

var description: String {
switch self {
case .nominal(let nominal): return nominal.description
case .function(let functionType): return functionType.description
case .metatype(let instanceType):
return "(\(instanceType.description)).Type"
var instanceTypeStr = instanceType.description
if instanceType.postfixRequiresParentheses {
instanceTypeStr = "(\(instanceTypeStr))"
}
return "\(instanceTypeStr).Type"
case .optional(let wrappedType):
return "\(wrappedType.description)?"
case .tuple(let elements):
Expand Down Expand Up @@ -213,4 +232,14 @@ extension SwiftType {
)
)
}

/// Produce an expression that creates the metatype for this type in
/// Swift source code.
var metatypeReferenceExprSyntax: ExprSyntax {
let type: ExprSyntax = "\(raw: description)"
if postfixRequiresParentheses {
return "(\(type)).self"
}
return "\(type).self"
}
}
3 changes: 2 additions & 1 deletion Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ func assertLoweredFunction(
inputFunction,
enclosingType: enclosingType
)
let loweredCDecl = loweredFunction.cdecl.createFunctionDecl(inputFunction.name.text)
let loweredCDecl = loweredFunction.cdeclThunk(cName: "c_\(inputFunction.name.text)", inputFunction: inputFunction)

#expect(
loweredCDecl.description == expectedCDecl.description,
sourceLocation: Testing.SourceLocation(
Expand Down
Loading

0 comments on commit 464475e

Please sign in to comment.