From 2ca6d55db815eadf6f09331e2cdf0f09a2491c8c Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Thu, 30 Jan 2025 16:24:03 -0800 Subject: [PATCH 01/15] C lowering: drop the Java FFM type from this lowering We're going to separate out the lowering of Swift declarations to C from the handling of those C declarations on the Java side. That's cleaner and more generalizable. --- ...wift2JavaTranslator+FunctionLowering.swift | 48 ++++++++----------- .../SwiftTypes/SwiftParameter.swift | 1 + 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift b/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift index 5e0c5b1c..ffb550b1 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift @@ -64,19 +64,22 @@ extension Swift2JavaTranslator { ) // If the result type doesn't lower to either empty (void) or a single - // primitive result, make it indirect. + // result, make it indirect. let indirectResult: Bool - if !(loweredResult.javaFFMParameters.count == 0 || - (loweredResult.javaFFMParameters.count == 1 && - loweredResult.javaFFMParameters[0].isPrimitive)) { + if loweredResult.cdeclParameters.count == 0 { + // void result type + indirectResult = false + } else if loweredResult.cdeclParameters.count == 1, + loweredResult.cdeclParameters[0].isPrimitive { + // Primitive result type + indirectResult = false + } else { loweredResult = try lowerParameter( signature.result.type, convention: .inout, parameterName: "_result" ) indirectResult = true - } else { - indirectResult = false } // Collect all of the lowered parameters for the @_cdecl function. @@ -146,8 +149,7 @@ extension Swift2JavaTranslator { ) ) ) - ], - javaFFMParameters: [.SwiftPointer] + ] ) case .nominal(let nominal): @@ -190,8 +192,7 @@ extension Swift2JavaTranslator { ) ) ) - ], - javaFFMParameters: [.SwiftPointer] + ] ) case .tuple(let tuple): @@ -201,8 +202,7 @@ extension Swift2JavaTranslator { } return LoweredParameters( cdeclToOriginal: .tuplify(loweredElements.map { $0.cdeclToOriginal }), - cdeclParameters: loweredElements.flatMap { $0.cdeclParameters }, - javaFFMParameters: loweredElements.flatMap { $0.javaFFMParameters } + cdeclParameters: loweredElements.flatMap { $0.cdeclParameters } ) } } @@ -217,6 +217,9 @@ extension Swift2JavaTranslator { // Swift types that map directly to Java primitive types. if let primitiveType = JavaType(swiftTypeName: nominalName) { + // FIXME: Should be using C types here, not Java types. + _ = primitiveType + // We cannot handle inout on primitive types. if convention == .inout { throw LoweringError.inoutNotSupported(type) @@ -228,11 +231,9 @@ extension Swift2JavaTranslator { SwiftParameter( convention: convention, parameterName: parameterName, - type: type + type: type, + isPrimitive: true ) - ], - javaFFMParameters: [ - ForeignValueLayout(javaType: primitiveType)! ] ) } @@ -251,11 +252,9 @@ extension Swift2JavaTranslator { SwiftParameter( convention: convention, parameterName: parameterName, - type: type + type: type, + isPrimitive: true ) - ], - javaFFMParameters: [ - .SwiftInt ] ) } @@ -351,8 +350,7 @@ extension Swift2JavaTranslator { return LoweredParameters( cdeclToOriginal: cdeclToOriginal, - cdeclParameters: lowered.map(\.0), - javaFFMParameters: lowered.map(\.1) + cdeclParameters: lowered.map(\.0) ) } } @@ -405,12 +403,6 @@ struct LoweredParameters: Equatable { /// The lowering of the parameters at the C level in Swift. var cdeclParameters: [SwiftParameter] - - /// 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 { diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift b/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift index 35b7a0e1..15b905a4 100644 --- a/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift +++ b/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift @@ -19,6 +19,7 @@ struct SwiftParameter: Equatable { var argumentLabel: String? var parameterName: String? var type: SwiftType + var isPrimitive = false } extension SwiftParameter: CustomStringConvertible { From 4fdef82f956753001f14357500e87f6e93ad9314 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Thu, 30 Jan 2025 20:36:13 -0800 Subject: [PATCH 02/15] Introduce a C type system for lowering purposes Swift functions are lowered into C-compatible thunks. Those thunks currently have their types represented in Swift, which is needed for the `@_cdecl` declaration. Introduce a representation of the C type system so that we can describe the specific C types that each parameter has. This intentionally represents C types in an abstract form that fits will with the mapping from Swift, for example representing integral types by the number of bits and their signedness, rather than the actual C primitive types like `int` or `unsigned long`. Implement printing of C types and functions, in case we decide that we want to render C declarations for consumption by humans or tools that want to bind to them. --- Sources/JExtractSwift/CTypes/CEnum.swift | 21 ++ Sources/JExtractSwift/CTypes/CFunction.swift | 72 +++++++ Sources/JExtractSwift/CTypes/CParameter.swift | 33 +++ Sources/JExtractSwift/CTypes/CStruct.swift | 22 ++ Sources/JExtractSwift/CTypes/CTag.swift | 29 +++ Sources/JExtractSwift/CTypes/CType.swift | 197 ++++++++++++++++++ Sources/JExtractSwift/CTypes/CUnion.swift | 21 ++ Tests/JExtractSwiftTests/CTypeTests.swift | 73 +++++++ 8 files changed, 468 insertions(+) create mode 100644 Sources/JExtractSwift/CTypes/CEnum.swift create mode 100644 Sources/JExtractSwift/CTypes/CFunction.swift create mode 100644 Sources/JExtractSwift/CTypes/CParameter.swift create mode 100644 Sources/JExtractSwift/CTypes/CStruct.swift create mode 100644 Sources/JExtractSwift/CTypes/CTag.swift create mode 100644 Sources/JExtractSwift/CTypes/CType.swift create mode 100644 Sources/JExtractSwift/CTypes/CUnion.swift create mode 100644 Tests/JExtractSwiftTests/CTypeTests.swift diff --git a/Sources/JExtractSwift/CTypes/CEnum.swift b/Sources/JExtractSwift/CTypes/CEnum.swift new file mode 100644 index 00000000..3caddb96 --- /dev/null +++ b/Sources/JExtractSwift/CTypes/CEnum.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Describes a C enum type. +public class CEnum { + public var name: String + public init(name: String) { + self.name = name + } +} diff --git a/Sources/JExtractSwift/CTypes/CFunction.swift b/Sources/JExtractSwift/CTypes/CFunction.swift new file mode 100644 index 00000000..9dc69e41 --- /dev/null +++ b/Sources/JExtractSwift/CTypes/CFunction.swift @@ -0,0 +1,72 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Describes a C function. +public struct CFunction { + /// The result type of the function. + public var resultType: CType + + /// The name of the function. + public var name: String + + /// The parameters of the function. + public var parameters: [CParameter] + + /// Whether the function is variadic. + public var isVariadic: Bool + + public init(resultType: CType, name: String, parameters: [CParameter], isVariadic: Bool) { + self.resultType = resultType + self.name = name + self.parameters = parameters + self.isVariadic = isVariadic + } + + /// Produces the type of the function. + public var functionType: CType { + .function( + resultType: resultType, + parameters: parameters.map { $0.type }, + variadic: isVariadic + ) + } +} + +extension CFunction: CustomStringConvertible { + /// Print the declaration of this C function + public var description: String { + var result = "" + + resultType.printBefore(result: &result) + + // FIXME: parentheses when needed. + result += " " + result += name + + // Function parameters. + result += "(" + result += parameters.map { $0.description }.joined(separator: ", ") + CType.printFunctionParametersSuffix( + isVariadic: isVariadic, + hasZeroParameters: parameters.isEmpty, + to: &result + ) + result += ")" + + resultType.printAfter(result: &result) + + result += "" + return result + } +} diff --git a/Sources/JExtractSwift/CTypes/CParameter.swift b/Sources/JExtractSwift/CTypes/CParameter.swift new file mode 100644 index 00000000..500af4b9 --- /dev/null +++ b/Sources/JExtractSwift/CTypes/CParameter.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Describes a parameter to a C function. +public struct CParameter { + /// The name of the parameter, if provided. + public var name: String? + + /// The type of the parameter. + public var type: CType + + public init(name: String? = nil, type: CType) { + self.name = name + self.type = type + } +} + +extension CParameter: CustomStringConvertible { + public var description: String { + type.print(placeholder: name ?? "") + } +} diff --git a/Sources/JExtractSwift/CTypes/CStruct.swift b/Sources/JExtractSwift/CTypes/CStruct.swift new file mode 100644 index 00000000..c7e3ab68 --- /dev/null +++ b/Sources/JExtractSwift/CTypes/CStruct.swift @@ -0,0 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Describes a C struct type. +public class CStruct { + public var name: String + + public init(name: String) { + self.name = name + } +} diff --git a/Sources/JExtractSwift/CTypes/CTag.swift b/Sources/JExtractSwift/CTypes/CTag.swift new file mode 100644 index 00000000..3e26d2db --- /dev/null +++ b/Sources/JExtractSwift/CTypes/CTag.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Describes a tag type in C, which is either a struct or an enum. +public enum CTag { + case `struct`(CStruct) + case `enum`(CEnum) + case `union`(CUnion) + + public var name: String { + switch self { + case .struct(let cStruct): return cStruct.name + case .enum(let cEnum): return cEnum.name + case .union(let cUnion): return cUnion.name + } + } +} + diff --git a/Sources/JExtractSwift/CTypes/CType.swift b/Sources/JExtractSwift/CTypes/CType.swift new file mode 100644 index 00000000..54716b90 --- /dev/null +++ b/Sources/JExtractSwift/CTypes/CType.swift @@ -0,0 +1,197 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Describes a type in the C type system as it is used for lowering of Swift +/// declarations to C. +/// +/// This description of the C type system only has to account for the types +/// that are used when providing C-compatible thunks from Swift code. It is +/// not a complete representation of the C type system, and leaves some +/// target-specific types (like the actual type that ptrdiff_t and size_t +/// map to) unresolved. +public enum CType { + /// A tag type, such as a struct or enum. + case tag(CTag) + + /// An integral type. + case integral(IntegralType) + + /// A floating-point type. + case floating(FloatingType) + + case void + + /// A qualiied type, such as 'const T'. + indirect case qualified(const: Bool, volatile: Bool, type: CType) + + /// A pointer to the given type. + indirect case pointer(CType) + + /// A function type. + indirect case function(resultType: CType, parameters: [CType], variadic: Bool) + + /// An integral type in C, described mostly in terms of the bit-sized + /// typedefs rather than actual C types (like int or long), because Swift + /// deals in bit-widths. + public enum IntegralType { + case bool + + /// A signed integer type stored with the given number of bits. This + /// corresponds to the intNNN_t types from . + case signed(bits: Int) + + /// An unsigned integer type stored with the given number of bits. This + /// corresponds to the uintNNN_t types from . + case unsigned(bits: Int) + + /// The ptrdiff_t type, which in C is a typedef for the signed integer + /// type that is the same size as a pointer. + case ptrdiff_t + + /// The size_t type, which in C is a typedef for the unsigned integer + /// type that is the same size as a pointer. + case size_t + } + + /// A floating point type in C. + public enum FloatingType { + case float + case double + } +} + +extension CType: CustomStringConvertible { + /// Print the part of this type that comes before the declarator, appending + /// it to the provided `result` string. + func printBefore(result: inout String) { + switch self { + case .floating(let floating): + switch floating { + case .float: result += "float" + case .double: result += "double" + } + + case .function(resultType: let resultType, parameters: _, variadic: _): + resultType.printBefore(result: &result) + + // FIXME: Clang inserts a parentheses in here if there's a non-empty + // placeholder, which is Very Stateful. How should I model that? + + case .integral(let integral): + switch integral { + case .bool: result += "_Bool" + case .signed(let bits): result += "int\(bits)_t" + case .unsigned(let bits): result += "uint\(bits)_t" + case .ptrdiff_t: result += "ptrdiff_t" + case .size_t: result += "size_t" + } + + case .pointer(let pointee): + pointee.printBefore(result: &result) + result += "*" + + case .qualified(const: let isConst, volatile: let isVolatile, type: let underlying): + underlying.printBefore(result: &result) + + // FIXME: "east const" is easier to print correctly, so do that. We could + // follow Clang and decide when it's correct to print "west const" by + // splitting the qualifiers before we get here. + if isConst { + result += " const" + } + if isVolatile { + result += " volatile" + } + + case .tag(let tag): + switch tag { + case .enum(let cEnum): result += "enum \(cEnum.name)" + case .struct(let cStruct): result += "struct \(cStruct.name)" + case .union(let cUnion): result += "union \(cUnion.name)" + } + + case .void: result += "void" + } + } + + /// Render an appropriate "suffix" to the parameter list of a function, + /// which goes just before the closing ")" of that function, appending + /// it to the string. This includes whether the function is variadic and + /// whether is had zero parameters. + static func printFunctionParametersSuffix( + isVariadic: Bool, + hasZeroParameters: Bool, + to result: inout String + ) { + // Take care of variadic parameters and empty parameter lists together, + // because the formatter of the former depends on the latter. + switch (isVariadic, hasZeroParameters) { + case (true, false): result += ", ..." + case (true, true): result += "..." + case (false, true): result += "void" + case (false, false): break + } + } + + /// Print the part of the type that comes after the declarator, appending + /// it to the provided `result` string. + func printAfter(result: inout String) { + switch self { + case .floating, .integral, .tag, .void: break + + case .function(resultType: let resultType, parameters: let parameters, variadic: let variadic): + // FIXME: Clang inserts a parentheses in here if there's a non-empty + // placeholder, which is Very Stateful. How should I model that? + + result += "(" + + // Render the parameter types. + result += parameters.map { $0.description }.joined(separator: ", ") + + CType.printFunctionParametersSuffix( + isVariadic: variadic, + hasZeroParameters: parameters.isEmpty, + to: &result + ) + + result += ")" + + resultType.printAfter(result: &result) + + case .pointer(let pointee): + pointee.printAfter(result: &result) + + case .qualified(const: _, volatile: _, type: let underlying): + underlying.printAfter(result: &result) + } + } + + /// Print this type into a string, with the given placeholder as the name + /// of the entity being declared. + public func print(placeholder: String?) -> String { + var result = "" + printBefore(result: &result) + if let placeholder { + result += " " + result += placeholder + } + printAfter(result: &result) + return result + } + + /// Render the C type into a string that represents the type in C. + public var description: String { + print(placeholder: nil) + } +} diff --git a/Sources/JExtractSwift/CTypes/CUnion.swift b/Sources/JExtractSwift/CTypes/CUnion.swift new file mode 100644 index 00000000..e8d68c15 --- /dev/null +++ b/Sources/JExtractSwift/CTypes/CUnion.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// Describes a C union type. +public class CUnion { + public var name: String + public init(name: String) { + self.name = name + } +} diff --git a/Tests/JExtractSwiftTests/CTypeTests.swift b/Tests/JExtractSwiftTests/CTypeTests.swift new file mode 100644 index 00000000..c60a38b6 --- /dev/null +++ b/Tests/JExtractSwiftTests/CTypeTests.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwift +import Testing + +@Suite("C type system tests") +struct CTypeTests { + @Test("Function declaration printing") + func testFunctionDeclarationPrint() { + let malloc = CFunction( + resultType: .pointer(.void), + name: "malloc", + parameters: [ + CParameter(name: "size", type: .integral(.size_t)) + ], + isVariadic: false + ) + #expect(malloc.description == "void* malloc(size_t size)") + + let free = CFunction( + resultType: .void, + name: "free", + parameters: [ + CParameter(name: "ptr", type: .pointer(.void)) + ], + isVariadic: false + ) + #expect(free.description == "void free(void* ptr)") + + let snprintf = CFunction( + resultType: .integral(.signed(bits: 32)), + name: "snprintf", + parameters: [ + CParameter(name: "str", type: .pointer(.integral(.signed(bits: 8)))), + CParameter(name: "size", type: .integral(.size_t)), + CParameter( + name: "format", + type: .pointer( + .qualified( + const: true, + volatile: false, + type: .integral(.signed(bits: 8)) + ) + ) + ) + ], + isVariadic: true + ) + #expect(snprintf.description == "int32_t snprintf(int8_t* str, size_t size, int8_t const* format, ...)") + #expect(snprintf.functionType.description == "int32_t(int8_t*, size_t, int8_t const*, ...)") + + let rand = CFunction( + resultType: .integral(.signed(bits: 32)), + name: "rand", + parameters: [], + isVariadic: false + ) + #expect(rand.description == "int32_t rand(void)") + #expect(rand.functionType.description == "int32_t(void)") + } +} From 68d48d4c3717949d1a6f85e0821ba0e95dc0edce Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Fri, 31 Jan 2025 13:55:53 +0000 Subject: [PATCH 03/15] Implement lowering of Swift cdecl functions to C functions Leverage the new C type system so that we can lower Swift cdecl functions down to their C representation. This could be used to generate C headers (albeit ugly ones) in the future, but for now is part of the validation of lowering Swift functions to cdecl thunks. --- Sources/JExtractSwift/CTypes/CType.swift | 13 + ...wift2JavaTranslator+FunctionLowering.swift | 39 ++- .../SwiftStandardLibraryTypes+CLowering.swift | 85 +++++++ .../SwiftStandardLibraryTypes.swift | 236 ++++++++---------- .../Asserts/LoweringAssertions.swift | 15 ++ .../FunctionLoweringTests.swift | 22 +- 6 files changed, 267 insertions(+), 143 deletions(-) create mode 100644 Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes+CLowering.swift diff --git a/Sources/JExtractSwift/CTypes/CType.swift b/Sources/JExtractSwift/CTypes/CType.swift index 54716b90..e69e89f4 100644 --- a/Sources/JExtractSwift/CTypes/CType.swift +++ b/Sources/JExtractSwift/CTypes/CType.swift @@ -195,3 +195,16 @@ extension CType: CustomStringConvertible { print(placeholder: nil) } } + +extension CType { + /// Apply the rules for function parameter decay to produce the resulting + /// decayed type. For example, this will adjust a function type to a + /// pointer-to-function type. + var parameterDecay: CType { + switch self { + case .floating, .integral, .pointer, .qualified, .tag, .void: self + + case .function: .pointer(self) + } + } +} diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift b/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift index ffb550b1..af168139 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift @@ -145,7 +145,7 @@ extension Swift2JavaTranslator { parameterName: parameterName, type: .nominal( SwiftNominalType( - nominalTypeDecl: swiftStdlibTypes.unsafeRawPointerDecl + nominalTypeDecl: swiftStdlibTypes[.unsafeRawPointer] ) ) ) @@ -187,8 +187,8 @@ extension Swift2JavaTranslator { type: .nominal( SwiftNominalType( nominalTypeDecl: mutable - ? swiftStdlibTypes.unsafeMutableRawPointerDecl - : swiftStdlibTypes.unsafeRawPointerDecl + ? swiftStdlibTypes[.unsafeMutableRawPointer] + : swiftStdlibTypes[.unsafeRawPointer] ) ) ) @@ -276,8 +276,8 @@ extension Swift2JavaTranslator { // At the @_cdecl level, make everything a raw pointer. let cdeclPointerType = mutable - ? swiftStdlibTypes.unsafeMutableRawPointerDecl - : swiftStdlibTypes.unsafeRawPointerDecl + ? swiftStdlibTypes[.unsafeMutableRawPointer] + : swiftStdlibTypes[.unsafeRawPointer] var cdeclToOriginal: LoweringStep switch (requiresArgument, hasCount) { case (false, false): @@ -327,7 +327,7 @@ extension Swift2JavaTranslator { convention: convention, parameterName: parameterName + "_count", type: SwiftType.nominal( - SwiftNominalType(nominalTypeDecl: swiftStdlibTypes.intDecl) + SwiftNominalType(nominalTypeDecl: swiftStdlibTypes[.int]) ) ), .SwiftInt @@ -353,6 +353,33 @@ extension Swift2JavaTranslator { cdeclParameters: lowered.map(\.0) ) } + + /// Given a Swift function signature that represents a @_cdecl function, + /// produce the equivalent C function with the given name. + /// + /// Lowering to a @_cdecl function should never produce a + @_spi(Testing) + public func cdeclToCFunctionLowering( + _ cdeclSignature: SwiftFunctionSignature, + cName: String + ) -> CFunction { + assert(cdeclSignature.selfParameter == nil) + + let cResultType = try! swiftStdlibTypes.cdeclToCLowering(cdeclSignature.result.type) + let cParameters = cdeclSignature.parameters.map { parameter in + CParameter( + name: parameter.parameterName, + type: try! swiftStdlibTypes.cdeclToCLowering(parameter.type).parameterDecay + ) + } + + return CFunction( + resultType: cResultType, + name: cName, + parameters: cParameters, + isVariadic: false + ) + } } struct LabeledArgument { diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes+CLowering.swift b/Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes+CLowering.swift new file mode 100644 index 00000000..3913f9cc --- /dev/null +++ b/Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes+CLowering.swift @@ -0,0 +1,85 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +extension SwiftStandardLibraryTypes { + /// Lower the given Swift type down to a its corresponding C type. + /// + /// This operation only supports the subset of Swift types that are + /// representable in a Swift `@_cdecl` function. If lowering an arbitrary + /// Swift function, first go through Swift -> cdecl lowering. + func cdeclToCLowering(_ swiftType: SwiftType) throws -> CType { + switch swiftType { + case .nominal(let nominalType): + if let knownType = self[nominalType.nominalTypeDecl] { + return try knownType.loweredCType() + } + + throw CDeclToCLoweringError.invalidNominalType(nominalType.nominalTypeDecl) + + case .function(let functionType): + switch functionType.convention { + case .swift: + throw CDeclToCLoweringError.invalidFunctionConvention(functionType) + + case .c: + let resultType = try cdeclToCLowering(functionType.resultType) + let parameterTypes = try functionType.parameters.map { param in + try cdeclToCLowering(param.type) + } + + return .function( + resultType: resultType, + parameters: parameterTypes, + variadic: false + ) + } + + case .tuple([]): + return .void + + case .metatype, .optional, .tuple: + throw CDeclToCLoweringError.invalidCDeclType(swiftType) + } + } +} + +extension KnownStandardLibraryType { + func loweredCType() throws -> CType { + switch self { + case .bool: .integral(.bool) + case .int: .integral(.ptrdiff_t) + case .uint: .integral(.size_t) + case .int8: .integral(.signed(bits: 8)) + case .uint8: .integral(.unsigned(bits: 8)) + case .int16: .integral(.signed(bits: 16)) + case .uint16: .integral(.unsigned(bits: 16)) + case .int32: .integral(.signed(bits: 32)) + case .uint32: .integral(.unsigned(bits: 32)) + case .int64: .integral(.signed(bits: 64)) + case .uint64: .integral(.unsigned(bits: 64)) + case .float: .floating(.float) + case .double: .floating(.double) + case .unsafeMutableRawPointer: .pointer(.void) + case .unsafeRawPointer: .pointer( + .qualified(const: true, volatile: false, type: .void) + ) + } + } +} +enum CDeclToCLoweringError: Error { + case invalidCDeclType(SwiftType) + case invalidNominalType(SwiftNominalTypeDeclaration) + case invalidFunctionConvention(SwiftFunctionType) +} + diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes.swift b/Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes.swift index 57a5865f..b9689e9c 100644 --- a/Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes.swift +++ b/Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes.swift @@ -14,75 +14,101 @@ import SwiftSyntax +enum KnownStandardLibraryType: Int, Hashable, CaseIterable { + case bool = 0 + case int + case uint + case int8 + case uint8 + case int16 + case uint16 + case int32 + case uint32 + case int64 + case uint64 + case float + case double + case unsafeRawPointer + case unsafeMutableRawPointer + + var typeName: String { + switch self { + case .bool: return "Bool" + case .int: return "Int" + case .uint: return "UInt" + case .int8: return "Int8" + case .uint8: return "UInt8" + case .int16: return "Int16" + case .uint16: return "UInt16" + case .int32: return "Int32" + case .uint32: return "UInt32" + case .int64: return "Int64" + case .uint64: return "UInt64" + case .float: return "Float" + case .double: return "Double" + case .unsafeRawPointer: return "UnsafeRawPointer" + case .unsafeMutableRawPointer: return "UnsafeMutableRawPointer" + } + } + + var isGeneric: Bool { + false + } +} + /// Captures many types from the Swift standard library in their most basic /// forms, so that the translator can reason about them in source code. struct SwiftStandardLibraryTypes { - /// Swift.UnsafeRawPointer - var unsafeRawPointerDecl: SwiftNominalTypeDeclaration - - /// Swift.UnsafeMutableRawPointer - var unsafeMutableRawPointerDecl: SwiftNominalTypeDeclaration - // Swift.UnsafePointer - var unsafePointerDecl: SwiftNominalTypeDeclaration + let unsafePointerDecl: SwiftNominalTypeDeclaration // Swift.UnsafeMutablePointer - var unsafeMutablePointerDecl: SwiftNominalTypeDeclaration + let unsafeMutablePointerDecl: SwiftNominalTypeDeclaration // Swift.UnsafeBufferPointer - var unsafeBufferPointerDecl: SwiftNominalTypeDeclaration + let unsafeBufferPointerDecl: SwiftNominalTypeDeclaration // Swift.UnsafeMutableBufferPointer - var unsafeMutableBufferPointerDecl: SwiftNominalTypeDeclaration - - /// Swift.Bool - var boolDecl: SwiftNominalTypeDeclaration - - /// Swift.Int8 - var int8Decl: SwiftNominalTypeDeclaration - - /// Swift.Int16 - var int16Decl: SwiftNominalTypeDeclaration - - /// Swift.UInt16 - var uint16Decl: SwiftNominalTypeDeclaration - - /// Swift.Int32 - var int32Decl: SwiftNominalTypeDeclaration - - /// Swift.Int64 - var int64Decl: SwiftNominalTypeDeclaration - - /// Swift.Int - var intDecl: SwiftNominalTypeDeclaration - - /// Swift.Float - var floatDecl: SwiftNominalTypeDeclaration - - /// Swift.Double - var doubleDecl: SwiftNominalTypeDeclaration - - /// Swift.String - var stringDecl: SwiftNominalTypeDeclaration - - init(into parsedModule: inout SwiftParsedModuleSymbolTable) { - // Pointer types - self.unsafeRawPointerDecl = parsedModule.addNominalTypeDeclaration( - StructDeclSyntax( - name: .identifier("UnsafeRawPointer"), - memberBlock: .init(members: []) - ), - parent: nil - ) + let unsafeMutableBufferPointerDecl: SwiftNominalTypeDeclaration + + /// Mapping from known standard library types to their nominal type declaration. + let knownTypeToNominal: [KnownStandardLibraryType: SwiftNominalTypeDeclaration] + + /// Mapping from nominal type declarations to known types. + let nominalTypeDeclToKnownType: [SwiftNominalTypeDeclaration: KnownStandardLibraryType] + + private static func recordKnownType( + _ type: KnownStandardLibraryType, + _ syntax: NominalTypeDeclSyntaxNode, + knownTypeToNominal: inout [KnownStandardLibraryType: SwiftNominalTypeDeclaration], + nominalTypeDeclToKnownType: inout [SwiftNominalTypeDeclaration: KnownStandardLibraryType], + parsedModule: inout SwiftParsedModuleSymbolTable + ) { + let nominalDecl = parsedModule.addNominalTypeDeclaration(syntax, parent: nil) + knownTypeToNominal[type] = nominalDecl + nominalTypeDeclToKnownType[nominalDecl] = type + } - self.unsafeMutableRawPointerDecl = parsedModule.addNominalTypeDeclaration( + private static func recordKnownNonGenericStruct( + _ type: KnownStandardLibraryType, + knownTypeToNominal: inout [KnownStandardLibraryType: SwiftNominalTypeDeclaration], + nominalTypeDeclToKnownType: inout [SwiftNominalTypeDeclaration: KnownStandardLibraryType], + parsedModule: inout SwiftParsedModuleSymbolTable + ) { + recordKnownType( + type, StructDeclSyntax( - name: .identifier("UnsafeMutableRawPointer"), + name: .identifier(type.typeName), memberBlock: .init(members: []) ), - parent: nil + knownTypeToNominal: &knownTypeToNominal, + nominalTypeDeclToKnownType: &nominalTypeDeclToKnownType, + parsedModule: &parsedModule ) + } + init(into parsedModule: inout SwiftParsedModuleSymbolTable) { + // Pointer types self.unsafePointerDecl = parsedModule.addNominalTypeDeclaration( StructDeclSyntax( name: .identifier("UnsafePointer"), @@ -127,82 +153,32 @@ struct SwiftStandardLibraryTypes { parent: nil ) - self.boolDecl = parsedModule.addNominalTypeDeclaration( - StructDeclSyntax( - name: .identifier("Bool"), - memberBlock: .init(members: []) - ), - parent: nil - ) - self.intDecl = parsedModule.addNominalTypeDeclaration( - StructDeclSyntax( - name: .identifier("Int"), - memberBlock: .init(members: []) - ), - parent: nil - ) - self.int8Decl = parsedModule.addNominalTypeDeclaration( - StructDeclSyntax( - name: .identifier("Int8"), - memberBlock: .init(members: []) - ), - parent: nil - ) - self.int16Decl = parsedModule.addNominalTypeDeclaration( - StructDeclSyntax( - name: .identifier("Int16"), - memberBlock: .init(members: []) - ), - parent: nil - ) - self.uint16Decl = parsedModule.addNominalTypeDeclaration( - StructDeclSyntax( - name: .identifier("UInt16"), - memberBlock: .init(members: []) - ), - parent: nil - ) - self.int32Decl = parsedModule.addNominalTypeDeclaration( - StructDeclSyntax( - name: .identifier("Int32"), - memberBlock: .init(members: []) - ), - parent: nil - ) - self.int64Decl = parsedModule.addNominalTypeDeclaration( - StructDeclSyntax( - name: .identifier("Int64"), - memberBlock: .init(members: []) - ), - parent: nil - ) - self.floatDecl = parsedModule.addNominalTypeDeclaration( - StructDeclSyntax( - name: .identifier("Float"), - memberBlock: .init(members: []) - ), - parent: nil - ) - self.doubleDecl = parsedModule.addNominalTypeDeclaration( - StructDeclSyntax( - name: .identifier("Double"), - memberBlock: .init(members: []) - ), - parent: nil - ) - self.intDecl = parsedModule.addNominalTypeDeclaration( - StructDeclSyntax( - name: .identifier("Int"), - memberBlock: .init(members: []) - ), - parent: nil - ) - self.stringDecl = parsedModule.addNominalTypeDeclaration( - StructDeclSyntax( - name: .identifier("String"), - memberBlock: .init(members: []) - ), - parent: nil - ) + var knownTypeToNominal: [KnownStandardLibraryType: SwiftNominalTypeDeclaration] = [:] + var nominalTypeDeclToKnownType: [SwiftNominalTypeDeclaration: KnownStandardLibraryType] = [:] + + // Handle all of the non-generic types at once. + for knownType in KnownStandardLibraryType.allCases { + guard !knownType.isGeneric else { + continue + } + + Self.recordKnownNonGenericStruct( + knownType, + knownTypeToNominal: &knownTypeToNominal, + nominalTypeDeclToKnownType: &nominalTypeDeclToKnownType, + parsedModule: &parsedModule + ) + } + + self.knownTypeToNominal = knownTypeToNominal + self.nominalTypeDeclToKnownType = nominalTypeDeclToKnownType + } + + subscript(knownType: KnownStandardLibraryType) -> SwiftNominalTypeDeclaration { + knownTypeToNominal[knownType]! + } + + subscript(nominalType: SwiftNominalTypeDeclaration) -> KnownStandardLibraryType? { + nominalTypeDeclToKnownType[nominalType] } } diff --git a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift index b5304b71..3be4931c 100644 --- a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift @@ -25,6 +25,7 @@ func assertLoweredFunction( sourceFile: SourceFileSyntax? = nil, enclosingType: TypeSyntax? = nil, expectedCDecl: DeclSyntax, + expectedCFunction: String, fileID: String = #fileID, filePath: String = #filePath, line: Int = #line, @@ -57,4 +58,18 @@ func assertLoweredFunction( column: column ) ) + + let cFunction = translator.cdeclToCFunctionLowering( + loweredFunction.cdecl, + cName: "c_\(inputFunction.name.text)" + ) + #expect( + cFunction.description == expectedCFunction, + sourceLocation: Testing.SourceLocation( + fileID: fileID, + filePath: filePath, + line: line, + column: column + ) + ) } diff --git a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift index 08187914..e929a5fa 100644 --- a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift +++ b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift @@ -29,7 +29,8 @@ final class FunctionLoweringTests { func c_f(_ x: Int, _ y: Float, _ z_pointer: UnsafeRawPointer, _ z_count: Int) { f(x: x, y: y, z: UnsafeBufferPointer(start: z_pointer.assumingMemoryBound(to: Bool.self), count: z_count)) } - """ + """, + expectedCFunction: "void c_f(ptrdiff_t x, float y, void const* z_pointer, ptrdiff_t z_count)" ) } @@ -43,7 +44,8 @@ final class FunctionLoweringTests { 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)) } - """ + """, + expectedCFunction: "ptrdiff_t c_f(ptrdiff_t t_0, float t_1_0, double t_1_1, void const* z_pointer)" ) } @@ -60,9 +62,11 @@ final class FunctionLoweringTests { func c_shift(_ point: UnsafeMutableRawPointer, _ delta_0: Double, _ delta_1: Double) { shift(point: &point.assumingMemoryBound(to: Point.self).pointee, by: (delta_0, delta_1)) } - """ + """, + expectedCFunction: "void c_shift(void* point, double delta_0, double delta_1)" ) } + @Test("Lowering methods") func loweringMethods() throws { try assertLoweredFunction(""" @@ -77,7 +81,8 @@ final class FunctionLoweringTests { 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)) } - """ + """, + expectedCFunction: "void c_shifted(void const* self, double delta_0, double delta_1, void* _result)" ) } @@ -95,7 +100,8 @@ final class FunctionLoweringTests { func c_shift(_ self: UnsafeMutableRawPointer, _ delta_0: Double, _ delta_1: Double) { self.assumingMemoryBound(to: Point.self).pointee.shift(by: (delta_0, delta_1)) } - """ + """, + expectedCFunction: "void c_shift(void* self, double delta_0, double delta_1)" ) } @@ -113,7 +119,8 @@ final class FunctionLoweringTests { func c_shift(_ self: UnsafeRawPointer, _ delta_0: Double, _ delta_1: Double) { unsafeBitCast(self, to: Point.self).shift(by: (delta_0, delta_1)) } - """ + """, + expectedCFunction: "void c_shift(void const* self, double delta_0, double delta_1)" ) } @@ -127,7 +134,8 @@ final class FunctionLoweringTests { func c_f(_ t: UnsafeRawPointer) { f(t: unsafeBitCast(t, to: Int.self)) } - """ + """, + expectedCFunction: "void c_f(void const* t)" ) } } From 169b2f0f4eacce80426fb138193fbf69518c250b Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 3 Feb 2025 12:09:22 +0100 Subject: [PATCH 04/15] Use placeholders rather than parameter names in cdecl-to-swift sequences --- ...wift2JavaTranslator+FunctionLowering.swift | 82 +++++++++++-------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift b/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift index af168139..99e8227b 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift @@ -44,7 +44,8 @@ extension Swift2JavaTranslator { let loweredSelf = try signature.selfParameter.map { selfParameter in try lowerParameter( selfParameter.type, - convention: selfParameter.convention, parameterName: "self" + convention: selfParameter.convention, + parameterName: selfParameter.parameterName ?? "self" ) } @@ -136,7 +137,7 @@ extension Swift2JavaTranslator { case .metatype(let instanceType): return LoweredParameters( cdeclToOriginal: .unsafeCastPointer( - .passDirectly(parameterName), + .value, swiftType: instanceType ), cdeclParameters: [ @@ -172,10 +173,10 @@ extension Swift2JavaTranslator { switch nominal.nominalTypeDecl.kind { case .actor, .class: loweringStep = - .unsafeCastPointer(.passDirectly(parameterName), swiftType: type) + .unsafeCastPointer(.value, swiftType: type) case .enum, .struct, .protocol: loweringStep = - .passIndirectly(.pointee( .typedPointer(.passDirectly(parameterName), swiftType: type))) + .passIndirectly(.pointee(.typedPointer(.value, swiftType: type))) } return LoweredParameters( @@ -226,7 +227,7 @@ extension Swift2JavaTranslator { } return LoweredParameters( - cdeclToOriginal: .passDirectly(parameterName), + cdeclToOriginal: .value, cdeclParameters: [ SwiftParameter( convention: convention, @@ -247,7 +248,7 @@ extension Swift2JavaTranslator { } return LoweredParameters( - cdeclToOriginal: .passDirectly(parameterName), + cdeclToOriginal: .value, cdeclParameters: [ SwiftParameter( convention: convention, @@ -281,30 +282,34 @@ extension Swift2JavaTranslator { var cdeclToOriginal: LoweringStep switch (requiresArgument, hasCount) { case (false, false): - cdeclToOriginal = .passDirectly(parameterName) + cdeclToOriginal = .value case (true, false): cdeclToOriginal = .typedPointer( - .passDirectly(parameterName + "_pointer"), + .explodedComponent(.value, component: "pointer"), swiftType: nominal.genericArguments![0] ) case (false, true): cdeclToOriginal = .initialize(type, arguments: [ - LabeledArgument(label: "start", argument: .passDirectly(parameterName + "_pointer")), - LabeledArgument(label: "count", argument: .passDirectly(parameterName + "_count")) + LabeledArgument(label: "start", argument: .explodedComponent(.value, component: "pointer")), + LabeledArgument(label: "count", argument: .explodedComponent(.value, component: "count")) ]) case (true, true): cdeclToOriginal = .initialize( type, arguments: [ - LabeledArgument(label: "start", - argument: .typedPointer( - .passDirectly(parameterName + "_pointer"), - swiftType: nominal.genericArguments![0])), - LabeledArgument(label: "count", - argument: .passDirectly(parameterName + "_count")) + LabeledArgument( + label: "start", + argument: .typedPointer( + .explodedComponent(.value, component: "pointer"), + swiftType: nominal.genericArguments![0]) + ), + LabeledArgument( + label: "count", + argument: .explodedComponent(.value, component: "count") + ) ] ) } @@ -393,8 +398,12 @@ extension LabeledArgument: Equatable where Element: Equatable { } /// 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) + /// The value being lowered. + case value + + /// A reference to a component in a value that has been exploded, such as + /// a tuple element or part of a buffer pointer. + indirect case explodedComponent(LoweringStep, component: String) /// Cast the pointer described by the lowering step to the given /// Swift type using `unsafeBitCast(_:to:)`. @@ -435,36 +444,39 @@ struct LoweredParameters: Equatable { 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) + func cdeclToOriginalArgumentExpr(isSelf: Bool, value: String)-> ExprSyntax { + cdeclToOriginal.asExprSyntax(isSelf: isSelf, value: value) } } extension LoweringStep { - func asExprSyntax(isSelf: Bool) -> ExprSyntax { + func asExprSyntax(isSelf: Bool, value: String) -> ExprSyntax { switch self { - case .passDirectly(let rawArgument): - return "\(raw: rawArgument)" + case .value: + return "\(raw: value)" + + case .explodedComponent(let step, component: let component): + return step.asExprSyntax(isSelf: false, value: "\(value)_\(component)") case .unsafeCastPointer(let step, swiftType: let swiftType): - let untypedExpr = step.asExprSyntax(isSelf: false) + let untypedExpr = step.asExprSyntax(isSelf: false, value: value) return "unsafeBitCast(\(untypedExpr), to: \(swiftType.metatypeReferenceExprSyntax))" case .typedPointer(let step, swiftType: let type): - let untypedExpr = step.asExprSyntax(isSelf: isSelf) + let untypedExpr = step.asExprSyntax(isSelf: isSelf, value: value) return "\(untypedExpr).assumingMemoryBound(to: \(type.metatypeReferenceExprSyntax))" case .pointee(let step): - let untypedExpr = step.asExprSyntax(isSelf: isSelf) + let untypedExpr = step.asExprSyntax(isSelf: isSelf, value: value) return "\(untypedExpr).pointee" case .passIndirectly(let step): - let innerExpr = step.asExprSyntax(isSelf: false) + let innerExpr = step.asExprSyntax(isSelf: false, value: value) 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) + let renderedArg = labeledArgument.argument.asExprSyntax(isSelf: false, value: value) if let argmentLabel = labeledArgument.label { return "\(argmentLabel): \(renderedArg.description)" } else { @@ -478,8 +490,8 @@ extension LoweringStep { return "\(raw: type.description)(\(raw: renderedArgumentList))" case .tuplify(let elements): - let renderedElements: [String] = elements.map { element in - element.asExprSyntax(isSelf: false).description + let renderedElements: [String] = elements.enumerated().map { (index, element) in + element.asExprSyntax(isSelf: false, value: "\(value)_\(index)").description } // FIXME: Should be able to use structured initializers here instead @@ -520,8 +532,8 @@ extension LoweredFunctionSignature { // Lower "self", if there is one. let parametersToLower: ArraySlice let cdeclToOriginalSelf: ExprSyntax? - if original.selfParameter != nil { - cdeclToOriginalSelf = parameters[0].cdeclToOriginalArgumentExpr(isSelf: true) + if let originalSelfParam = original.selfParameter { + cdeclToOriginalSelf = parameters[0].cdeclToOriginalArgumentExpr(isSelf: true, value: originalSelfParam.parameterName ?? "self") parametersToLower = parameters[1...] } else { cdeclToOriginalSelf = nil @@ -529,10 +541,8 @@ extension LoweredFunctionSignature { } // 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) + let cdeclToOriginalArg = lowering.cdeclToOriginalArgumentExpr(isSelf: false, value: originalParam.parameterName ?? "FIXME") if let argumentLabel = originalParam.argumentLabel { return "\(argumentLabel): \(cdeclToOriginalArg.description)" } else { @@ -559,7 +569,7 @@ extension LoweredFunctionSignature { // into a loweredCDecl.body = """ { - \(result.cdeclToOriginalArgumentExpr(isSelf: true)) = \(callExpression) + \(result.cdeclToOriginalArgumentExpr(isSelf: true, value: "_result")) = \(callExpression) } """ } else { From aff35dd06844238197e69f2388c2a0a0155366ca Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 3 Feb 2025 12:20:14 +0100 Subject: [PATCH 05/15] Move "self" parameter to the end of the @_cdecl thunk This matches what we're doing elsewhere, and also the way that the Swift calling convention is lowered to LLVM IR by the compiler. --- ...wift2JavaTranslator+FunctionLowering.swift | 32 ++++++++++--------- .../FunctionLoweringTests.swift | 12 +++---- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift b/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift index 99e8227b..6375b92f 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift @@ -41,14 +41,6 @@ extension Swift2JavaTranslator { _ signature: SwiftFunctionSignature ) throws -> LoweredFunctionSignature { // Lower all of the parameters. - let loweredSelf = try signature.selfParameter.map { selfParameter in - try lowerParameter( - selfParameter.type, - convention: selfParameter.convention, - parameterName: selfParameter.parameterName ?? "self" - ) - } - let loweredParameters = try signature.parameters.enumerated().map { (index, param) in try lowerParameter( param.type, @@ -83,13 +75,17 @@ extension Swift2JavaTranslator { indirectResult = true } + let loweredSelf = try signature.selfParameter.map { selfParameter in + try lowerParameter( + selfParameter.type, + convention: selfParameter.convention, + parameterName: selfParameter.parameterName ?? "self" + ) + } + // Collect all of the lowered parameters for the @_cdecl function. var allLoweredParameters: [LoweredParameters] = [] var cdeclLoweredParameters: [SwiftParameter] = [] - if let loweredSelf { - allLoweredParameters.append(loweredSelf) - cdeclLoweredParameters.append(contentsOf: loweredSelf.cdeclParameters) - } allLoweredParameters.append(contentsOf: loweredParameters) cdeclLoweredParameters.append( contentsOf: loweredParameters.flatMap { $0.cdeclParameters } @@ -110,6 +106,11 @@ extension Swift2JavaTranslator { fatalError("Improper lowering of result for \(signature)") } + if let loweredSelf { + allLoweredParameters.append(loweredSelf) + cdeclLoweredParameters.append(contentsOf: loweredSelf.cdeclParameters) + } + let cdeclSignature = SwiftFunctionSignature( isStaticOrClass: false, selfParameter: nil, @@ -532,9 +533,10 @@ extension LoweredFunctionSignature { // Lower "self", if there is one. let parametersToLower: ArraySlice let cdeclToOriginalSelf: ExprSyntax? - if let originalSelfParam = original.selfParameter { - cdeclToOriginalSelf = parameters[0].cdeclToOriginalArgumentExpr(isSelf: true, value: originalSelfParam.parameterName ?? "self") - parametersToLower = parameters[1...] + if let originalSelfParam = original.selfParameter, + let selfParameter = parameters.last { + cdeclToOriginalSelf = selfParameter.cdeclToOriginalArgumentExpr(isSelf: true, value: originalSelfParam.parameterName ?? "self") + parametersToLower = parameters.dropLast() } else { cdeclToOriginalSelf = nil parametersToLower = parameters[...] diff --git a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift index e929a5fa..0c51ccf6 100644 --- a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift +++ b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift @@ -78,11 +78,11 @@ final class FunctionLoweringTests { enclosingType: "Point", expectedCDecl: """ @_cdecl("c_shifted") - func c_shifted(_ self: UnsafeRawPointer, _ delta_0: Double, _ delta_1: Double, _ _result: UnsafeMutableRawPointer) { + func c_shifted(_ delta_0: Double, _ delta_1: Double, _ _result: UnsafeMutableRawPointer, _ self: UnsafeRawPointer) { _result.assumingMemoryBound(to: Point.self).pointee = self.assumingMemoryBound(to: Point.self).pointee.shifted(by: (delta_0, delta_1)) } """, - expectedCFunction: "void c_shifted(void const* self, double delta_0, double delta_1, void* _result)" + expectedCFunction: "void c_shifted(double delta_0, double delta_1, void* _result, void const* self)" ) } @@ -97,11 +97,11 @@ final class FunctionLoweringTests { enclosingType: "Point", expectedCDecl: """ @_cdecl("c_shift") - func c_shift(_ self: UnsafeMutableRawPointer, _ delta_0: Double, _ delta_1: Double) { + func c_shift(_ delta_0: Double, _ delta_1: Double, _ self: UnsafeMutableRawPointer) { self.assumingMemoryBound(to: Point.self).pointee.shift(by: (delta_0, delta_1)) } """, - expectedCFunction: "void c_shift(void* self, double delta_0, double delta_1)" + expectedCFunction: "void c_shift(double delta_0, double delta_1, void* self)" ) } @@ -116,11 +116,11 @@ final class FunctionLoweringTests { enclosingType: "Point", expectedCDecl: """ @_cdecl("c_shift") - func c_shift(_ self: UnsafeRawPointer, _ delta_0: Double, _ delta_1: Double) { + func c_shift(_ delta_0: Double, _ delta_1: Double, _ self: UnsafeRawPointer) { unsafeBitCast(self, to: Point.self).shift(by: (delta_0, delta_1)) } """, - expectedCFunction: "void c_shift(void const* self, double delta_0, double delta_1)" + expectedCFunction: "void c_shift(double delta_0, double delta_1, void const* self)" ) } From 1873da5476c5627576719d1459cf7e10da824dfa Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 3 Feb 2025 12:27:47 +0100 Subject: [PATCH 06/15] Factor out the lowering step as a more general "ConversionStep" --- Sources/JExtractSwift/ConversionStep.swift | 106 ++++++++++++++++ ...wift2JavaTranslator+FunctionLowering.swift | 120 +++--------------- 2 files changed, 121 insertions(+), 105 deletions(-) create mode 100644 Sources/JExtractSwift/ConversionStep.swift diff --git a/Sources/JExtractSwift/ConversionStep.swift b/Sources/JExtractSwift/ConversionStep.swift new file mode 100644 index 00000000..cde2dc4f --- /dev/null +++ b/Sources/JExtractSwift/ConversionStep.swift @@ -0,0 +1,106 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +/// 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 ConversionStep: Equatable { + /// The value being lowered. + case placeholder + + /// A reference to a component in a value that has been exploded, such as + /// a tuple element or part of a buffer pointer. + indirect case explodedComponent(ConversionStep, component: String) + + /// Cast the pointer described by the lowering step to the given + /// Swift type using `unsafeBitCast(_:to:)`. + indirect case unsafeCastPointer(ConversionStep, swiftType: SwiftType) + + /// Assume at the untyped pointer described by the lowering step to the + /// given type, using `assumingMemoryBound(to:).` + indirect case typedPointer(ConversionStep, 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(ConversionStep) + + /// Pass this value indirectly, via & for explicit `inout` parameters. + indirect case passIndirectly(ConversionStep) + + /// Initialize a value of the given Swift type with the set of labeled + /// arguments. + case initialize(SwiftType, arguments: [LabeledArgument]) + + /// 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([ConversionStep]) + + /// Convert the conversion step into an expression with the given + /// value as the placeholder value in the expression. + func asExprSyntax(isSelf: Bool, placeholder: String) -> ExprSyntax { + switch self { + case .placeholder: + return "\(raw: placeholder)" + + case .explodedComponent(let step, component: let component): + return step.asExprSyntax(isSelf: false, placeholder: "\(placeholder)_\(component)") + + case .unsafeCastPointer(let step, swiftType: let swiftType): + let untypedExpr = step.asExprSyntax(isSelf: false, placeholder: placeholder) + return "unsafeBitCast(\(untypedExpr), to: \(swiftType.metatypeReferenceExprSyntax))" + + case .typedPointer(let step, swiftType: let type): + let untypedExpr = step.asExprSyntax(isSelf: isSelf, placeholder: placeholder) + return "\(untypedExpr).assumingMemoryBound(to: \(type.metatypeReferenceExprSyntax))" + + case .pointee(let step): + let untypedExpr = step.asExprSyntax(isSelf: isSelf, placeholder: placeholder) + return "\(untypedExpr).pointee" + + case .passIndirectly(let step): + let innerExpr = step.asExprSyntax(isSelf: false, placeholder: placeholder) + 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, placeholder: placeholder) + 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.enumerated().map { (index, element) in + element.asExprSyntax(isSelf: false, placeholder: "\(placeholder)_\(index)").description + } + + // FIXME: Should be able to use structured initializers here instead + // of splatting out text. + let renderedElementList = renderedElements.joined(separator: ", ") + return "(\(raw: renderedElementList))" + } + } +} diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift b/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift index 6375b92f..8143e3cd 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift @@ -138,7 +138,7 @@ extension Swift2JavaTranslator { case .metatype(let instanceType): return LoweredParameters( cdeclToOriginal: .unsafeCastPointer( - .value, + .placeholder, swiftType: instanceType ), cdeclParameters: [ @@ -170,14 +170,14 @@ extension Swift2JavaTranslator { } let mutable = (convention == .inout) - let loweringStep: LoweringStep + let loweringStep: ConversionStep switch nominal.nominalTypeDecl.kind { case .actor, .class: loweringStep = - .unsafeCastPointer(.value, swiftType: type) + .unsafeCastPointer(.placeholder, swiftType: type) case .enum, .struct, .protocol: loweringStep = - .passIndirectly(.pointee(.typedPointer(.value, swiftType: type))) + .passIndirectly(.pointee(.typedPointer(.placeholder, swiftType: type))) } return LoweredParameters( @@ -228,7 +228,7 @@ extension Swift2JavaTranslator { } return LoweredParameters( - cdeclToOriginal: .value, + cdeclToOriginal: .placeholder, cdeclParameters: [ SwiftParameter( convention: convention, @@ -249,7 +249,7 @@ extension Swift2JavaTranslator { } return LoweredParameters( - cdeclToOriginal: .value, + cdeclToOriginal: .placeholder, cdeclParameters: [ SwiftParameter( convention: convention, @@ -280,21 +280,21 @@ extension Swift2JavaTranslator { let cdeclPointerType = mutable ? swiftStdlibTypes[.unsafeMutableRawPointer] : swiftStdlibTypes[.unsafeRawPointer] - var cdeclToOriginal: LoweringStep + var cdeclToOriginal: ConversionStep switch (requiresArgument, hasCount) { case (false, false): - cdeclToOriginal = .value + cdeclToOriginal = .placeholder case (true, false): cdeclToOriginal = .typedPointer( - .explodedComponent(.value, component: "pointer"), + .explodedComponent(.placeholder, component: "pointer"), swiftType: nominal.genericArguments![0] ) case (false, true): cdeclToOriginal = .initialize(type, arguments: [ - LabeledArgument(label: "start", argument: .explodedComponent(.value, component: "pointer")), - LabeledArgument(label: "count", argument: .explodedComponent(.value, component: "count")) + LabeledArgument(label: "start", argument: .explodedComponent(.placeholder, component: "pointer")), + LabeledArgument(label: "count", argument: .explodedComponent(.placeholder, component: "count")) ]) case (true, true): @@ -304,12 +304,12 @@ extension Swift2JavaTranslator { LabeledArgument( label: "start", argument: .typedPointer( - .explodedComponent(.value, component: "pointer"), + .explodedComponent(.placeholder, component: "pointer"), swiftType: nominal.genericArguments![0]) ), LabeledArgument( label: "count", - argument: .explodedComponent(.value, component: "count") + argument: .explodedComponent(.placeholder, component: "count") ) ] ) @@ -395,48 +395,11 @@ struct LabeledArgument { extension LabeledArgument: Equatable where Element: Equatable { } -/// 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 { - /// The value being lowered. - case value - - /// A reference to a component in a value that has been exploded, such as - /// a tuple element or part of a buffer pointer. - indirect case explodedComponent(LoweringStep, component: 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]) - - /// 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 parameters to the original function /// parameter. - var cdeclToOriginal: LoweringStep + var cdeclToOriginal: ConversionStep /// The lowering of the parameters at the C level in Swift. var cdeclParameters: [SwiftParameter] @@ -446,60 +409,7 @@ 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, value: String)-> ExprSyntax { - cdeclToOriginal.asExprSyntax(isSelf: isSelf, value: value) - } -} - -extension LoweringStep { - func asExprSyntax(isSelf: Bool, value: String) -> ExprSyntax { - switch self { - case .value: - return "\(raw: value)" - - case .explodedComponent(let step, component: let component): - return step.asExprSyntax(isSelf: false, value: "\(value)_\(component)") - - case .unsafeCastPointer(let step, swiftType: let swiftType): - let untypedExpr = step.asExprSyntax(isSelf: false, value: value) - return "unsafeBitCast(\(untypedExpr), to: \(swiftType.metatypeReferenceExprSyntax))" - - case .typedPointer(let step, swiftType: let type): - let untypedExpr = step.asExprSyntax(isSelf: isSelf, value: value) - return "\(untypedExpr).assumingMemoryBound(to: \(type.metatypeReferenceExprSyntax))" - - case .pointee(let step): - let untypedExpr = step.asExprSyntax(isSelf: isSelf, value: value) - return "\(untypedExpr).pointee" - - case .passIndirectly(let step): - let innerExpr = step.asExprSyntax(isSelf: false, value: value) - 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, value: value) - 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.enumerated().map { (index, element) in - element.asExprSyntax(isSelf: false, value: "\(value)_\(index)").description - } - - // FIXME: Should be able to use structured initializers here instead - // of splatting out text. - let renderedElementList = renderedElements.joined(separator: ", ") - return "(\(raw: renderedElementList))" - } + cdeclToOriginal.asExprSyntax(isSelf: isSelf, placeholder: value) } } From 4c156028b1cbbc2352b02506b9604255591ff933 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 3 Feb 2025 17:23:15 +0100 Subject: [PATCH 07/15] Separate Swift-to-cdecl parameter lowering from the conversion sequences Make the operation to convert from cdecl parameter(s) into a Swift parameter a standalone operation, separate from the lowering of Swift parameters to cdecl parameters. They need to be synchronized, but they are not the same. --- .../CDeclLowering/CDeclConversions.swift | 90 ++++++ .../CRepresentation.swift} | 62 +++- ...wift2JavaTranslator+FunctionLowering.swift | 295 +++++------------- .../SwiftNominalTypeDeclaration.swift | 16 + .../SwiftStandardLibraryTypes.swift | 72 ++--- 5 files changed, 270 insertions(+), 265 deletions(-) create mode 100644 Sources/JExtractSwift/CDeclLowering/CDeclConversions.swift rename Sources/JExtractSwift/{SwiftTypes/SwiftStandardLibraryTypes+CLowering.swift => CDeclLowering/CRepresentation.swift} (63%) rename Sources/JExtractSwift/{ => CDeclLowering}/Swift2JavaTranslator+FunctionLowering.swift (55%) diff --git a/Sources/JExtractSwift/CDeclLowering/CDeclConversions.swift b/Sources/JExtractSwift/CDeclLowering/CDeclConversions.swift new file mode 100644 index 00000000..08816139 --- /dev/null +++ b/Sources/JExtractSwift/CDeclLowering/CDeclConversions.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +extension ConversionStep { + /// Produce a conversion that takes in a value (or set of values) that + /// would be available in a @_cdecl function to represent the given Swift + /// type, and convert that to an instance of the Swift type. + init(cdeclToSwift swiftType: SwiftType) throws { + switch swiftType { + case .function, .optional: + throw LoweringError.unhandledType(swiftType) + + case .metatype(let instanceType): + self = .unsafeCastPointer( + .placeholder, + swiftType: instanceType + ) + + case .nominal(let nominal): + if let knownType = nominal.nominalTypeDecl.knownStandardLibraryType { + // Swift types that map to primitive types in C. These can be passed + // through directly. + if knownType.primitiveCType != nil { + self = .placeholder + return + } + + // Typed pointers + if let firstGenericArgument = nominal.genericArguments?.first { + switch knownType { + case .unsafePointer, .unsafeMutablePointer: + self = .typedPointer( + .explodedComponent(.placeholder, component: "pointer"), + swiftType: firstGenericArgument + ) + return + + case .unsafeBufferPointer, .unsafeMutableBufferPointer: + self = .initialize( + swiftType, + arguments: [ + LabeledArgument( + label: "start", + argument: .typedPointer( + .explodedComponent(.placeholder, component: "pointer"), + swiftType: firstGenericArgument) + ), + LabeledArgument( + label: "count", + argument: .explodedComponent(.placeholder, component: "count") + ) + ] + ) + return + + default: + break + } + } + } + + // Arbitrary nominal types. + switch nominal.nominalTypeDecl.kind { + case .actor, .class: + // For actor and class, we pass around the pointer directly. + self = .unsafeCastPointer(.placeholder, swiftType: swiftType) + case .enum, .struct, .protocol: + // For enums, structs, and protocol types, we pass around the + // values indirectly. + self = .passIndirectly( + .pointee(.typedPointer(.placeholder, swiftType: swiftType)) + ) + } + + case .tuple(let elements): + self = .tuplify(try elements.map { try ConversionStep(cdeclToSwift: $0) }) + } + } +} diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes+CLowering.swift b/Sources/JExtractSwift/CDeclLowering/CRepresentation.swift similarity index 63% rename from Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes+CLowering.swift rename to Sources/JExtractSwift/CDeclLowering/CRepresentation.swift index 3913f9cc..08e10de1 100644 --- a/Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes+CLowering.swift +++ b/Sources/JExtractSwift/CDeclLowering/CRepresentation.swift @@ -12,17 +12,19 @@ // //===----------------------------------------------------------------------===// -extension SwiftStandardLibraryTypes { +extension CType { /// Lower the given Swift type down to a its corresponding C type. /// /// This operation only supports the subset of Swift types that are /// representable in a Swift `@_cdecl` function. If lowering an arbitrary /// Swift function, first go through Swift -> cdecl lowering. - func cdeclToCLowering(_ swiftType: SwiftType) throws -> CType { - switch swiftType { + init(cdeclType: SwiftType) throws { + switch cdeclType { case .nominal(let nominalType): - if let knownType = self[nominalType.nominalTypeDecl] { - return try knownType.loweredCType() + if let knownType = nominalType.nominalTypeDecl.knownStandardLibraryType, + let primitiveCType = knownType.primitiveCType { + self = primitiveCType + return } throw CDeclToCLoweringError.invalidNominalType(nominalType.nominalTypeDecl) @@ -33,12 +35,12 @@ extension SwiftStandardLibraryTypes { throw CDeclToCLoweringError.invalidFunctionConvention(functionType) case .c: - let resultType = try cdeclToCLowering(functionType.resultType) + let resultType = try CType(cdeclType: functionType.resultType) let parameterTypes = try functionType.parameters.map { param in - try cdeclToCLowering(param.type) + try CType(cdeclType: param.type) } - return .function( + self = .function( resultType: resultType, parameters: parameterTypes, variadic: false @@ -46,16 +48,46 @@ extension SwiftStandardLibraryTypes { } case .tuple([]): - return .void + self = .void case .metatype, .optional, .tuple: - throw CDeclToCLoweringError.invalidCDeclType(swiftType) + throw CDeclToCLoweringError.invalidCDeclType(cdeclType) } } } +extension CFunction { + /// Produce a C function that represents the given @_cdecl Swift function. + init(cdeclSignature: SwiftFunctionSignature, cName: String) throws { + assert(cdeclSignature.selfParameter == nil) + + let cResultType = try CType(cdeclType: cdeclSignature.result.type) + let cParameters = try cdeclSignature.parameters.map { parameter in + CParameter( + name: parameter.parameterName, + type: try CType(cdeclType: parameter.type).parameterDecay + ) + } + + self = CFunction( + resultType: cResultType, + name: cName, + parameters: cParameters, + isVariadic: false + ) + } +} + +enum CDeclToCLoweringError: Error { + case invalidCDeclType(SwiftType) + case invalidNominalType(SwiftNominalTypeDeclaration) + case invalidFunctionConvention(SwiftFunctionType) +} + extension KnownStandardLibraryType { - func loweredCType() throws -> CType { + /// Determine the primitive C type that corresponds to this C standard + /// library type, if there is one. + var primitiveCType: CType? { switch self { case .bool: .integral(.bool) case .int: .integral(.ptrdiff_t) @@ -74,12 +106,8 @@ extension KnownStandardLibraryType { case .unsafeRawPointer: .pointer( .qualified(const: true, volatile: false, type: .void) ) + case .unsafePointer, .unsafeMutablePointer, .unsafeBufferPointer, .unsafeMutableBufferPointer: + nil } } } -enum CDeclToCLoweringError: Error { - case invalidCDeclType(SwiftType) - case invalidNominalType(SwiftNominalTypeDeclaration) - case invalidFunctionConvention(SwiftFunctionType) -} - diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift b/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift similarity index 55% rename from Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift rename to Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift index 8143e3cd..ab89c0b2 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift +++ b/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift @@ -135,12 +135,8 @@ extension Swift2JavaTranslator { case .function, .optional: throw LoweringError.unhandledType(type) - case .metatype(let instanceType): + case .metatype: return LoweredParameters( - cdeclToOriginal: .unsafeCastPointer( - .placeholder, - swiftType: instanceType - ), cdeclParameters: [ SwiftParameter( convention: .byValue, @@ -156,39 +152,80 @@ extension Swift2JavaTranslator { case .nominal(let nominal): // Types from the Swift standard library that we know about. - if nominal.nominalTypeDecl.moduleName == "Swift", - nominal.nominalTypeDecl.parent == nil { - // Primitive types - if let loweredPrimitive = try lowerParameterPrimitive(nominal, convention: convention, parameterName: parameterName) { - return loweredPrimitive + if let knownType = nominal.nominalTypeDecl.knownStandardLibraryType, + convention != .inout { + // Swift types that map to primitive types in C. These can be passed + // through directly. + if knownType.primitiveCType != nil { + return LoweredParameters( + cdeclParameters: [ + SwiftParameter( + convention: convention, + parameterName: parameterName, + type: type, + isPrimitive: true + ) + ] + ) } - // Swift pointer types. - if let loweredPointers = try lowerParameterPointers(nominal, convention: convention, parameterName: parameterName) { - return loweredPointers + // Typed pointers are mapped down to their raw forms in cdecl entry + // points. These can be passed through directly. + if knownType == .unsafePointer || knownType == .unsafeMutablePointer { + let isMutable = knownType == .unsafeMutablePointer + let cdeclPointerType = isMutable + ? swiftStdlibTypes[.unsafeMutableRawPointer] + : swiftStdlibTypes[.unsafeRawPointer] + return LoweredParameters( + cdeclParameters: [ + SwiftParameter( + convention: convention, + parameterName: parameterName + "_pointer", + type: SwiftType.nominal( + SwiftNominalType(nominalTypeDecl: cdeclPointerType) + ) + ) + ] + ) } - } - let mutable = (convention == .inout) - let loweringStep: ConversionStep - switch nominal.nominalTypeDecl.kind { - case .actor, .class: - loweringStep = - .unsafeCastPointer(.placeholder, swiftType: type) - case .enum, .struct, .protocol: - loweringStep = - .passIndirectly(.pointee(.typedPointer(.placeholder, swiftType: type))) + // Typed buffer pointers are mapped down to a (pointer, count) pair + // so those parts can be passed through directly. + if knownType == .unsafeBufferPointer || knownType == .unsafeMutableBufferPointer { + let isMutable = knownType == .unsafeMutableBufferPointer + let cdeclPointerType = isMutable + ? swiftStdlibTypes[.unsafeMutableRawPointer] + : swiftStdlibTypes[.unsafeRawPointer] + return LoweredParameters( + cdeclParameters: [ + SwiftParameter( + convention: convention, + parameterName: parameterName + "_pointer", + type: SwiftType.nominal( + SwiftNominalType(nominalTypeDecl: cdeclPointerType) + ) + ), + SwiftParameter( + convention: convention, + parameterName: parameterName + "_count", + type: SwiftType.nominal( + SwiftNominalType(nominalTypeDecl: swiftStdlibTypes[.int]) + ) + ) + ] + ) + } } + let isMutable = (convention == .inout) return LoweredParameters( - cdeclToOriginal: loweringStep, cdeclParameters: [ SwiftParameter( convention: .byValue, parameterName: parameterName, type: .nominal( SwiftNominalType( - nominalTypeDecl: mutable + nominalTypeDecl: isMutable ? swiftStdlibTypes[.unsafeMutableRawPointer] : swiftStdlibTypes[.unsafeRawPointer] ) @@ -203,163 +240,11 @@ extension Swift2JavaTranslator { try lowerParameter(element, convention: convention, parameterName: name) } return LoweredParameters( - cdeclToOriginal: .tuplify(loweredElements.map { $0.cdeclToOriginal }), cdeclParameters: loweredElements.flatMap { $0.cdeclParameters } ) } } - func lowerParameterPrimitive( - _ nominal: SwiftNominalType, - convention: SwiftParameterConvention, - parameterName: String - ) throws -> LoweredParameters? { - let nominalName = nominal.nominalTypeDecl.name - let type = SwiftType.nominal(nominal) - - // Swift types that map directly to Java primitive types. - if let primitiveType = JavaType(swiftTypeName: nominalName) { - // FIXME: Should be using C types here, not Java types. - _ = primitiveType - - // We cannot handle inout on primitive types. - if convention == .inout { - throw LoweringError.inoutNotSupported(type) - } - - return LoweredParameters( - cdeclToOriginal: .placeholder, - cdeclParameters: [ - SwiftParameter( - convention: convention, - parameterName: parameterName, - type: type, - isPrimitive: true - ) - ] - ) - } - - // The Swift "Int" type, which maps to whatever the pointer-sized primitive - // integer type is in Java (int for 32-bit, long for 64-bit). - if nominalName == "Int" { - // We cannot handle inout on primitive types. - if convention == .inout { - throw LoweringError.inoutNotSupported(type) - } - - return LoweredParameters( - cdeclToOriginal: .placeholder, - cdeclParameters: [ - SwiftParameter( - convention: convention, - parameterName: parameterName, - type: type, - isPrimitive: true - ) - ] - ) - } - - return nil - } - - func lowerParameterPointers( - _ nominal: SwiftNominalType, - convention: SwiftParameterConvention, - parameterName: String - ) throws -> LoweredParameters? { - let nominalName = nominal.nominalTypeDecl.name - let type = SwiftType.nominal(nominal) - - guard let (requiresArgument, mutable, hasCount) = nominalName.isNameOfSwiftPointerType else { - return nil - } - - // At the @_cdecl level, make everything a raw pointer. - let cdeclPointerType = mutable - ? swiftStdlibTypes[.unsafeMutableRawPointer] - : swiftStdlibTypes[.unsafeRawPointer] - var cdeclToOriginal: ConversionStep - switch (requiresArgument, hasCount) { - case (false, false): - cdeclToOriginal = .placeholder - - case (true, false): - cdeclToOriginal = .typedPointer( - .explodedComponent(.placeholder, component: "pointer"), - swiftType: nominal.genericArguments![0] - ) - - case (false, true): - cdeclToOriginal = .initialize(type, arguments: [ - LabeledArgument(label: "start", argument: .explodedComponent(.placeholder, component: "pointer")), - LabeledArgument(label: "count", argument: .explodedComponent(.placeholder, component: "count")) - ]) - - case (true, true): - cdeclToOriginal = .initialize( - type, - arguments: [ - LabeledArgument( - label: "start", - argument: .typedPointer( - .explodedComponent(.placeholder, component: "pointer"), - swiftType: nominal.genericArguments![0]) - ), - LabeledArgument( - label: "count", - argument: .explodedComponent(.placeholder, component: "count") - ) - ] - ) - } - - let lowered: [(SwiftParameter, ForeignValueLayout)] - if hasCount { - lowered = [ - ( - SwiftParameter( - convention: convention, - parameterName: parameterName + "_pointer", - type: SwiftType.nominal( - SwiftNominalType(nominalTypeDecl: cdeclPointerType) - ) - ), - .SwiftPointer - ), - ( - SwiftParameter( - convention: convention, - parameterName: parameterName + "_count", - type: SwiftType.nominal( - SwiftNominalType(nominalTypeDecl: swiftStdlibTypes[.int]) - ) - ), - .SwiftInt - ) - ] - } else { - lowered = [ - ( - SwiftParameter( - convention: convention, - parameterName: parameterName + "_pointer", - type: SwiftType.nominal( - SwiftNominalType(nominalTypeDecl: cdeclPointerType) - ) - ), - .SwiftPointer - ), - ] - } - - return LoweredParameters( - cdeclToOriginal: cdeclToOriginal, - cdeclParameters: lowered.map(\.0) - ) - } - /// Given a Swift function signature that represents a @_cdecl function, /// produce the equivalent C function with the given name. /// @@ -369,22 +254,7 @@ extension Swift2JavaTranslator { _ cdeclSignature: SwiftFunctionSignature, cName: String ) -> CFunction { - assert(cdeclSignature.selfParameter == nil) - - let cResultType = try! swiftStdlibTypes.cdeclToCLowering(cdeclSignature.result.type) - let cParameters = cdeclSignature.parameters.map { parameter in - CParameter( - name: parameter.parameterName, - type: try! swiftStdlibTypes.cdeclToCLowering(parameter.type).parameterDecay - ) - } - - return CFunction( - resultType: cResultType, - name: cName, - parameters: cParameters, - isVariadic: false - ) + return try! CFunction(cdeclSignature: cdeclSignature, cName: cName) } } @@ -397,22 +267,10 @@ extension LabeledArgument: Equatable where Element: Equatable { } struct LoweredParameters: Equatable { - /// The steps needed to get from the @_cdecl parameters to the original function - /// parameter. - var cdeclToOriginal: ConversionStep - /// The lowering of the parameters at the C level in Swift. var cdeclParameters: [SwiftParameter] } -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, value: String)-> ExprSyntax { - cdeclToOriginal.asExprSyntax(isSelf: isSelf, placeholder: value) - } -} - enum LoweringError: Error { case inoutNotSupported(SwiftType) case unhandledType(SwiftType) @@ -443,9 +301,13 @@ extension LoweredFunctionSignature { // Lower "self", if there is one. let parametersToLower: ArraySlice let cdeclToOriginalSelf: ExprSyntax? - if let originalSelfParam = original.selfParameter, - let selfParameter = parameters.last { - cdeclToOriginalSelf = selfParameter.cdeclToOriginalArgumentExpr(isSelf: true, value: originalSelfParam.parameterName ?? "self") + if let originalSelfParam = original.selfParameter { + cdeclToOriginalSelf = try! ConversionStep( + cdeclToSwift: originalSelfParam.type + ).asExprSyntax( + isSelf: true, + placeholder: originalSelfParam.parameterName ?? "self" + ) parametersToLower = parameters.dropLast() } else { cdeclToOriginalSelf = nil @@ -453,8 +315,15 @@ extension LoweredFunctionSignature { } // Lower the remaining arguments. - let cdeclToOriginalArguments = zip(parametersToLower, original.parameters).map { lowering, originalParam in - let cdeclToOriginalArg = lowering.cdeclToOriginalArgumentExpr(isSelf: false, value: originalParam.parameterName ?? "FIXME") + let cdeclToOriginalArguments = parametersToLower.indices.map { index in + let originalParam = original.parameters[index] + let cdeclToOriginalArg = try! ConversionStep( + cdeclToSwift: originalParam.type + ).asExprSyntax( + isSelf: false, + placeholder: originalParam.parameterName ?? "_\(index)" + ) + if let argumentLabel = originalParam.argumentLabel { return "\(argumentLabel): \(cdeclToOriginalArg.description)" } else { @@ -478,10 +347,12 @@ extension LoweredFunctionSignature { """ } else if cdecl.result.type.isVoid { // Indirect return. This is a regular return in Swift that turns - // into a + // into an assignment via the indirect parameters. + // FIXME: This should actually be a swiftToCDecl conversion! + let resultConversion = try! ConversionStep(cdeclToSwift: original.result.type) loweredCDecl.body = """ { - \(result.cdeclToOriginalArgumentExpr(isSelf: true, value: "_result")) = \(callExpression) + \(resultConversion.asExprSyntax(isSelf: true, placeholder: "_result")) = \(callExpression) } """ } else { diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftNominalTypeDeclaration.swift b/Sources/JExtractSwift/SwiftTypes/SwiftNominalTypeDeclaration.swift index cf017f98..53e103b0 100644 --- a/Sources/JExtractSwift/SwiftTypes/SwiftNominalTypeDeclaration.swift +++ b/Sources/JExtractSwift/SwiftTypes/SwiftNominalTypeDeclaration.swift @@ -42,6 +42,12 @@ class SwiftNominalTypeDeclaration { // TODO: Generic parameters. + /// Identify this nominal declaration as one of the known standard library + /// types, like 'Swift.Int[. + lazy var knownStandardLibraryType: KnownStandardLibraryType? = { + self.computeKnownStandardLibraryType() + }() + /// Create a nominal type declaration from the syntax node for a nominal type /// declaration. init( @@ -63,6 +69,16 @@ class SwiftNominalTypeDeclaration { default: fatalError("Not a nominal type declaration") } } + + /// Determine the known standard library type for this nominal type + /// declaration. + private func computeKnownStandardLibraryType() -> KnownStandardLibraryType? { + if parent != nil || moduleName != "Swift" { + return nil + } + + return KnownStandardLibraryType(typeNameInSwiftModule: name) + } } extension SwiftNominalTypeDeclaration: Equatable { diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes.swift b/Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes.swift index b9689e9c..77ab9530 100644 --- a/Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes.swift +++ b/Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes.swift @@ -14,45 +14,45 @@ import SwiftSyntax -enum KnownStandardLibraryType: Int, Hashable, CaseIterable { - case bool = 0 - case int - case uint - case int8 - case uint8 - case int16 - case uint16 - case int32 - case uint32 - case int64 - case uint64 - case float - case double - case unsafeRawPointer - case unsafeMutableRawPointer - - var typeName: String { - switch self { - case .bool: return "Bool" - case .int: return "Int" - case .uint: return "UInt" - case .int8: return "Int8" - case .uint8: return "UInt8" - case .int16: return "Int16" - case .uint16: return "UInt16" - case .int32: return "Int32" - case .uint32: return "UInt32" - case .int64: return "Int64" - case .uint64: return "UInt64" - case .float: return "Float" - case .double: return "Double" - case .unsafeRawPointer: return "UnsafeRawPointer" - case .unsafeMutableRawPointer: return "UnsafeMutableRawPointer" - } +enum KnownStandardLibraryType: String, Hashable, CaseIterable { + case bool = "Bool" + case int = "Int" + case uint = "UInt" + case int8 = "Int8" + case uint8 = "UInt8" + case int16 = "Int16" + case uint16 = "UInt16" + case int32 = "Int32" + case uint32 = "UInt32" + case int64 = "Int64" + case uint64 = "UInt64" + case float = "Float" + case double = "Double" + case unsafeRawPointer = "UnsafeRawPointer" + case unsafeMutableRawPointer = "UnsafeMutableRawPointer" + case unsafePointer = "UnsafePointer" + case unsafeMutablePointer = "UnsafeMutablePointer" + case unsafeBufferPointer = "UnsafeBufferPointer" + case unsafeMutableBufferPointer = "UnsafeMutableBufferPointer" + + var typeName: String { rawValue } + + init?(typeNameInSwiftModule: String) { + self.init(rawValue: typeNameInSwiftModule) } + /// Whether this declaration is generic. var isGeneric: Bool { - false + switch self { + case .bool, .double, .float, .int, .int8, .int16, .int32, .int64, + .uint, .uint8, .uint16, .uint32, .uint64, .unsafeRawPointer, + .unsafeMutableRawPointer: + false + + case .unsafePointer, .unsafeMutablePointer, .unsafeBufferPointer, + .unsafeMutableBufferPointer: + true + } } } From a2982f953d259e1e28f405bce8cf695c391103e2 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 3 Feb 2025 19:15:26 +0100 Subject: [PATCH 08/15] 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). --- .../CDeclLowering/CDeclConversions.swift | 103 +++++++++++++++++ ...wift2JavaTranslator+FunctionLowering.swift | 105 ++++++++++++++---- Sources/JExtractSwift/ConversionStep.swift | 50 +++++++++ .../JExtractSwift/Swift2JavaTranslator.swift | 2 +- .../SwiftTypes/SwiftParameter.swift | 2 +- .../SwiftStandardLibraryTypes.swift | 2 +- .../Asserts/LoweringAssertions.swift | 6 +- .../FunctionLoweringTests.swift | 85 +++++++++++++- 8 files changed, 328 insertions(+), 27 deletions(-) diff --git a/Sources/JExtractSwift/CDeclLowering/CDeclConversions.swift b/Sources/JExtractSwift/CDeclLowering/CDeclConversions.swift index 08816139..c8b7e57c 100644 --- a/Sources/JExtractSwift/CDeclLowering/CDeclConversions.swift +++ b/Sources/JExtractSwift/CDeclLowering/CDeclConversions.swift @@ -87,4 +87,107 @@ extension ConversionStep { self = .tuplify(try elements.map { try ConversionStep(cdeclToSwift: $0) }) } } + + /// Produce a conversion that takes in a value that would be available in a + /// Swift function and convert that to the corresponding cdecl values. + /// + /// This conversion goes in the opposite direction of init(cdeclToSwift:), and + /// is used for (e.g.) returning the Swift value from a cdecl function. When + /// there are multiple cdecl values that correspond to this one Swift value, + /// the result will be a tuple that can be assigned to a tuple of the cdecl + /// values, e.g., (.baseAddress, .count). + init( + swiftToCDecl swiftType: SwiftType, + stdlibTypes: SwiftStandardLibraryTypes + ) throws { + switch swiftType { + case .function, .optional: + throw LoweringError.unhandledType(swiftType) + + case .metatype: + self = .unsafeCastPointer( + .placeholder, + swiftType: .nominal( + SwiftNominalType( + nominalTypeDecl: stdlibTypes[.unsafeRawPointer] + ) + ) + ) + return + + case .nominal(let nominal): + if let knownType = nominal.nominalTypeDecl.knownStandardLibraryType { + // Swift types that map to primitive types in C. These can be passed + // through directly. + if knownType.primitiveCType != nil { + self = .placeholder + return + } + + // Typed pointers + if nominal.genericArguments?.first != nil { + switch knownType { + case .unsafePointer, .unsafeMutablePointer: + let isMutable = knownType == .unsafeMutablePointer + self = ConversionStep( + initializeRawPointerFromTyped: .placeholder, + isMutable: isMutable, + isPartOfBufferPointer: false, + stdlibTypes: stdlibTypes + ) + return + + case .unsafeBufferPointer, .unsafeMutableBufferPointer: + let isMutable = knownType == .unsafeMutableBufferPointer + self = .tuplify( + [ + ConversionStep( + initializeRawPointerFromTyped: .explodedComponent( + .placeholder, + component: "pointer" + ), + isMutable: isMutable, + isPartOfBufferPointer: true, + stdlibTypes: stdlibTypes + ), + .explodedComponent(.placeholder, component: "count") + ] + ) + return + + default: + break + } + } + } + + // Arbitrary nominal types. + switch nominal.nominalTypeDecl.kind { + case .actor, .class: + // For actor and class, we pass around the pointer directly. Case to + // the unsafe raw pointer type we use to represent it in C. + self = .unsafeCastPointer( + .placeholder, + swiftType: .nominal( + SwiftNominalType( + nominalTypeDecl: stdlibTypes[.unsafeRawPointer] + ) + ) + ) + + case .enum, .struct, .protocol: + // For enums, structs, and protocol types, we leave the value alone. + // The indirection will be handled by the caller. + self = .placeholder + } + + case .tuple(let elements): + // Convert all of the elements. + self = .tuplify( + try elements.map { element in + try ConversionStep(swiftToCDecl: element, stdlibTypes: stdlibTypes) + } + ) + } + } } diff --git a/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift b/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift index ab89c0b2..9ea8ea63 100644 --- a/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift +++ b/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift @@ -63,7 +63,7 @@ extension Swift2JavaTranslator { // void result type indirectResult = false } else if loweredResult.cdeclParameters.count == 1, - loweredResult.cdeclParameters[0].isPrimitive { + loweredResult.cdeclParameters[0].canBeDirectReturn { // Primitive result type indirectResult = false } else { @@ -145,7 +145,8 @@ extension Swift2JavaTranslator { SwiftNominalType( nominalTypeDecl: swiftStdlibTypes[.unsafeRawPointer] ) - ) + ), + canBeDirectReturn: true ) ] ) @@ -163,7 +164,7 @@ extension Swift2JavaTranslator { convention: convention, parameterName: parameterName, type: type, - isPrimitive: true + canBeDirectReturn: true ) ] ) @@ -183,7 +184,8 @@ extension Swift2JavaTranslator { parameterName: parameterName + "_pointer", type: SwiftType.nominal( SwiftNominalType(nominalTypeDecl: cdeclPointerType) - ) + ), + canBeDirectReturn: true ) ] ) @@ -217,6 +219,13 @@ extension Swift2JavaTranslator { } } + // Arbitrary types are lowered to raw pointers that either "are" the + // reference (for classes and actors) or will point to it. + let canBeDirectReturn = switch nominal.nominalTypeDecl.kind { + case .actor, .class: true + case .enum, .protocol, .struct: false + } + let isMutable = (convention == .inout) return LoweredParameters( cdeclParameters: [ @@ -229,7 +238,8 @@ extension Swift2JavaTranslator { ? swiftStdlibTypes[.unsafeMutableRawPointer] : swiftStdlibTypes[.unsafeRawPointer] ) - ) + ), + canBeDirectReturn: canBeDirectReturn ) ] ) @@ -289,7 +299,11 @@ 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 { + public func cdeclThunk( + cName: String, + inputFunction: FunctionDeclSyntax, + stdlibTypes: SwiftStandardLibraryTypes + ) -> FunctionDeclSyntax { var loweredCDecl = cdecl.createFunctionDecl(cName) // Add the @_cdecl attribute. @@ -345,23 +359,70 @@ extension LoweredFunctionSignature { \(callExpression) } """ - } else if cdecl.result.type.isVoid { - // Indirect return. This is a regular return in Swift that turns - // into an assignment via the indirect parameters. - // FIXME: This should actually be a swiftToCDecl conversion! - let resultConversion = try! ConversionStep(cdeclToSwift: original.result.type) - loweredCDecl.body = """ - { - \(resultConversion.asExprSyntax(isSelf: true, placeholder: "_result")) = \(callExpression) - } - """ } else { - // Direct return. - loweredCDecl.body = """ - { - return \(callExpression) - } - """ + // Determine the necessary conversion of the Swift return value to the + // cdecl return value. + let resultConversion = try! ConversionStep( + swiftToCDecl: original.result.type, + stdlibTypes: stdlibTypes + ) + + var bodyItems: [CodeBlockItemSyntax] = [] + + // If the are multiple places in the result conversion that reference + // the placeholder, capture the result of the call in a local variable. + // This prevents us from calling the function multiple times. + let originalResult: ExprSyntax + if resultConversion.placeholderCount > 1 { + bodyItems.append(""" + let __swift_result = \(callExpression) + """ + ) + originalResult = "__swift_result" + } else { + originalResult = callExpression + } + + // FIXME: Check whether there are multiple places in which we reference + // the placeholder in resultConversion. If so, we should write it into a + // local let "_resultValue" or similar so we don't call the underlying + // function multiple times. + + // Convert the result. + let convertedResult = resultConversion.asExprSyntax( + isSelf: true, + placeholder: originalResult.description + ) + + if cdecl.result.type.isVoid { + // Indirect return. This is a regular return in Swift that turns + // into an assignment via the indirect parameters. We do a cdeclToSwift + // conversion on the left-hand side of the tuple to gather all of the + // indirect output parameters we need to assign to, and the result + // conversion is the corresponding right-hand side. + let cdeclParamConversion = try! ConversionStep( + cdeclToSwift: original.result.type + ) + let indirectResults = cdeclParamConversion.asExprSyntax( + isSelf: true, + placeholder: "_result" + ) + bodyItems.append(""" + \(indirectResults) = \(convertedResult) + """ + ) + } else { + // Direct return. Just convert the expression. + bodyItems.append(""" + return \(convertedResult) + """ + ) + } + + loweredCDecl.body = CodeBlockSyntax( + leftBrace: .leftBraceToken(trailingTrivia: .newline), + statements: .init(bodyItems.map { $0.with(\.trailingTrivia, .newline) }) + ) } return loweredCDecl diff --git a/Sources/JExtractSwift/ConversionStep.swift b/Sources/JExtractSwift/ConversionStep.swift index cde2dc4f..31b33a86 100644 --- a/Sources/JExtractSwift/ConversionStep.swift +++ b/Sources/JExtractSwift/ConversionStep.swift @@ -51,6 +51,56 @@ enum ConversionStep: Equatable { /// tuples, which Swift will convert to the labeled tuple form. case tuplify([ConversionStep]) + /// Create an initialization step that produces the raw pointer type that + /// corresponds to the typed pointer. + init( + initializeRawPointerFromTyped typedStep: ConversionStep, + isMutable: Bool, + isPartOfBufferPointer: Bool, + stdlibTypes: SwiftStandardLibraryTypes + ) { + // Initialize the corresponding raw pointer type from the typed + // pointer we have on the Swift side. + let rawPointerType = isMutable + ? stdlibTypes[.unsafeMutableRawPointer] + : stdlibTypes[.unsafeRawPointer] + self = .initialize( + .nominal( + SwiftNominalType( + nominalTypeDecl: rawPointerType + ) + ), + arguments: [ + LabeledArgument( + argument: isPartOfBufferPointer + ? .explodedComponent( + typedStep, + component: "pointer" + ) + : typedStep + ), + ] + ) + } + + /// Count the number of times that the placeholder occurs within this + /// conversion step. + var placeholderCount: Int { + switch self { + case .explodedComponent(let inner, component: _), + .passIndirectly(let inner), .pointee(let inner), + .typedPointer(let inner, swiftType: _), + .unsafeCastPointer(let inner, swiftType: _): + inner.placeholderCount + case .initialize(_, arguments: let arguments): + arguments.reduce(0) { $0 + $1.argument.placeholderCount } + case .placeholder: + 1 + case .tuplify(let elements): + elements.reduce(0) { $0 + $1.placeholderCount } + } + } + /// Convert the conversion step into an expression with the given /// value as the placeholder value in the expression. func asExprSyntax(isSelf: Bool, placeholder: String) -> ExprSyntax { diff --git a/Sources/JExtractSwift/Swift2JavaTranslator.swift b/Sources/JExtractSwift/Swift2JavaTranslator.swift index ff81ef16..6577a37c 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator.swift @@ -39,7 +39,7 @@ public final class Swift2JavaTranslator { /// type representation. package var importedTypes: [String: ImportedNominalType] = [:] - var swiftStdlibTypes: SwiftStandardLibraryTypes + public var swiftStdlibTypes: SwiftStandardLibraryTypes let symbolTable: SwiftSymbolTable let nominalResolution: NominalTypeResolution = NominalTypeResolution() diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift b/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift index 15b905a4..4cdb27e8 100644 --- a/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift +++ b/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift @@ -19,7 +19,7 @@ struct SwiftParameter: Equatable { var argumentLabel: String? var parameterName: String? var type: SwiftType - var isPrimitive = false + var canBeDirectReturn = false } extension SwiftParameter: CustomStringConvertible { diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes.swift b/Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes.swift index 77ab9530..fbd7b4f0 100644 --- a/Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes.swift +++ b/Sources/JExtractSwift/SwiftTypes/SwiftStandardLibraryTypes.swift @@ -58,7 +58,7 @@ enum KnownStandardLibraryType: String, Hashable, CaseIterable { /// Captures many types from the Swift standard library in their most basic /// forms, so that the translator can reason about them in source code. -struct SwiftStandardLibraryTypes { +public struct SwiftStandardLibraryTypes { // Swift.UnsafePointer let unsafePointerDecl: SwiftNominalTypeDeclaration diff --git a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift index 3be4931c..e86d09df 100644 --- a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift @@ -47,7 +47,11 @@ func assertLoweredFunction( inputFunction, enclosingType: enclosingType ) - let loweredCDecl = loweredFunction.cdeclThunk(cName: "c_\(inputFunction.name.text)", inputFunction: inputFunction) + let loweredCDecl = loweredFunction.cdeclThunk( + cName: "c_\(inputFunction.name.text)", + inputFunction: inputFunction, + stdlibTypes: translator.swiftStdlibTypes + ) #expect( loweredCDecl.description == expectedCDecl.description, diff --git a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift index 0c51ccf6..f47ee3b9 100644 --- a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift +++ b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift @@ -136,7 +136,90 @@ final class FunctionLoweringTests { } """, expectedCFunction: "void c_f(void const* t)" - ) + ) + + try assertLoweredFunction(""" + func f() -> Int.Type { } + """, + expectedCDecl: """ + @_cdecl("c_f") + func c_f() -> UnsafeRawPointer { + return unsafeBitCast(f(), to: UnsafeRawPointer.self) + } + """, + expectedCFunction: "void const* c_f(void)" + ) + } + + @Test("Lowering class returns") + func lowerClassReturns() throws { + try assertLoweredFunction(""" + func shifted(by delta: (Double, Double)) -> Point { } + """, + sourceFile: """ + class Point { } + """, + enclosingType: "Point", + expectedCDecl: """ + @_cdecl("c_shifted") + func c_shifted(_ delta_0: Double, _ delta_1: Double, _ self: UnsafeRawPointer) -> UnsafeRawPointer { + return unsafeBitCast(unsafeBitCast(self, to: Point.self).shifted(by: (delta_0, delta_1)), to: UnsafeRawPointer.self) + } + """, + expectedCFunction: "void const* c_shifted(double delta_0, double delta_1, void const* self)" + ) + } + + @Test("Lowering pointer returns") + func lowerPointerReturns() throws { + try assertLoweredFunction(""" + func getPointer() -> UnsafePointer { } + """, + sourceFile: """ + struct Point { } + """, + expectedCDecl: """ + @_cdecl("c_getPointer") + func c_getPointer() -> UnsafeRawPointer { + return UnsafeRawPointer(getPointer()) + } + """, + expectedCFunction: "void const* c_getPointer(void)" + ) + } + + @Test("Lowering tuple returns") + func lowerTupleReturns() throws { + try assertLoweredFunction(""" + func getTuple() -> (Int, (Float, Double)) { } + """, + expectedCDecl: """ + @_cdecl("c_getTuple") + func c_getTuple(_ _result_0: UnsafeMutableRawPointer, _ _result_1_0: UnsafeMutableRawPointer, _ _result_1_1: UnsafeMutableRawPointer) { + let __swift_result = getTuple() + (_result_0, (_result_1_0, _result_1_1)) = (__swift_result_0, (__swift_result_1_0, __swift_result_1_1)) + } + """, + expectedCFunction: "void c_getTuple(void* _result_0, void* _result_1_0, void* _result_1_1)" + ) + } + + @Test("Lowering buffer pointer returns", .disabled("Doesn't turn into the indirect returns")) + func lowerBufferPointerReturns() throws { + try assertLoweredFunction(""" + func getBufferPointer() -> UnsafeMutableBufferPointer { } + """, + sourceFile: """ + struct Point { } + """, + expectedCDecl: """ + @_cdecl("c_getBufferPointer") + func c_getBufferPointer(_result_pointer: UnsafeMutableRawPointer, _result_count: UnsafeMutableRawPointer) { + return UnsafeRawPointer(getPointer()) + } + """, + expectedCFunction: "c_getBufferPointer(void* _result_pointer, void* _result_count)" + ) } } From cebc48ece7e87abd116041268901318216a18d46 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 3 Feb 2025 20:05:31 +0100 Subject: [PATCH 09/15] Lower initializers and static methods to C declarations --- ...wift2JavaTranslator+FunctionLowering.swift | 83 ++++++++++---- .../SwiftTypes/SwiftFunctionSignature.swift | 101 +++++++++++++----- .../Asserts/LoweringAssertions.swift | 29 +++-- .../FunctionLoweringTests.swift | 71 +++++++++++- 4 files changed, 230 insertions(+), 54 deletions(-) diff --git a/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift b/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift index 9ea8ea63..84218a01 100644 --- a/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift +++ b/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift @@ -33,6 +33,22 @@ extension Swift2JavaTranslator { return try lowerFunctionSignature(signature) } + /// Lower the given initializer 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: InitializerDeclSyntax, + enclosingType: TypeSyntax? = nil + ) throws -> LoweredFunctionSignature { + let signature = try SwiftFunctionSignature( + decl, + enclosingType: try enclosingType.map { try SwiftType($0, symbolTable: symbolTable) }, + symbolTable: symbolTable + ) + + return try lowerFunctionSignature(signature) + } /// Lower the given Swift function signature to a Swift @_cdecl function signature, /// which is C compatible, and the corresponding Java method signature. /// @@ -75,12 +91,18 @@ extension Swift2JavaTranslator { indirectResult = true } - let loweredSelf = try signature.selfParameter.map { selfParameter in - try lowerParameter( - selfParameter.type, - convention: selfParameter.convention, - parameterName: selfParameter.parameterName ?? "self" - ) + // Lower the self parameter. + let loweredSelf = try signature.selfParameter.flatMap { selfParameter in + switch selfParameter { + case .instance(let selfParameter): + try lowerParameter( + selfParameter.type, + convention: selfParameter.convention, + parameterName: selfParameter.parameterName ?? "self" + ) + case .initializer, .staticMethod: + nil + } } // Collect all of the lowered parameters for the @_cdecl function. @@ -112,7 +134,6 @@ extension Swift2JavaTranslator { } let cdeclSignature = SwiftFunctionSignature( - isStaticOrClass: false, selfParameter: nil, parameters: cdeclLoweredParameters, result: cdeclResult @@ -301,7 +322,7 @@ extension LoweredFunctionSignature { @_spi(Testing) public func cdeclThunk( cName: String, - inputFunction: FunctionDeclSyntax, + swiftFunctionName: String, stdlibTypes: SwiftStandardLibraryTypes ) -> FunctionDeclSyntax { var loweredCDecl = cdecl.createFunctionDecl(cName) @@ -315,14 +336,33 @@ extension LoweredFunctionSignature { // Lower "self", if there is one. let parametersToLower: ArraySlice let cdeclToOriginalSelf: ExprSyntax? - if let originalSelfParam = original.selfParameter { - cdeclToOriginalSelf = try! ConversionStep( - cdeclToSwift: originalSelfParam.type - ).asExprSyntax( - isSelf: true, - placeholder: originalSelfParam.parameterName ?? "self" - ) - parametersToLower = parameters.dropLast() + var initializerType: SwiftType? = nil + if let originalSelf = original.selfParameter { + switch originalSelf { + case .instance(let originalSelfParam): + // The instance was provided to the cdecl thunk, so convert it to + // its Swift representation. + cdeclToOriginalSelf = try! ConversionStep( + cdeclToSwift: originalSelfParam.type + ).asExprSyntax( + isSelf: true, + placeholder: originalSelfParam.parameterName ?? "self" + ) + parametersToLower = parameters.dropLast() + + case .staticMethod(let selfType): + // Static methods use the Swift type as "self", but there is no + // corresponding cdecl parameter. + cdeclToOriginalSelf = "\(raw: selfType.description)" + parametersToLower = parameters[...] + + case .initializer(let selfType): + // Initializers use the Swift type to create the instance. Save it + // for later. There is no corresponding cdecl parameter. + initializerType = selfType + cdeclToOriginalSelf = nil + parametersToLower = parameters[...] + } } else { cdeclToOriginalSelf = nil parametersToLower = parameters[...] @@ -346,9 +386,14 @@ extension LoweredFunctionSignature { } // Form the call expression. - var callExpression: ExprSyntax = "\(inputFunction.name)(\(raw: cdeclToOriginalArguments.joined(separator: ", ")))" - if let cdeclToOriginalSelf { - callExpression = "\(cdeclToOriginalSelf).\(callExpression)" + let callArguments: ExprSyntax = "(\(raw: cdeclToOriginalArguments.joined(separator: ", ")))" + let callExpression: ExprSyntax + if let initializerType { + callExpression = "\(raw: initializerType.description)\(callArguments)" + } else if let cdeclToOriginalSelf { + callExpression = "\(cdeclToOriginalSelf).\(raw: swiftFunctionName)\(callArguments)" + } else { + callExpression = "\(raw: swiftFunctionName)\(callArguments)" } // Handle the return. diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftFunctionSignature.swift b/Sources/JExtractSwift/SwiftTypes/SwiftFunctionSignature.swift index c2b626f2..dc8aadb1 100644 --- a/Sources/JExtractSwift/SwiftTypes/SwiftFunctionSignature.swift +++ b/Sources/JExtractSwift/SwiftTypes/SwiftFunctionSignature.swift @@ -19,13 +19,25 @@ 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 selfParameter: SwiftSelfParameter? var parameters: [SwiftParameter] var result: SwiftResult } +/// Describes the "self" parameter of a Swift function signature. +enum SwiftSelfParameter: Equatable { + /// 'self' is an instance parameter. + case instance(SwiftParameter) + + /// 'self' is a metatype for a static method. We only need the type to + /// form the call. + case staticMethod(SwiftType) + + /// 'self' is the type for a call to an initializer. We only need the type + /// to form the call. + case initializer(SwiftType) +} + extension SwiftFunctionSignature { /// Create a function declaration with the given name that has this /// signature. @@ -49,6 +61,33 @@ extension SwiftFunctionSignature { } extension SwiftFunctionSignature { + init( + _ node: InitializerDeclSyntax, + enclosingType: SwiftType?, + symbolTable: SwiftSymbolTable + ) throws { + guard let enclosingType else { + throw SwiftFunctionTranslationError.missingEnclosingType(node) + } + + // We do not yet support failable initializers. + if node.optionalMark != nil { + throw SwiftFunctionTranslationError.failableInitializer(node) + } + + // Prohibit generics for now. + if let generics = node.genericParameterClause { + throw SwiftFunctionTranslationError.generic(generics) + } + + self.selfParameter = .initializer(enclosingType) + self.result = SwiftResult(convention: .direct, type: enclosingType) + self.parameters = try Self.translateFunctionSignature( + node.signature, + symbolTable: symbolTable + ) + } + init( _ node: FunctionDeclSyntax, enclosingType: SwiftType?, @@ -59,40 +98,36 @@ extension SwiftFunctionSignature { if let enclosingType { var isMutating = false var isConsuming = false - var isStaticOrClass = false + var isStatic = false for modifier in node.modifiers { switch modifier.name.tokenKind { case .keyword(.mutating): isMutating = true - case .keyword(.static), .keyword(.class): isStaticOrClass = true + case .keyword(.static): isStatic = true case .keyword(.consuming): isConsuming = true + case .keyword(.class): throw SwiftFunctionTranslationError.classMethod(modifier.name) default: break } } - if isStaticOrClass { - self.selfParameter = SwiftParameter( - convention: .byValue, - type: .metatype( - enclosingType - ) - ) + if isStatic { + self.selfParameter = .staticMethod(enclosingType) } else { - self.selfParameter = SwiftParameter( - convention: isMutating ? .inout : isConsuming ? .consuming : .byValue, - type: enclosingType + self.selfParameter = .instance( + SwiftParameter( + convention: isMutating ? .inout : isConsuming ? .consuming : .byValue, + type: enclosingType + ) ) } - - self.isStaticOrClass = isStaticOrClass } else { self.selfParameter = nil - self.isStaticOrClass = false } // Translate the parameters. - self.parameters = try node.signature.parameterClause.parameters.map { param in - try SwiftParameter(param, symbolTable: symbolTable) - } + self.parameters = try Self.translateFunctionSignature( + node.signature, + symbolTable: symbolTable + ) // Translate the result type. if let resultType = node.signature.returnClause?.type { @@ -104,17 +139,28 @@ extension SwiftFunctionSignature { self.result = SwiftResult(convention: .direct, type: .tuple([])) } + // Prohibit generics for now. + if let generics = node.genericParameterClause { + throw SwiftFunctionTranslationError.generic(generics) + } + } + + /// Translate the function signature, returning the list of translated + /// parameters. + static func translateFunctionSignature( + _ signature: FunctionSignatureSyntax, + symbolTable: SwiftSymbolTable + ) throws -> [SwiftParameter] { // FIXME: Prohibit effects for now. - if let throwsClause = node.signature.effectSpecifiers?.throwsClause { + if let throwsClause = signature.effectSpecifiers?.throwsClause { throw SwiftFunctionTranslationError.throws(throwsClause) } - if let asyncSpecifier = node.signature.effectSpecifiers?.asyncSpecifier { + if let asyncSpecifier = signature.effectSpecifiers?.asyncSpecifier { throw SwiftFunctionTranslationError.async(asyncSpecifier) } - // Prohibit generics for now. - if let generics = node.genericParameterClause { - throw SwiftFunctionTranslationError.generic(generics) + return try signature.parameterClause.parameters.map { param in + try SwiftParameter(param, symbolTable: symbolTable) } } } @@ -123,4 +169,7 @@ enum SwiftFunctionTranslationError: Error { case `throws`(ThrowsClauseSyntax) case async(TokenSyntax) case generic(GenericParameterClauseSyntax) + case classMethod(TokenSyntax) + case missingEnclosingType(InitializerDeclSyntax) + case failableInitializer(InitializerDeclSyntax) } diff --git a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift index e86d09df..6f7a5e83 100644 --- a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift @@ -42,14 +42,27 @@ func assertLoweredFunction( translator.prepareForTranslation() - let inputFunction = inputDecl.cast(FunctionDeclSyntax.self) - let loweredFunction = try translator.lowerFunctionSignature( - inputFunction, - enclosingType: enclosingType - ) + let swiftFunctionName: String + let loweredFunction: LoweredFunctionSignature + if let inputFunction = inputDecl.as(FunctionDeclSyntax.self) { + loweredFunction = try translator.lowerFunctionSignature( + inputFunction, + enclosingType: enclosingType + ) + swiftFunctionName = inputFunction.name.text + } else if let inputInitializer = inputDecl.as(InitializerDeclSyntax.self) { + loweredFunction = try translator.lowerFunctionSignature( + inputInitializer, + enclosingType: enclosingType + ) + swiftFunctionName = "init" + } else { + fatalError("Unhandling declaration kind for lowering") + } + let loweredCDecl = loweredFunction.cdeclThunk( - cName: "c_\(inputFunction.name.text)", - inputFunction: inputFunction, + cName: "c_\(swiftFunctionName)", + swiftFunctionName: swiftFunctionName, stdlibTypes: translator.swiftStdlibTypes ) @@ -65,7 +78,7 @@ func assertLoweredFunction( let cFunction = translator.cdeclToCFunctionLowering( loweredFunction.cdecl, - cName: "c_\(inputFunction.name.text)" + cName: "c_\(swiftFunctionName)" ) #expect( cFunction.description == expectedCFunction, diff --git a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift index f47ee3b9..66c02632 100644 --- a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift +++ b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift @@ -124,6 +124,76 @@ final class FunctionLoweringTests { ) } + @Test("Lowering static methods") + func loweringStaticMethods() throws { + try assertLoweredFunction(""" + static func scaledUnit(by value: Double) -> Point { } + """, + sourceFile: """ + struct Point { } + """, + enclosingType: "Point", + expectedCDecl: """ + @_cdecl("c_scaledUnit") + func c_scaledUnit(_ value: Double, _ _result: UnsafeMutableRawPointer) { + _result.assumingMemoryBound(to: Point.self).pointee = Point.scaledUnit(by: value) + } + """, + expectedCFunction: "void c_scaledUnit(double value, void* _result)" + ) + + try assertLoweredFunction(""" + static func randomPerson(seed: Double) -> Person { } + """, + sourceFile: """ + class Person { } + """, + enclosingType: "Person", + expectedCDecl: """ + @_cdecl("c_randomPerson") + func c_randomPerson(_ seed: Double) -> UnsafeRawPointer { + return unsafeBitCast(Person.randomPerson(seed: seed), to: UnsafeRawPointer.self) + } + """, + expectedCFunction: "void const* c_randomPerson(double seed)" + ) + } + + @Test("Lowering initializers") + func loweringInitializers() throws { + try assertLoweredFunction(""" + init(scaledBy value: Double) { } + """, + sourceFile: """ + struct Point { } + """, + enclosingType: "Point", + expectedCDecl: """ + @_cdecl("c_init") + func c_init(_ value: Double, _ _result: UnsafeMutableRawPointer) { + _result.assumingMemoryBound(to: Point.self).pointee = Point(scaledBy: value) + } + """, + expectedCFunction: "void c_init(double value, void* _result)" + ) + + try assertLoweredFunction(""" + init(seed: Double) { } + """, + sourceFile: """ + class Person { } + """, + enclosingType: "Person", + expectedCDecl: """ + @_cdecl("c_init") + func c_init(_ seed: Double) -> UnsafeRawPointer { + return unsafeBitCast(Person(seed: seed), to: UnsafeRawPointer.self) + } + """, + expectedCFunction: "void const* c_init(double seed)" + ) + } + @Test("Lowering metatypes") func lowerMetatype() throws { try assertLoweredFunction(""" @@ -222,4 +292,3 @@ final class FunctionLoweringTests { ) } } - From 97299cc39e15d247493819c4a5365e7c9309c10c Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 3 Feb 2025 20:36:36 +0100 Subject: [PATCH 10/15] cdecl lowering: move indirect returns after self This is meant to match the existing thunk generation. --- .../Swift2JavaTranslator+FunctionLowering.swift | 12 +++++++----- Tests/JExtractSwiftTests/FunctionLoweringTests.swift | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift b/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift index 84218a01..a18ddc76 100644 --- a/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift +++ b/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift @@ -113,6 +113,13 @@ extension Swift2JavaTranslator { contentsOf: loweredParameters.flatMap { $0.cdeclParameters } ) + // Lower self. + if let loweredSelf { + allLoweredParameters.append(loweredSelf) + cdeclLoweredParameters.append(contentsOf: loweredSelf.cdeclParameters) + } + + // Lower indirect results. let cdeclResult: SwiftResult if indirectResult { cdeclLoweredParameters.append( @@ -128,11 +135,6 @@ extension Swift2JavaTranslator { fatalError("Improper lowering of result for \(signature)") } - if let loweredSelf { - allLoweredParameters.append(loweredSelf) - cdeclLoweredParameters.append(contentsOf: loweredSelf.cdeclParameters) - } - let cdeclSignature = SwiftFunctionSignature( selfParameter: nil, parameters: cdeclLoweredParameters, diff --git a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift index 66c02632..c3b3bf58 100644 --- a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift +++ b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift @@ -78,11 +78,11 @@ final class FunctionLoweringTests { enclosingType: "Point", expectedCDecl: """ @_cdecl("c_shifted") - func c_shifted(_ delta_0: Double, _ delta_1: Double, _ _result: UnsafeMutableRawPointer, _ self: UnsafeRawPointer) { + func c_shifted(_ delta_0: Double, _ delta_1: Double, _ self: UnsafeRawPointer, _ _result: UnsafeMutableRawPointer) { _result.assumingMemoryBound(to: Point.self).pointee = self.assumingMemoryBound(to: Point.self).pointee.shifted(by: (delta_0, delta_1)) } """, - expectedCFunction: "void c_shifted(double delta_0, double delta_1, void* _result, void const* self)" + expectedCFunction: "void c_shifted(double delta_0, double delta_1, void const* self, void* _result)" ) } From 8f00df5e124a5d929fd2d215705cdea127ab7044 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 3 Feb 2025 22:02:20 +0100 Subject: [PATCH 11/15] cdecl thunks: property initialize indirect returns via initialize(to:) This requires us to walk the parallel structure between indirect returns (where we need to introduce the initialize(to:) calls instead of pointee references) and the Swift value. --- ...wift2JavaTranslator+FunctionLowering.swift | 87 +++++++++++++++---- .../FunctionLoweringTests.swift | 15 ++-- 2 files changed, 79 insertions(+), 23 deletions(-) diff --git a/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift b/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift index a18ddc76..95bbeb0b 100644 --- a/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift +++ b/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift @@ -430,17 +430,6 @@ extension LoweredFunctionSignature { originalResult = callExpression } - // FIXME: Check whether there are multiple places in which we reference - // the placeholder in resultConversion. If so, we should write it into a - // local let "_resultValue" or similar so we don't call the underlying - // function multiple times. - - // Convert the result. - let convertedResult = resultConversion.asExprSyntax( - isSelf: true, - placeholder: originalResult.description - ) - if cdecl.result.type.isVoid { // Indirect return. This is a regular return in Swift that turns // into an assignment via the indirect parameters. We do a cdeclToSwift @@ -450,16 +439,23 @@ extension LoweredFunctionSignature { let cdeclParamConversion = try! ConversionStep( cdeclToSwift: original.result.type ) - let indirectResults = cdeclParamConversion.asExprSyntax( - isSelf: true, - placeholder: "_result" - ) - bodyItems.append(""" - \(indirectResults) = \(convertedResult) - """ + + // For each indirect result, initialize the value directly with the + // corresponding element in the converted result. + bodyItems.append( + contentsOf: cdeclParamConversion.initialize( + placeholder: "_result", + from: resultConversion, + otherPlaceholder: originalResult.description + ) ) } else { // Direct return. Just convert the expression. + let convertedResult = resultConversion.asExprSyntax( + isSelf: true, + placeholder: originalResult.description + ) + bodyItems.append(""" return \(convertedResult) """ @@ -475,3 +471,58 @@ extension LoweredFunctionSignature { return loweredCDecl } } + +extension ConversionStep { + /// Form a set of statements that initializes the placeholders within + /// the given conversion step from ones in the other step, effectively + /// exploding something like `(a, (b, c)) = (d, (e, f))` into + /// separate initializations for a, b, and c from d, e, and f, respectively. + func initialize( + placeholder: String, + from otherStep: ConversionStep, + otherPlaceholder: String + ) -> [CodeBlockItemSyntax] { + // Create separate assignments for each element in paired tuples. + if case .tuplify(let elements) = self, + case .tuplify(let otherElements) = otherStep { + assert(elements.count == otherElements.count) + + return elements.indices.flatMap { index in + elements[index].initialize( + placeholder: "\(placeholder)_\(index)", + from: otherElements[index], + otherPlaceholder: "\(otherPlaceholder)_\(index)" + ) + } + } + + // Look through "pass indirectly" steps; they do nothing here. + if case .passIndirectly(let conversionStep) = self { + return conversionStep.initialize( + placeholder: placeholder, + from: otherStep, + otherPlaceholder: otherPlaceholder + ) + } + + // The value we're initializing from. + let otherExpr = otherStep.asExprSyntax( + isSelf: false, + placeholder: otherPlaceholder + ) + + // If we have a "pointee" on where we are performing initialization, we + // need to instead produce an initialize(to:) call. + if case .pointee(let innerSelf) = self { + let selfPointerExpr = innerSelf.asExprSyntax( + isSelf: true, + placeholder: placeholder + ) + + return [ " \(selfPointerExpr).initialize(to: \(otherExpr))" ] + } + + let selfExpr = self.asExprSyntax(isSelf: true, placeholder: placeholder) + return [ " \(selfExpr) = \(otherExpr)" ] + } +} diff --git a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift index c3b3bf58..bc08245c 100644 --- a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift +++ b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift @@ -79,7 +79,7 @@ final class FunctionLoweringTests { expectedCDecl: """ @_cdecl("c_shifted") func c_shifted(_ delta_0: Double, _ delta_1: Double, _ self: UnsafeRawPointer, _ _result: UnsafeMutableRawPointer) { - _result.assumingMemoryBound(to: Point.self).pointee = self.assumingMemoryBound(to: Point.self).pointee.shifted(by: (delta_0, delta_1)) + _result.assumingMemoryBound(to: Point.self).initialize(to: self.assumingMemoryBound(to: Point.self).pointee.shifted(by: (delta_0, delta_1))) } """, expectedCFunction: "void c_shifted(double delta_0, double delta_1, void const* self, void* _result)" @@ -136,7 +136,7 @@ final class FunctionLoweringTests { expectedCDecl: """ @_cdecl("c_scaledUnit") func c_scaledUnit(_ value: Double, _ _result: UnsafeMutableRawPointer) { - _result.assumingMemoryBound(to: Point.self).pointee = Point.scaledUnit(by: value) + _result.assumingMemoryBound(to: Point.self).initialize(to: Point.scaledUnit(by: value)) } """, expectedCFunction: "void c_scaledUnit(double value, void* _result)" @@ -171,7 +171,7 @@ final class FunctionLoweringTests { expectedCDecl: """ @_cdecl("c_init") func c_init(_ value: Double, _ _result: UnsafeMutableRawPointer) { - _result.assumingMemoryBound(to: Point.self).pointee = Point(scaledBy: value) + _result.assumingMemoryBound(to: Point.self).initialize(to: Point(scaledBy: value)) } """, expectedCFunction: "void c_init(double value, void* _result)" @@ -261,13 +261,18 @@ final class FunctionLoweringTests { @Test("Lowering tuple returns") func lowerTupleReturns() throws { try assertLoweredFunction(""" - func getTuple() -> (Int, (Float, Double)) { } + func getTuple() -> (Int, (Float, Point)) { } + """, + sourceFile: """ + struct Point { } """, expectedCDecl: """ @_cdecl("c_getTuple") func c_getTuple(_ _result_0: UnsafeMutableRawPointer, _ _result_1_0: UnsafeMutableRawPointer, _ _result_1_1: UnsafeMutableRawPointer) { let __swift_result = getTuple() - (_result_0, (_result_1_0, _result_1_1)) = (__swift_result_0, (__swift_result_1_0, __swift_result_1_1)) + _result_0 = __swift_result_0 + _result_1_0 = __swift_result_1_0 + _result_1_1.assumingMemoryBound(to: Point.self).initialize(to: __swift_result_1_1) } """, expectedCFunction: "void c_getTuple(void* _result_0, void* _result_1_0, void* _result_1_1)" From e11dceae04607c50b399e2d63159a48169c52fef Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 3 Feb 2025 22:13:23 +0100 Subject: [PATCH 12/15] Always mark @cdecl thunks we generate in lowering as public --- ...wift2JavaTranslator+FunctionLowering.swift | 6 ++++ .../FunctionLoweringTests.swift | 32 +++++++++---------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift b/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift index 95bbeb0b..edc78fa7 100644 --- a/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift +++ b/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift @@ -49,6 +49,7 @@ extension Swift2JavaTranslator { return try lowerFunctionSignature(signature) } + /// Lower the given Swift function signature to a Swift @_cdecl function signature, /// which is C compatible, and the corresponding Java method signature. /// @@ -333,6 +334,11 @@ extension LoweredFunctionSignature { let cdeclAttribute: AttributeSyntax = "@_cdecl(\(literal: cName))\n" loweredCDecl.attributes.append(.attribute(cdeclAttribute)) + // Make it public. + loweredCDecl.modifiers.append( + DeclModifierSyntax(name: .keyword(.public), trailingTrivia: .space) + ) + // Create the body. // Lower "self", if there is one. diff --git a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift index bc08245c..3a036594 100644 --- a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift +++ b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift @@ -26,7 +26,7 @@ final class FunctionLoweringTests { """, expectedCDecl: """ @_cdecl("c_f") - func c_f(_ x: Int, _ y: Float, _ z_pointer: UnsafeRawPointer, _ z_count: Int) { + public func c_f(_ x: Int, _ y: Float, _ z_pointer: UnsafeRawPointer, _ z_count: Int) { f(x: x, y: y, z: UnsafeBufferPointer(start: z_pointer.assumingMemoryBound(to: Bool.self), count: z_count)) } """, @@ -41,7 +41,7 @@ final class FunctionLoweringTests { """, expectedCDecl: """ @_cdecl("c_f") - func c_f(_ t_0: Int, _ t_1_0: Float, _ t_1_1: Double, _ z_pointer: UnsafeRawPointer) -> Int { + public 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)) } """, @@ -59,7 +59,7 @@ final class FunctionLoweringTests { """, expectedCDecl: """ @_cdecl("c_shift") - func c_shift(_ point: UnsafeMutableRawPointer, _ delta_0: Double, _ delta_1: Double) { + public func c_shift(_ point: UnsafeMutableRawPointer, _ delta_0: Double, _ delta_1: Double) { shift(point: &point.assumingMemoryBound(to: Point.self).pointee, by: (delta_0, delta_1)) } """, @@ -78,7 +78,7 @@ final class FunctionLoweringTests { enclosingType: "Point", expectedCDecl: """ @_cdecl("c_shifted") - func c_shifted(_ delta_0: Double, _ delta_1: Double, _ self: UnsafeRawPointer, _ _result: UnsafeMutableRawPointer) { + public func c_shifted(_ delta_0: Double, _ delta_1: Double, _ self: UnsafeRawPointer, _ _result: UnsafeMutableRawPointer) { _result.assumingMemoryBound(to: Point.self).initialize(to: self.assumingMemoryBound(to: Point.self).pointee.shifted(by: (delta_0, delta_1))) } """, @@ -97,7 +97,7 @@ final class FunctionLoweringTests { enclosingType: "Point", expectedCDecl: """ @_cdecl("c_shift") - func c_shift(_ delta_0: Double, _ delta_1: Double, _ self: UnsafeMutableRawPointer) { + public func c_shift(_ delta_0: Double, _ delta_1: Double, _ self: UnsafeMutableRawPointer) { self.assumingMemoryBound(to: Point.self).pointee.shift(by: (delta_0, delta_1)) } """, @@ -116,7 +116,7 @@ final class FunctionLoweringTests { enclosingType: "Point", expectedCDecl: """ @_cdecl("c_shift") - func c_shift(_ delta_0: Double, _ delta_1: Double, _ self: UnsafeRawPointer) { + public func c_shift(_ delta_0: Double, _ delta_1: Double, _ self: UnsafeRawPointer) { unsafeBitCast(self, to: Point.self).shift(by: (delta_0, delta_1)) } """, @@ -135,7 +135,7 @@ final class FunctionLoweringTests { enclosingType: "Point", expectedCDecl: """ @_cdecl("c_scaledUnit") - func c_scaledUnit(_ value: Double, _ _result: UnsafeMutableRawPointer) { + public func c_scaledUnit(_ value: Double, _ _result: UnsafeMutableRawPointer) { _result.assumingMemoryBound(to: Point.self).initialize(to: Point.scaledUnit(by: value)) } """, @@ -151,7 +151,7 @@ final class FunctionLoweringTests { enclosingType: "Person", expectedCDecl: """ @_cdecl("c_randomPerson") - func c_randomPerson(_ seed: Double) -> UnsafeRawPointer { + public func c_randomPerson(_ seed: Double) -> UnsafeRawPointer { return unsafeBitCast(Person.randomPerson(seed: seed), to: UnsafeRawPointer.self) } """, @@ -170,7 +170,7 @@ final class FunctionLoweringTests { enclosingType: "Point", expectedCDecl: """ @_cdecl("c_init") - func c_init(_ value: Double, _ _result: UnsafeMutableRawPointer) { + public func c_init(_ value: Double, _ _result: UnsafeMutableRawPointer) { _result.assumingMemoryBound(to: Point.self).initialize(to: Point(scaledBy: value)) } """, @@ -186,7 +186,7 @@ final class FunctionLoweringTests { enclosingType: "Person", expectedCDecl: """ @_cdecl("c_init") - func c_init(_ seed: Double) -> UnsafeRawPointer { + public func c_init(_ seed: Double) -> UnsafeRawPointer { return unsafeBitCast(Person(seed: seed), to: UnsafeRawPointer.self) } """, @@ -201,7 +201,7 @@ final class FunctionLoweringTests { """, expectedCDecl: """ @_cdecl("c_f") - func c_f(_ t: UnsafeRawPointer) { + public func c_f(_ t: UnsafeRawPointer) { f(t: unsafeBitCast(t, to: Int.self)) } """, @@ -213,7 +213,7 @@ final class FunctionLoweringTests { """, expectedCDecl: """ @_cdecl("c_f") - func c_f() -> UnsafeRawPointer { + public func c_f() -> UnsafeRawPointer { return unsafeBitCast(f(), to: UnsafeRawPointer.self) } """, @@ -232,7 +232,7 @@ final class FunctionLoweringTests { enclosingType: "Point", expectedCDecl: """ @_cdecl("c_shifted") - func c_shifted(_ delta_0: Double, _ delta_1: Double, _ self: UnsafeRawPointer) -> UnsafeRawPointer { + public func c_shifted(_ delta_0: Double, _ delta_1: Double, _ self: UnsafeRawPointer) -> UnsafeRawPointer { return unsafeBitCast(unsafeBitCast(self, to: Point.self).shifted(by: (delta_0, delta_1)), to: UnsafeRawPointer.self) } """, @@ -250,7 +250,7 @@ final class FunctionLoweringTests { """, expectedCDecl: """ @_cdecl("c_getPointer") - func c_getPointer() -> UnsafeRawPointer { + public func c_getPointer() -> UnsafeRawPointer { return UnsafeRawPointer(getPointer()) } """, @@ -268,7 +268,7 @@ final class FunctionLoweringTests { """, expectedCDecl: """ @_cdecl("c_getTuple") - func c_getTuple(_ _result_0: UnsafeMutableRawPointer, _ _result_1_0: UnsafeMutableRawPointer, _ _result_1_1: UnsafeMutableRawPointer) { + public func c_getTuple(_ _result_0: UnsafeMutableRawPointer, _ _result_1_0: UnsafeMutableRawPointer, _ _result_1_1: UnsafeMutableRawPointer) { let __swift_result = getTuple() _result_0 = __swift_result_0 _result_1_0 = __swift_result_1_0 @@ -289,7 +289,7 @@ final class FunctionLoweringTests { """, expectedCDecl: """ @_cdecl("c_getBufferPointer") - func c_getBufferPointer(_result_pointer: UnsafeMutableRawPointer, _result_count: UnsafeMutableRawPointer) { + public func c_getBufferPointer(_result_pointer: UnsafeMutableRawPointer, _result_count: UnsafeMutableRawPointer) { return UnsafeRawPointer(getPointer()) } """, From c6066af00adcd6a378f82989d34cfba7264dc5c0 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 3 Feb 2025 22:53:21 +0100 Subject: [PATCH 13/15] Leverage the direct Swift cdecl type <-> C type mapping for lowering When lowering a Swift function to a cdecl thunk, detect when a given Swift type is exactly representable in C. In such cases, just keep the type as written and pass it through without modification. Use this to allow `@convention(c)` functions to go through untouched. --- .../CDeclLowering/CDeclConversions.swift | 30 +++++++++--------- .../CDeclLowering/CRepresentation.swift | 7 +++-- ...wift2JavaTranslator+FunctionLowering.swift | 31 ++++++++++--------- .../SwiftTypes/SwiftFunctionType.swift | 7 ++++- .../SwiftTypes/SwiftParameter.swift | 8 +++-- .../JExtractSwift/SwiftTypes/SwiftType.swift | 4 +-- .../FunctionLoweringTests.swift | 17 ++++++++++ 7 files changed, 68 insertions(+), 36 deletions(-) diff --git a/Sources/JExtractSwift/CDeclLowering/CDeclConversions.swift b/Sources/JExtractSwift/CDeclLowering/CDeclConversions.swift index c8b7e57c..0099ae5c 100644 --- a/Sources/JExtractSwift/CDeclLowering/CDeclConversions.swift +++ b/Sources/JExtractSwift/CDeclLowering/CDeclConversions.swift @@ -17,6 +17,14 @@ extension ConversionStep { /// would be available in a @_cdecl function to represent the given Swift /// type, and convert that to an instance of the Swift type. init(cdeclToSwift swiftType: SwiftType) throws { + // If there is a 1:1 mapping between this Swift type and a C type, then + // there is no translation to do. + if let cType = try? CType(cdeclType: swiftType) { + _ = cType + self = .placeholder + return + } + switch swiftType { case .function, .optional: throw LoweringError.unhandledType(swiftType) @@ -29,13 +37,6 @@ extension ConversionStep { case .nominal(let nominal): if let knownType = nominal.nominalTypeDecl.knownStandardLibraryType { - // Swift types that map to primitive types in C. These can be passed - // through directly. - if knownType.primitiveCType != nil { - self = .placeholder - return - } - // Typed pointers if let firstGenericArgument = nominal.genericArguments?.first { switch knownType { @@ -100,6 +101,14 @@ extension ConversionStep { swiftToCDecl swiftType: SwiftType, stdlibTypes: SwiftStandardLibraryTypes ) throws { + // If there is a 1:1 mapping between this Swift type and a C type, then + // there is no translation to do. + if let cType = try? CType(cdeclType: swiftType) { + _ = cType + self = .placeholder + return + } + switch swiftType { case .function, .optional: throw LoweringError.unhandledType(swiftType) @@ -117,13 +126,6 @@ extension ConversionStep { case .nominal(let nominal): if let knownType = nominal.nominalTypeDecl.knownStandardLibraryType { - // Swift types that map to primitive types in C. These can be passed - // through directly. - if knownType.primitiveCType != nil { - self = .placeholder - return - } - // Typed pointers if nominal.genericArguments?.first != nil { switch knownType { diff --git a/Sources/JExtractSwift/CDeclLowering/CRepresentation.swift b/Sources/JExtractSwift/CDeclLowering/CRepresentation.swift index 08e10de1..15e4ebb8 100644 --- a/Sources/JExtractSwift/CDeclLowering/CRepresentation.swift +++ b/Sources/JExtractSwift/CDeclLowering/CRepresentation.swift @@ -16,8 +16,11 @@ extension CType { /// Lower the given Swift type down to a its corresponding C type. /// /// This operation only supports the subset of Swift types that are - /// representable in a Swift `@_cdecl` function. If lowering an arbitrary - /// Swift function, first go through Swift -> cdecl lowering. + /// representable in a Swift `@_cdecl` function, which means that they are + /// also directly representable in C. If lowering an arbitrary Swift + /// function, first go through Swift -> cdecl lowering. This function + /// will throw an error if it encounters a type that is not expressible in + /// C. init(cdeclType: SwiftType) throws { switch cdeclType { case .nominal(let nominalType): diff --git a/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift b/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift index edc78fa7..d65948ac 100644 --- a/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift +++ b/Sources/JExtractSwift/CDeclLowering/Swift2JavaTranslator+FunctionLowering.swift @@ -155,6 +155,22 @@ extension Swift2JavaTranslator { convention: SwiftParameterConvention, parameterName: String ) throws -> LoweredParameters { + // If there is a 1:1 mapping between this Swift type and a C type, we just + // need to add the corresponding C [arameter. + if let cType = try? CType(cdeclType: type), convention != .inout { + _ = cType + return LoweredParameters( + cdeclParameters: [ + SwiftParameter( + convention: convention, + parameterName: parameterName, + type: type, + canBeDirectReturn: true + ) + ] + ) + } + switch type { case .function, .optional: throw LoweringError.unhandledType(type) @@ -179,21 +195,6 @@ extension Swift2JavaTranslator { // Types from the Swift standard library that we know about. if let knownType = nominal.nominalTypeDecl.knownStandardLibraryType, convention != .inout { - // Swift types that map to primitive types in C. These can be passed - // through directly. - if knownType.primitiveCType != nil { - return LoweredParameters( - cdeclParameters: [ - SwiftParameter( - convention: convention, - parameterName: parameterName, - type: type, - canBeDirectReturn: true - ) - ] - ) - } - // Typed pointers are mapped down to their raw forms in cdecl entry // points. These can be passed through directly. if knownType == .unsafePointer || knownType == .unsafeMutablePointer { diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftFunctionType.swift b/Sources/JExtractSwift/SwiftTypes/SwiftFunctionType.swift index 1e5637e3..565a24c3 100644 --- a/Sources/JExtractSwift/SwiftTypes/SwiftFunctionType.swift +++ b/Sources/JExtractSwift/SwiftTypes/SwiftFunctionType.swift @@ -27,7 +27,12 @@ struct SwiftFunctionType: Equatable { extension SwiftFunctionType: CustomStringConvertible { var description: String { - return "(\(parameters.map { $0.descriptionInType } )) -> \(resultType.description)" + let parameterString = parameters.map { $0.descriptionInType }.joined(separator: ", ") + let conventionPrefix = switch convention { + case .c: "@convention(c) " + case .swift: "" + } + return "\(conventionPrefix)(\(parameterString)) -> \(resultType.description)" } } diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift b/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift index 4cdb27e8..24b3d8b4 100644 --- a/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift +++ b/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift @@ -64,8 +64,10 @@ extension SwiftParameter { var type = node.type var convention = SwiftParameterConvention.byValue if let attributedType = type.as(AttributedTypeSyntax.self) { + var sawUnknownSpecifier = false for specifier in attributedType.specifiers { guard case .simpleTypeSpecifier(let simple) = specifier else { + sawUnknownSpecifier = true continue } @@ -75,13 +77,15 @@ extension SwiftParameter { case .keyword(.inout): convention = .inout default: + sawUnknownSpecifier = true break } } // Ignore anything else in the attributed type. - // FIXME: We might want to check for these and ignore them. - type = attributedType.baseType + if !sawUnknownSpecifier && attributedType.attributes.isEmpty { + type = attributedType.baseType + } } self.convention = convention diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftType.swift b/Sources/JExtractSwift/SwiftTypes/SwiftType.swift index d83d5e63..8ee0b767 100644 --- a/Sources/JExtractSwift/SwiftTypes/SwiftType.swift +++ b/Sources/JExtractSwift/SwiftTypes/SwiftType.swift @@ -129,12 +129,12 @@ extension SwiftType { // Only recognize the "@convention(c)" and "@convention(swift)" attributes, and // then only on function types. // FIXME: This string matching is a horrible hack. - switch attributedType.trimmedDescription { + switch attributedType.attributes.trimmedDescription { case "@convention(c)", "@convention(swift)": let innerType = try SwiftType(attributedType.baseType, symbolTable: symbolTable) switch innerType { case .function(var functionType): - let isConventionC = attributedType.trimmedDescription == "@convention(c)" + let isConventionC = attributedType.attributes.trimmedDescription == "@convention(c)" let convention: SwiftFunctionType.Convention = isConventionC ? .c : .swift functionType.convention = convention self = .function(functionType) diff --git a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift index 3a036594..a17bfe26 100644 --- a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift +++ b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift @@ -296,4 +296,21 @@ final class FunctionLoweringTests { expectedCFunction: "c_getBufferPointer(void* _result_pointer, void* _result_count)" ) } + + @Test("Lowering C function types") + func lowerFunctionTypes() throws { + // FIXME: C pretty printing isn't handling parameters of function pointer + // type yet. + try assertLoweredFunction(""" + func doSomething(body: @convention(c) (Int32) -> Double) { } + """, + expectedCDecl: """ + @_cdecl("c_doSomething") + public func c_doSomething(_ body: @convention(c) (Int32) -> Double) { + doSomething(body: body) + } + """, + expectedCFunction: "void c_doSomething(double* body(int32_t))" + ) + } } From bfd791d69337b2b4b28f66dd2f624861175e00b6 Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 3 Feb 2025 17:32:02 -0800 Subject: [PATCH 14/15] Improve printing of C types with better spacing and proper parentheses --- Sources/JExtractSwift/CTypes/CFunction.swift | 10 +- Sources/JExtractSwift/CTypes/CType.swift | 115 +++++++++++++++--- Tests/JExtractSwiftTests/CTypeTests.swift | 34 +++++- .../FunctionLoweringTests.swift | 32 ++--- 4 files changed, 147 insertions(+), 44 deletions(-) diff --git a/Sources/JExtractSwift/CTypes/CFunction.swift b/Sources/JExtractSwift/CTypes/CFunction.swift index 9dc69e41..0d01f383 100644 --- a/Sources/JExtractSwift/CTypes/CFunction.swift +++ b/Sources/JExtractSwift/CTypes/CFunction.swift @@ -48,10 +48,9 @@ extension CFunction: CustomStringConvertible { public var description: String { var result = "" - resultType.printBefore(result: &result) + var hasEmptyPlaceholder = false + resultType.printBefore(hasEmptyPlaceholder: &hasEmptyPlaceholder, result: &result) - // FIXME: parentheses when needed. - result += " " result += name // Function parameters. @@ -64,7 +63,10 @@ extension CFunction: CustomStringConvertible { ) result += ")" - resultType.printAfter(result: &result) + resultType.printAfter( + hasEmptyPlaceholder: &hasEmptyPlaceholder, + result: &result + ) result += "" return result diff --git a/Sources/JExtractSwift/CTypes/CType.swift b/Sources/JExtractSwift/CTypes/CType.swift index e69e89f4..699ccfaf 100644 --- a/Sources/JExtractSwift/CTypes/CType.swift +++ b/Sources/JExtractSwift/CTypes/CType.swift @@ -74,7 +74,14 @@ public enum CType { extension CType: CustomStringConvertible { /// Print the part of this type that comes before the declarator, appending /// it to the provided `result` string. - func printBefore(result: inout String) { + func printBefore(hasEmptyPlaceholder: inout Bool, result: inout String) { + // Save the value of hasEmptyPlaceholder and restore it once we're done + // here. + let previousHasEmptyPlaceholder = hasEmptyPlaceholder + defer { + hasEmptyPlaceholder = previousHasEmptyPlaceholder + } + switch self { case .floating(let floating): switch floating { @@ -82,11 +89,25 @@ extension CType: CustomStringConvertible { case .double: result += "double" } + spaceBeforePlaceHolder( + hasEmptyPlaceholder: hasEmptyPlaceholder, + result: &result + ) + case .function(resultType: let resultType, parameters: _, variadic: _): - resultType.printBefore(result: &result) + let previousHasEmptyPlaceholder = hasEmptyPlaceholder + hasEmptyPlaceholder = false + defer { + hasEmptyPlaceholder = previousHasEmptyPlaceholder + } + resultType.printBefore( + hasEmptyPlaceholder: &hasEmptyPlaceholder, + result: &result + ) - // FIXME: Clang inserts a parentheses in here if there's a non-empty - // placeholder, which is Very Stateful. How should I model that? + if !previousHasEmptyPlaceholder { + result += "(" + } case .integral(let integral): switch integral { @@ -97,21 +118,47 @@ extension CType: CustomStringConvertible { case .size_t: result += "size_t" } + spaceBeforePlaceHolder( + hasEmptyPlaceholder: hasEmptyPlaceholder, + result: &result + ) + case .pointer(let pointee): - pointee.printBefore(result: &result) + var innerHasEmptyPlaceholder = false + pointee.printBefore( + hasEmptyPlaceholder: &innerHasEmptyPlaceholder, + result: &result + ) result += "*" case .qualified(const: let isConst, volatile: let isVolatile, type: let underlying): - underlying.printBefore(result: &result) + if isConst || isVolatile { + hasEmptyPlaceholder = false + } + + underlying.printBefore(hasEmptyPlaceholder: &hasEmptyPlaceholder, result: &result) // FIXME: "east const" is easier to print correctly, so do that. We could // follow Clang and decide when it's correct to print "west const" by // splitting the qualifiers before we get here. if isConst { - result += " const" + result += "const" + hasEmptyPlaceholder = false + + spaceBeforePlaceHolder( + hasEmptyPlaceholder: hasEmptyPlaceholder, + result: &result + ) + } if isVolatile { - result += " volatile" + result += "volatile" + hasEmptyPlaceholder = false + + spaceBeforePlaceHolder( + hasEmptyPlaceholder: hasEmptyPlaceholder, + result: &result + ) } case .tag(let tag): @@ -121,7 +168,18 @@ extension CType: CustomStringConvertible { case .union(let cUnion): result += "union \(cUnion.name)" } - case .void: result += "void" + spaceBeforePlaceHolder( + hasEmptyPlaceholder: hasEmptyPlaceholder, + result: &result + ) + + case .void: + result += "void" + + spaceBeforePlaceHolder( + hasEmptyPlaceholder: hasEmptyPlaceholder, + result: &result + ) } } @@ -146,13 +204,14 @@ extension CType: CustomStringConvertible { /// Print the part of the type that comes after the declarator, appending /// it to the provided `result` string. - func printAfter(result: inout String) { + func printAfter(hasEmptyPlaceholder: inout Bool, result: inout String) { switch self { case .floating, .integral, .tag, .void: break case .function(resultType: let resultType, parameters: let parameters, variadic: let variadic): - // FIXME: Clang inserts a parentheses in here if there's a non-empty - // placeholder, which is Very Stateful. How should I model that? + if !hasEmptyPlaceholder { + result += ")" + } result += "(" @@ -167,26 +226,37 @@ extension CType: CustomStringConvertible { result += ")" - resultType.printAfter(result: &result) + var innerHasEmptyPlaceholder = false + resultType.printAfter( + hasEmptyPlaceholder: &innerHasEmptyPlaceholder, + result: &result + ) case .pointer(let pointee): - pointee.printAfter(result: &result) + var innerHasEmptyPlaceholder = false + pointee.printAfter( + hasEmptyPlaceholder: &innerHasEmptyPlaceholder, + result: &result + ) case .qualified(const: _, volatile: _, type: let underlying): - underlying.printAfter(result: &result) + underlying.printAfter( + hasEmptyPlaceholder: &hasEmptyPlaceholder, + result: &result + ) } } /// Print this type into a string, with the given placeholder as the name /// of the entity being declared. public func print(placeholder: String?) -> String { + var hasEmptyPlaceholder = (placeholder == nil) var result = "" - printBefore(result: &result) + printBefore(hasEmptyPlaceholder: &hasEmptyPlaceholder, result: &result) if let placeholder { - result += " " result += placeholder } - printAfter(result: &result) + printAfter(hasEmptyPlaceholder: &hasEmptyPlaceholder, result: &result) return result } @@ -194,6 +264,15 @@ extension CType: CustomStringConvertible { public var description: String { print(placeholder: nil) } + + private func spaceBeforePlaceHolder( + hasEmptyPlaceholder: Bool, + result: inout String + ) { + if !hasEmptyPlaceholder { + result += " " + } + } } extension CType { diff --git a/Tests/JExtractSwiftTests/CTypeTests.swift b/Tests/JExtractSwiftTests/CTypeTests.swift index c60a38b6..1599adeb 100644 --- a/Tests/JExtractSwiftTests/CTypeTests.swift +++ b/Tests/JExtractSwiftTests/CTypeTests.swift @@ -17,7 +17,7 @@ import Testing @Suite("C type system tests") struct CTypeTests { - @Test("Function declaration printing") + @Test("C function declaration printing") func testFunctionDeclarationPrint() { let malloc = CFunction( resultType: .pointer(.void), @@ -27,7 +27,7 @@ struct CTypeTests { ], isVariadic: false ) - #expect(malloc.description == "void* malloc(size_t size)") + #expect(malloc.description == "void *malloc(size_t size)") let free = CFunction( resultType: .void, @@ -37,7 +37,7 @@ struct CTypeTests { ], isVariadic: false ) - #expect(free.description == "void free(void* ptr)") + #expect(free.description == "void free(void *ptr)") let snprintf = CFunction( resultType: .integral(.signed(bits: 32)), @@ -58,8 +58,8 @@ struct CTypeTests { ], isVariadic: true ) - #expect(snprintf.description == "int32_t snprintf(int8_t* str, size_t size, int8_t const* format, ...)") - #expect(snprintf.functionType.description == "int32_t(int8_t*, size_t, int8_t const*, ...)") + #expect(snprintf.description == "int32_t snprintf(int8_t *str, size_t size, int8_t const *format, ...)") + #expect(snprintf.functionType.description == "int32_t (int8_t *, size_t, int8_t const *, ...)") let rand = CFunction( resultType: .integral(.signed(bits: 32)), @@ -68,6 +68,28 @@ struct CTypeTests { isVariadic: false ) #expect(rand.description == "int32_t rand(void)") - #expect(rand.functionType.description == "int32_t(void)") + #expect(rand.functionType.description == "int32_t (void)") + } + + @Test("C pointer declarator printing") + func testPointerDeclaratorPrinting() { + let doit = CFunction( + resultType: .void, + name: "doit", + parameters: [ + .init( + name: "body", + type: .pointer( + .function( + resultType: .void, + parameters: [.integral(.bool)], + variadic: false + ) + ) + ) + ], + isVariadic: false + ) + #expect(doit.description == "void doit(void (*body)(_Bool))") } } diff --git a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift index a17bfe26..3104e291 100644 --- a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift +++ b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift @@ -30,7 +30,7 @@ final class FunctionLoweringTests { f(x: x, y: y, z: UnsafeBufferPointer(start: z_pointer.assumingMemoryBound(to: Bool.self), count: z_count)) } """, - expectedCFunction: "void c_f(ptrdiff_t x, float y, void const* z_pointer, ptrdiff_t z_count)" + expectedCFunction: "void c_f(ptrdiff_t x, float y, void const *z_pointer, ptrdiff_t z_count)" ) } @@ -45,7 +45,7 @@ final class FunctionLoweringTests { return f(t: (t_0, (t_1_0, t_1_1)), z: z_pointer.assumingMemoryBound(to: Int.self)) } """, - expectedCFunction: "ptrdiff_t c_f(ptrdiff_t t_0, float t_1_0, double t_1_1, void const* z_pointer)" + expectedCFunction: "ptrdiff_t c_f(ptrdiff_t t_0, float t_1_0, double t_1_1, void const *z_pointer)" ) } @@ -63,7 +63,7 @@ final class FunctionLoweringTests { shift(point: &point.assumingMemoryBound(to: Point.self).pointee, by: (delta_0, delta_1)) } """, - expectedCFunction: "void c_shift(void* point, double delta_0, double delta_1)" + expectedCFunction: "void c_shift(void *point, double delta_0, double delta_1)" ) } @@ -82,7 +82,7 @@ final class FunctionLoweringTests { _result.assumingMemoryBound(to: Point.self).initialize(to: self.assumingMemoryBound(to: Point.self).pointee.shifted(by: (delta_0, delta_1))) } """, - expectedCFunction: "void c_shifted(double delta_0, double delta_1, void const* self, void* _result)" + expectedCFunction: "void c_shifted(double delta_0, double delta_1, void const *self, void *_result)" ) } @@ -101,7 +101,7 @@ final class FunctionLoweringTests { self.assumingMemoryBound(to: Point.self).pointee.shift(by: (delta_0, delta_1)) } """, - expectedCFunction: "void c_shift(double delta_0, double delta_1, void* self)" + expectedCFunction: "void c_shift(double delta_0, double delta_1, void *self)" ) } @@ -120,7 +120,7 @@ final class FunctionLoweringTests { unsafeBitCast(self, to: Point.self).shift(by: (delta_0, delta_1)) } """, - expectedCFunction: "void c_shift(double delta_0, double delta_1, void const* self)" + expectedCFunction: "void c_shift(double delta_0, double delta_1, void const *self)" ) } @@ -139,7 +139,7 @@ final class FunctionLoweringTests { _result.assumingMemoryBound(to: Point.self).initialize(to: Point.scaledUnit(by: value)) } """, - expectedCFunction: "void c_scaledUnit(double value, void* _result)" + expectedCFunction: "void c_scaledUnit(double value, void *_result)" ) try assertLoweredFunction(""" @@ -155,7 +155,7 @@ final class FunctionLoweringTests { return unsafeBitCast(Person.randomPerson(seed: seed), to: UnsafeRawPointer.self) } """, - expectedCFunction: "void const* c_randomPerson(double seed)" + expectedCFunction: "void const *c_randomPerson(double seed)" ) } @@ -174,7 +174,7 @@ final class FunctionLoweringTests { _result.assumingMemoryBound(to: Point.self).initialize(to: Point(scaledBy: value)) } """, - expectedCFunction: "void c_init(double value, void* _result)" + expectedCFunction: "void c_init(double value, void *_result)" ) try assertLoweredFunction(""" @@ -190,7 +190,7 @@ final class FunctionLoweringTests { return unsafeBitCast(Person(seed: seed), to: UnsafeRawPointer.self) } """, - expectedCFunction: "void const* c_init(double seed)" + expectedCFunction: "void const *c_init(double seed)" ) } @@ -205,7 +205,7 @@ final class FunctionLoweringTests { f(t: unsafeBitCast(t, to: Int.self)) } """, - expectedCFunction: "void c_f(void const* t)" + expectedCFunction: "void c_f(void const *t)" ) try assertLoweredFunction(""" @@ -217,7 +217,7 @@ final class FunctionLoweringTests { return unsafeBitCast(f(), to: UnsafeRawPointer.self) } """, - expectedCFunction: "void const* c_f(void)" + expectedCFunction: "void const *c_f(void)" ) } @@ -236,7 +236,7 @@ final class FunctionLoweringTests { return unsafeBitCast(unsafeBitCast(self, to: Point.self).shifted(by: (delta_0, delta_1)), to: UnsafeRawPointer.self) } """, - expectedCFunction: "void const* c_shifted(double delta_0, double delta_1, void const* self)" + expectedCFunction: "void const *c_shifted(double delta_0, double delta_1, void const *self)" ) } @@ -254,7 +254,7 @@ final class FunctionLoweringTests { return UnsafeRawPointer(getPointer()) } """, - expectedCFunction: "void const* c_getPointer(void)" + expectedCFunction: "void const *c_getPointer(void)" ) } @@ -275,7 +275,7 @@ final class FunctionLoweringTests { _result_1_1.assumingMemoryBound(to: Point.self).initialize(to: __swift_result_1_1) } """, - expectedCFunction: "void c_getTuple(void* _result_0, void* _result_1_0, void* _result_1_1)" + expectedCFunction: "void c_getTuple(void *_result_0, void *_result_1_0, void *_result_1_1)" ) } @@ -310,7 +310,7 @@ final class FunctionLoweringTests { doSomething(body: body) } """, - expectedCFunction: "void c_doSomething(double* body(int32_t))" + expectedCFunction: "void c_doSomething(double (*body)(int32_t))" ) } } From e7bd5d09ad428fa931041821c86f727cd993cb1a Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Mon, 3 Feb 2025 17:53:11 -0800 Subject: [PATCH 15/15] Switch C type printing to prefer east const/volatile when possible Consistency be damned, it's what looks nice. Clang does it too. --- Sources/JExtractSwift/CTypes/CType.swift | 53 +++++++++++-------- Tests/JExtractSwiftTests/CTypeTests.swift | 15 +++++- .../FunctionLoweringTests.swift | 20 +++---- 3 files changed, 55 insertions(+), 33 deletions(-) diff --git a/Sources/JExtractSwift/CTypes/CType.swift b/Sources/JExtractSwift/CTypes/CType.swift index 699ccfaf..9b8744d0 100644 --- a/Sources/JExtractSwift/CTypes/CType.swift +++ b/Sources/JExtractSwift/CTypes/CType.swift @@ -132,33 +132,33 @@ extension CType: CustomStringConvertible { result += "*" case .qualified(const: let isConst, volatile: let isVolatile, type: let underlying): - if isConst || isVolatile { - hasEmptyPlaceholder = false + func printQualifier(_ qualifier: String, if condition: Bool) { + if condition { + result += qualifier + hasEmptyPlaceholder = false + + spaceBeforePlaceHolder( + hasEmptyPlaceholder: hasEmptyPlaceholder, + result: &result + ) + } } - underlying.printBefore(hasEmptyPlaceholder: &hasEmptyPlaceholder, result: &result) + let canPrefixQualifiers = underlying.canPrefixQualifiers + if canPrefixQualifiers { + printQualifier("const", if: isConst) + printQualifier("volatile", if: isVolatile) + } - // FIXME: "east const" is easier to print correctly, so do that. We could - // follow Clang and decide when it's correct to print "west const" by - // splitting the qualifiers before we get here. - if isConst { - result += "const" + if (isConst || isVolatile) && !canPrefixQualifiers { hasEmptyPlaceholder = false - - spaceBeforePlaceHolder( - hasEmptyPlaceholder: hasEmptyPlaceholder, - result: &result - ) - } - if isVolatile { - result += "volatile" - hasEmptyPlaceholder = false - spaceBeforePlaceHolder( - hasEmptyPlaceholder: hasEmptyPlaceholder, - result: &result - ) + underlying.printBefore(hasEmptyPlaceholder: &hasEmptyPlaceholder, result: &result) + + if !canPrefixQualifiers { + printQualifier("const", if: isConst) + printQualifier("volatile", if: isVolatile) } case .tag(let tag): @@ -265,6 +265,7 @@ extension CType: CustomStringConvertible { print(placeholder: nil) } + /// Print a space before the placeholder in a declarator. private func spaceBeforePlaceHolder( hasEmptyPlaceholder: Bool, result: inout String @@ -273,6 +274,16 @@ extension CType: CustomStringConvertible { result += " " } } + + /// Determine whether qualifiers can be printed before the given type + /// (`const int`) vs. having to be afterward to maintain semantics. + var canPrefixQualifiers: Bool { + switch self { + case .floating, .integral, .tag, .void: true + case .function, .pointer: false + case .qualified(const: _, volatile: _, type: let type): type.canPrefixQualifiers + } + } } extension CType { diff --git a/Tests/JExtractSwiftTests/CTypeTests.swift b/Tests/JExtractSwiftTests/CTypeTests.swift index 1599adeb..569296d6 100644 --- a/Tests/JExtractSwiftTests/CTypeTests.swift +++ b/Tests/JExtractSwiftTests/CTypeTests.swift @@ -58,8 +58,8 @@ struct CTypeTests { ], isVariadic: true ) - #expect(snprintf.description == "int32_t snprintf(int8_t *str, size_t size, int8_t const *format, ...)") - #expect(snprintf.functionType.description == "int32_t (int8_t *, size_t, int8_t const *, ...)") + #expect(snprintf.description == "int32_t snprintf(int8_t *str, size_t size, const int8_t *format, ...)") + #expect(snprintf.functionType.description == "int32_t (int8_t *, size_t, const int8_t *, ...)") let rand = CFunction( resultType: .integral(.signed(bits: 32)), @@ -91,5 +91,16 @@ struct CTypeTests { isVariadic: false ) #expect(doit.description == "void doit(void (*body)(_Bool))") + + let ptrptr = CType.pointer( + .qualified( + const: true, + volatile: false, + type: .pointer( + .qualified(const: false, volatile: true, type: .void) + ) + ) + ) + #expect(ptrptr.description == "volatile void *const *") } } diff --git a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift index 3104e291..97fa95fc 100644 --- a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift +++ b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift @@ -30,7 +30,7 @@ final class FunctionLoweringTests { f(x: x, y: y, z: UnsafeBufferPointer(start: z_pointer.assumingMemoryBound(to: Bool.self), count: z_count)) } """, - expectedCFunction: "void c_f(ptrdiff_t x, float y, void const *z_pointer, ptrdiff_t z_count)" + expectedCFunction: "void c_f(ptrdiff_t x, float y, const void *z_pointer, ptrdiff_t z_count)" ) } @@ -45,7 +45,7 @@ final class FunctionLoweringTests { return f(t: (t_0, (t_1_0, t_1_1)), z: z_pointer.assumingMemoryBound(to: Int.self)) } """, - expectedCFunction: "ptrdiff_t c_f(ptrdiff_t t_0, float t_1_0, double t_1_1, void const *z_pointer)" + expectedCFunction: "ptrdiff_t c_f(ptrdiff_t t_0, float t_1_0, double t_1_1, const void *z_pointer)" ) } @@ -82,7 +82,7 @@ final class FunctionLoweringTests { _result.assumingMemoryBound(to: Point.self).initialize(to: self.assumingMemoryBound(to: Point.self).pointee.shifted(by: (delta_0, delta_1))) } """, - expectedCFunction: "void c_shifted(double delta_0, double delta_1, void const *self, void *_result)" + expectedCFunction: "void c_shifted(double delta_0, double delta_1, const void *self, void *_result)" ) } @@ -120,7 +120,7 @@ final class FunctionLoweringTests { unsafeBitCast(self, to: Point.self).shift(by: (delta_0, delta_1)) } """, - expectedCFunction: "void c_shift(double delta_0, double delta_1, void const *self)" + expectedCFunction: "void c_shift(double delta_0, double delta_1, const void *self)" ) } @@ -155,7 +155,7 @@ final class FunctionLoweringTests { return unsafeBitCast(Person.randomPerson(seed: seed), to: UnsafeRawPointer.self) } """, - expectedCFunction: "void const *c_randomPerson(double seed)" + expectedCFunction: "const void *c_randomPerson(double seed)" ) } @@ -190,7 +190,7 @@ final class FunctionLoweringTests { return unsafeBitCast(Person(seed: seed), to: UnsafeRawPointer.self) } """, - expectedCFunction: "void const *c_init(double seed)" + expectedCFunction: "const void *c_init(double seed)" ) } @@ -205,7 +205,7 @@ final class FunctionLoweringTests { f(t: unsafeBitCast(t, to: Int.self)) } """, - expectedCFunction: "void c_f(void const *t)" + expectedCFunction: "void c_f(const void *t)" ) try assertLoweredFunction(""" @@ -217,7 +217,7 @@ final class FunctionLoweringTests { return unsafeBitCast(f(), to: UnsafeRawPointer.self) } """, - expectedCFunction: "void const *c_f(void)" + expectedCFunction: "const void *c_f(void)" ) } @@ -236,7 +236,7 @@ final class FunctionLoweringTests { return unsafeBitCast(unsafeBitCast(self, to: Point.self).shifted(by: (delta_0, delta_1)), to: UnsafeRawPointer.self) } """, - expectedCFunction: "void const *c_shifted(double delta_0, double delta_1, void const *self)" + expectedCFunction: "const void *c_shifted(double delta_0, double delta_1, const void *self)" ) } @@ -254,7 +254,7 @@ final class FunctionLoweringTests { return UnsafeRawPointer(getPointer()) } """, - expectedCFunction: "void const *c_getPointer(void)" + expectedCFunction: "const void *c_getPointer(void)" ) }