Skip to content

Commit

Permalink
Merge pull request #212 from DougGregor/lowered-cdecl-function-bodies
Browse files Browse the repository at this point in the history
Implement function bodies for lowered @_cdecl thunks
  • Loading branch information
DougGregor authored Jan 30, 2025
2 parents 831397d + 3d72f9f commit 98129ae
Show file tree
Hide file tree
Showing 6 changed files with 327 additions and 41 deletions.
210 changes: 195 additions & 15 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 @@ -124,9 +127,29 @@ extension Swift2JavaTranslator {
parameterName: String
) throws -> LoweredParameters {
switch type {
case .function, .metatype, .optional:
case .function, .optional:
throw LoweringError.unhandledType(type)

case .metatype(let instanceType):
return LoweredParameters(
cdeclToOriginal: .unsafeCastPointer(
.passDirectly(parameterName),
swiftType: instanceType
),
cdeclParameters: [
SwiftParameter(
convention: .byValue,
parameterName: parameterName,
type: .nominal(
SwiftNominalType(
nominalTypeDecl: swiftStdlibTypes.unsafeRawPointerDecl
)
)
)
],
javaFFMParameters: [.SwiftPointer]
)

case .nominal(let nominal):
// Types from the Swift standard library that we know about.
if nominal.nominalTypeDecl.moduleName == "Swift",
Expand All @@ -145,8 +168,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 +200,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 +285,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 +301,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 +364,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 +484,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
Loading

0 comments on commit 98129ae

Please sign in to comment.