diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift b/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift index 6b49774f..5e0c5b1c 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift @@ -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, @@ -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", @@ -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( @@ -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 } ) @@ -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): @@ -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")) ] @@ -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) @@ -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 + } +} diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftFunctionSignature.swift b/Sources/JExtractSwift/SwiftTypes/SwiftFunctionSignature.swift index 187c18a6..c2b626f2 100644 --- a/Sources/JExtractSwift/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/JExtractSwift/SwiftTypes/SwiftFunctionSignature.swift @@ -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] @@ -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 } """ @@ -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 diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift b/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift index b7bc28fb..35b7a0e1 100644 --- a/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift +++ b/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift @@ -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 { diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftType.swift b/Sources/JExtractSwift/SwiftTypes/SwiftType.swift index 4eef607a..d83d5e63 100644 --- a/Sources/JExtractSwift/SwiftTypes/SwiftType.swift +++ b/Sources/JExtractSwift/SwiftTypes/SwiftType.swift @@ -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): @@ -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" + } } diff --git a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift index ca101133..b5304b71 100644 --- a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift @@ -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( diff --git a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift index fd909565..08187914 100644 --- a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift +++ b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift @@ -17,6 +17,7 @@ import SwiftSyntax import SwiftSyntaxBuilder import Testing +@Suite("Swift function lowering tests") final class FunctionLoweringTests { @Test("Lowering buffer pointers") func loweringBufferPointers() throws { @@ -24,8 +25,9 @@ final class FunctionLoweringTests { func f(x: Int, y: Swift.Float, z: UnsafeBufferPointer<Bool>) { } """, expectedCDecl: """ - func f(_ x: Int, _ y: Float, _ z_pointer: UnsafeRawPointer, _ z_count: Int) -> () { - // implementation + @_cdecl("c_f") + func c_f(_ x: Int, _ y: Float, _ z_pointer: UnsafeRawPointer, _ z_count: Int) { + f(x: x, y: y, z: UnsafeBufferPointer<Bool>(start: z_pointer.assumingMemoryBound(to: Bool.self), count: z_count)) } """ ) @@ -34,16 +36,33 @@ final class FunctionLoweringTests { @Test("Lowering tuples") func loweringTuples() throws { try assertLoweredFunction(""" - func f(t: (Int, (Float, Double)), z: UnsafePointer<Int>) { } + func f(t: (Int, (Float, Double)), z: UnsafePointer<Int>) -> Int { } """, expectedCDecl: """ - func f(_ t_0: Int, _ t_1_0: Float, _ t_1_1: Double, _ z_pointer: UnsafeRawPointer) -> () { - // implementation + @_cdecl("c_f") + func c_f(_ t_0: Int, _ t_1_0: Float, _ t_1_1: Double, _ z_pointer: UnsafeRawPointer) -> Int { + return f(t: (t_0, (t_1_0, t_1_1)), z: z_pointer.assumingMemoryBound(to: Int.self)) } """ ) } + @Test("Lowering functions involving inout") + func loweringInoutParameters() throws { + try assertLoweredFunction(""" + func shift(point: inout Point, by delta: (Double, Double)) { } + """, + sourceFile: """ + struct Point { } + """, + expectedCDecl: """ + @_cdecl("c_shift") + func c_shift(_ point: UnsafeMutableRawPointer, _ delta_0: Double, _ delta_1: Double) { + shift(point: &point.assumingMemoryBound(to: Point.self).pointee, by: (delta_0, delta_1)) + } + """ + ) + } @Test("Lowering methods") func loweringMethods() throws { try assertLoweredFunction(""" @@ -54,21 +73,59 @@ final class FunctionLoweringTests { """, enclosingType: "Point", expectedCDecl: """ - func shifted(_ self: UnsafeRawPointer, _ delta_0: Double, _ delta_1: Double, _ _result: UnsafeMutableRawPointer) -> () { - // implementation + @_cdecl("c_shifted") + func c_shifted(_ self: UnsafeRawPointer, _ delta_0: Double, _ delta_1: Double, _ _result: UnsafeMutableRawPointer) { + _result.assumingMemoryBound(to: Point.self).pointee = self.assumingMemoryBound(to: Point.self).pointee.shifted(by: (delta_0, delta_1)) + } + """ + ) + } + + @Test("Lowering mutating methods") + func loweringMutatingMethods() throws { + try assertLoweredFunction(""" + mutating func shift(by delta: (Double, Double)) { } + """, + sourceFile: """ + struct Point { } + """, + enclosingType: "Point", + expectedCDecl: """ + @_cdecl("c_shift") + func c_shift(_ self: UnsafeMutableRawPointer, _ delta_0: Double, _ delta_1: Double) { + self.assumingMemoryBound(to: Point.self).pointee.shift(by: (delta_0, delta_1)) + } + """ + ) + } + + @Test("Lowering instance methods of classes") + func loweringInstanceMethodsOfClass() throws { + try assertLoweredFunction(""" + func shift(by delta: (Double, Double)) { } + """, + sourceFile: """ + class Point { } + """, + enclosingType: "Point", + expectedCDecl: """ + @_cdecl("c_shift") + func c_shift(_ self: UnsafeRawPointer, _ delta_0: Double, _ delta_1: Double) { + unsafeBitCast(self, to: Point.self).shift(by: (delta_0, delta_1)) } """ ) } - @Test("Lowering metatypes", .disabled("Metatypes are not yet lowered")) + @Test("Lowering metatypes") func lowerMetatype() throws { try assertLoweredFunction(""" func f(t: Int.Type) { } """, expectedCDecl: """ - func f(t: RawPointerType) -> () { - // implementation + @_cdecl("c_f") + func c_f(_ t: UnsafeRawPointer) { + f(t: unsafeBitCast(t, to: Int.self)) } """ )