diff --git a/Samples/SwiftJavaExtractKotlinSampleApp/Package.swift b/Samples/SwiftJavaExtractKotlinSampleApp/Package.swift new file mode 100644 index 000000000..035b2f83d --- /dev/null +++ b/Samples/SwiftJavaExtractKotlinSampleApp/Package.swift @@ -0,0 +1,42 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import CompilerPluginSupport +import PackageDescription + +let package = Package( + name: "SwiftJavaExtractKotlinSampleApp", + platforms: [ + .macOS(.v15), + .iOS(.v18), + .watchOS(.v11), + .tvOS(.v18), + ], + products: [ + .library( + name: "MySwiftLibrary", + type: .dynamic, + targets: ["MySwiftLibrary"] + ) + ], + dependencies: [ + .package(name: "swift-java", path: "../../") + ], + targets: [ + .target( + name: "MySwiftLibrary", + dependencies: [ + .product(name: "SwiftJava", package: "swift-java") + ], + exclude: [ + "swift-java.config" + ], + swiftSettings: [ + .swiftLanguageMode(.v5) + ], + plugins: [ + .plugin(name: "JExtractSwiftPlugin", package: "swift-java") + ] + ) + ] +) diff --git a/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/Data.swift b/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/Data.swift new file mode 100644 index 000000000..12295fffe --- /dev/null +++ b/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/Data.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 +// +//===----------------------------------------------------------------------===// + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +public func echoData(_ data: Data) -> Data { + data +} + +public func makeData() -> Data { + Data([0x01, 0x02, 0x03, 0x04]) +} + +public func getDataCount(_ data: Data) -> Int { + data.count +} + +public func compareData(_ data1: Data, _ data2: Data) -> Bool { + data1 == data2 +} diff --git a/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/LibrarySubDirectory/SwiftTypeInSubDirectory.swift b/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/LibrarySubDirectory/SwiftTypeInSubDirectory.swift new file mode 100644 index 000000000..520c8c54e --- /dev/null +++ b/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/LibrarySubDirectory/SwiftTypeInSubDirectory.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +public final class SwiftTypeInSubDirectory { + public init() {} + + public func hello() -> Int { + 12 + } +} diff --git a/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/MultiplePublicTypes.swift b/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/MultiplePublicTypes.swift new file mode 100644 index 000000000..5b2ea2cf1 --- /dev/null +++ b/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/MultiplePublicTypes.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +// This file exists to exercise the swiftpm plugin generating separate output Java files +// for the public types; because Java public types must be in a file with the same name as the type. + +public struct PublicTypeOne { + public init() {} + public func test() {} +} + +public struct PublicTypeTwo { + public init() {} + public func test() {} +} diff --git a/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift new file mode 100644 index 000000000..b1a4e4899 --- /dev/null +++ b/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +public class MySwiftClass { + + public let byte: UInt8 = 0 + public var len: Int + public var cap: Int + + public init(len: Int, cap: Int) { + self.len = len + self.cap = cap + } + + deinit { + } + + public var counter: Int32 = 0 + + public static func factory(len: Int, cap: Int) -> MySwiftClass { + MySwiftClass(len: len, cap: cap) + } + + public func voidMethod() { + } + + public func takeIntMethod(i: Int) { + } + + public func echoIntMethod(i: Int) -> Int { + i + } + + public func makeIntMethod() -> Int { + 12 + } + + public func makeRandomIntMethod() -> Int { + Int.random(in: 1..<256) + } + + public func takeUnsignedChar(arg: UInt16) { + } + + public func takeUnsignedInt(arg: UInt32) { + } + + public func takeUnsignedLong(arg: UInt64) { + } +} diff --git a/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift new file mode 100644 index 000000000..f67e0bfc5 --- /dev/null +++ b/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -0,0 +1,142 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +// This is a "plain Swift" file containing various types of declarations, +// that is exported to Java by using the `jextract-swift` tool. +// +// No annotations are necessary on the Swift side to perform the export. + +import Foundation + +#if os(Linux) +import Glibc +#else +import Darwin.C +#endif + +public func helloWorld() { +} + +public func globalTakeInt(i: Int) { +} + +public func globalMakeInt() -> Int { + 42 +} + +public func globalWriteString(string: String) -> Int { + string.count +} + +public func globalTakeIntInt(i: Int, j: Int) { +} + +public func globalCallMeRunnable(run: () -> Void) { + run() +} + +public func globalReceiveRawBuffer(buf: UnsafeRawBufferPointer) -> Int { + buf.count +} + +public var globalBuffer: UnsafeRawBufferPointer = UnsafeRawBufferPointer( + UnsafeMutableRawBufferPointer.allocate(byteCount: 124, alignment: 1) +) + +public func globalReceiveReturnData(data: Data) -> Data { + Data(data) +} + +public func withBuffer(body: (UnsafeRawBufferPointer) -> Void) { + body(globalBuffer) +} + +public func getArray() -> [UInt8] { + [1, 2, 3] +} + +// Tuple round-trips for jextract FFM (see `FFMTupleTest` in the sample app). +public func ffmTupleReturnPair() -> (Int32, Int64) { + (42, 43) +} + +public func ffmTupleSumPair(_ arg: (Int32, Int64)) -> Int64 { + Int64(arg.0) + arg.1 +} + +public func ffmTupleLabeledPair() -> (x: Int32, y: Int32) { + (x: 10, y: 20) +} + +public func sumAllByteArrayElements(actuallyAnArray: UnsafeRawPointer, count: Int) -> Int { + let bufferPointer = UnsafeRawBufferPointer(start: actuallyAnArray, count: count) + let array = Array(bufferPointer) + return Int(array.reduce(0, { partialResult, element in partialResult + element })) +} + +public func sumAllByteArrayElements(array: [UInt8]) -> Int { + Int(array.reduce(0, { partialResult, element in partialResult + element })) +} +public func returnSwiftArray() -> [UInt8] { + [1, 2, 3, 4] +} + +public func withArray(body: ([UInt8]) -> Void) { + body([1, 2, 3]) +} + +public func globalReceiveSomeDataProtocol(data: some DataProtocol) -> Int { + p(Array(data).description) + return data.count +} + +public func globalReceiveOptional(o1: Int?, o2: (some DataProtocol)?) -> Int { + switch (o1, o2) { + case (nil, nil): + return 0 + case (let v1?, nil): + return 1 + case (nil, let v2?): + return 2 + case (let v1?, let v2?): + return 3 + } +} + +// ==== ----------------------------------------------------------------------- +// MARK: Overloaded functions + +public func globalOverloaded(a: Int) { + p("globalOverloaded(a: \(a))") +} + +public func globalOverloaded(b: Int) { + p("globalOverloaded(b: \(b))") +} + +// ==== Internal helpers + +func p(_ msg: String, file: String = #fileID, line: UInt = #line, function: String = #function) { + print("[swift][\(file):\(line)](\(function)) \(msg)") + fflush(stdout) +} + +#if os(Linux) +// FIXME: why do we need this workaround? +@_silgen_name("_objc_autoreleaseReturnValue") +public func _objc_autoreleaseReturnValue(a: Any) {} + +@_silgen_name("objc_autoreleaseReturnValue") +public func objc_autoreleaseReturnValue(a: Any) {} +#endif diff --git a/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift b/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift new file mode 100644 index 000000000..861d132db --- /dev/null +++ b/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/MySwiftStruct.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +public struct MySwiftStruct { + + private var cap: Int + private var len: Int + private var subscriptValue: Int + private var subscriptArray: [Int] + + public init(cap: Int, len: Int) { + self.cap = cap + self.len = len + self.subscriptValue = 0 + self.subscriptArray = [10, 20, 15, 75] + } + + public func voidMethod() { + } + + public func takeIntMethod(i: Int) { + } + + public func echoIntMethod(i: Int) -> Int { + i + } + + public func makeIntMethod() -> Int { + 12 + } + + public func getCapacity() -> Int { + self.cap + } + + public func getLength() -> Int { + self.len + } + + public func withCapLen(_ body: (Int, Int) -> Void) { + body(cap, len) + } + + public mutating func increaseCap(by value: Int) -> Int { + precondition(value > 0) + self.cap += value + return self.cap + } + + public func makeRandomIntMethod() -> Int { + Int.random(in: 1..<256) + } + + public func getSubscriptValue() -> Int { + self.subscriptValue + } + + public func getSubscriptArrayValue(index: Int) -> Int { + self.subscriptArray[index] + } + + public subscript() -> Int { + get { subscriptValue } + set { subscriptValue = newValue } + } + + public subscript(index: Int) -> Int { + get { subscriptArray[index] } + set { subscriptArray[index] = newValue } + } + + // operator functions are ignored. + public static func == (lhs: MySwiftStruct, rhs: MySwiftStruct) -> Bool { + false + } +} diff --git a/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/swift-java.config b/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/swift-java.config new file mode 100644 index 000000000..19ba627c2 --- /dev/null +++ b/Samples/SwiftJavaExtractKotlinSampleApp/Sources/MySwiftLibrary/swift-java.config @@ -0,0 +1,6 @@ +{ + "javaPackage": "com.example.swift", + "mode": "kotlin", + "enableJavaCallbacks": false, + "logLevel": "debug" +} diff --git a/Samples/SwiftJavaExtractKotlinSampleApp/build.gradle.kts b/Samples/SwiftJavaExtractKotlinSampleApp/build.gradle.kts new file mode 100644 index 000000000..ee8ba2c2a --- /dev/null +++ b/Samples/SwiftJavaExtractKotlinSampleApp/build.gradle.kts @@ -0,0 +1,51 @@ +import utilities.javaLibraryPaths +import utilities.registerJextractTask + +plugins { + kotlin("jvm") version "2.3.0" + application + id("build-logic.java-application-conventions") +} + +group = "com.example" +version = "unspecified" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(kotlin("test")) +} + +kotlin { + jvmToolchain(25) +} + +val jextract = registerJextractTask() + +sourceSets { + main { + kotlin { + srcDir(jextract) + } + } +} + +tasks.build { + dependsOn(jextract) +} + +registerCleanSwift() + +application { + mainClass = "com.example.swift.HelloKotlin2Swift" + + applicationDefaultJvmArgs = listOf( + "--enable-native-access=ALL-UNNAMED", + // Include the library paths where our dylibs are that we want to load and call + "-Djava.library.path=" + (javaLibraryPaths(rootDir) + javaLibraryPaths(project.projectDir)).joinToString(":"), + // Enable tracing downcalls (to Swift) + "-Djextract.trace.downcalls=true" + ) +} diff --git a/Samples/SwiftJavaExtractKotlinSampleApp/gradle.properties b/Samples/SwiftJavaExtractKotlinSampleApp/gradle.properties new file mode 120000 index 000000000..7677fb73b --- /dev/null +++ b/Samples/SwiftJavaExtractKotlinSampleApp/gradle.properties @@ -0,0 +1 @@ +../gradle.properties \ No newline at end of file diff --git a/Samples/SwiftJavaExtractKotlinSampleApp/gradlew b/Samples/SwiftJavaExtractKotlinSampleApp/gradlew new file mode 120000 index 000000000..343e0d2ca --- /dev/null +++ b/Samples/SwiftJavaExtractKotlinSampleApp/gradlew @@ -0,0 +1 @@ +../../gradlew \ No newline at end of file diff --git a/Samples/SwiftJavaExtractKotlinSampleApp/gradlew.bat b/Samples/SwiftJavaExtractKotlinSampleApp/gradlew.bat new file mode 120000 index 000000000..cb5a94645 --- /dev/null +++ b/Samples/SwiftJavaExtractKotlinSampleApp/gradlew.bat @@ -0,0 +1 @@ +../../gradlew.bat \ No newline at end of file diff --git a/Samples/SwiftJavaExtractKotlinSampleApp/src/main/kotlin/com/example/swift/HelloKotlin2Swift.kt b/Samples/SwiftJavaExtractKotlinSampleApp/src/main/kotlin/com/example/swift/HelloKotlin2Swift.kt new file mode 100644 index 000000000..581b53f34 --- /dev/null +++ b/Samples/SwiftJavaExtractKotlinSampleApp/src/main/kotlin/com/example/swift/HelloKotlin2Swift.kt @@ -0,0 +1,17 @@ +package com.example.swift + +/** + * Downcall to Swift: + * {@snippet lang=swift : + * public var cap: Int + * } + */ +fun main() { + println("Hello from Kotlin to Swift!") +} + +class HelloKotlin2Swift private constructor() { + override fun toString(): String { + TODO("Not implemented") + } +} diff --git a/Sources/JExtractSwiftLib/Kotlin/KotlinParameter.swift b/Sources/JExtractSwiftLib/Kotlin/KotlinParameter.swift new file mode 100644 index 000000000..99909722c --- /dev/null +++ b/Sources/JExtractSwiftLib/Kotlin/KotlinParameter.swift @@ -0,0 +1,57 @@ +// +// KotlinParameter.swift +// swift-java +// +// Created by Tanish Azad on 30/03/26. +// + +import SwiftJavaJNICore + +/// Represent a parameter in Java code. +struct KotlinParameter { + enum ParameterType: CustomStringConvertible { + case concrete(JavaType) + case generic(name: String, extends: [JavaType]) + + /// Returns the concrete JavaType, or `.class` for generics. + var javaType: JavaType { + switch self { + case .concrete(let type): type + case .generic: .class(package: "java.lang", name: "Object") + } + } + + var description: String { + switch self { + case .concrete(let type): type.description + case .generic(let name, _): name + } + } + } + var name: String + var type: ParameterType + + /// Parameter annotations are used in parameter declarations like this: `@Annotation int example` + let annotations: [JavaAnnotation] + + init(name: String, type: ParameterType, annotations: [JavaAnnotation] = []) { + self.name = name + self.type = type + self.annotations = annotations + } + + init(name: String, type: JavaType, annotations: [JavaAnnotation] = []) { + self.name = name + self.type = .concrete(type) + self.annotations = annotations + } + + func renderParameter() -> String { + if annotations.isEmpty { + return "\(name): \(type)" + } + + let annotationsStr = annotations.map({ $0.render() }).joined(separator: "") + return "\(annotationsStr) \(name): \(type)" + } +} diff --git a/Sources/JExtractSwiftLib/Kotlin/Swift2KotlinGenerator+KotlinBindingsPrinting.swift b/Sources/JExtractSwiftLib/Kotlin/Swift2KotlinGenerator+KotlinBindingsPrinting.swift new file mode 100644 index 000000000..e4b0f5396 --- /dev/null +++ b/Sources/JExtractSwiftLib/Kotlin/Swift2KotlinGenerator+KotlinBindingsPrinting.swift @@ -0,0 +1,50 @@ +// +// Swift2KotlinGenerator+KotlinBindingsPrinting.swift +// swift-java +// +// Created by Tanish Azad on 30/03/26. +// + +import CodePrinting + +extension Swift2KotlinGenerator { + /// Print the calling body that forwards all the parameters to the `methodName`, + package func printKotlinBindingPlaceholder( + _ printer: inout CodePrinter, + _ decl: ImportedFunc, + ) { + let translated = self.translatedDecl(for: decl)! + let methodName = translated.name + + var modifiers = "public" + switch decl.functionSignature.selfParameter { + case .staticMethod, .initializer, nil: + modifiers.append(" static") + default: + break + } + + let translatedSignature = translated.translatedSignature + let returnTy = translatedSignature.result.javaResultType + + var annotationsStr = translatedSignature.annotations.map({ $0.render() }).joined(separator: "\n") + if !annotationsStr.isEmpty { annotationsStr += "\n" } + + var paramDecls = translatedSignature.parameters + .flatMap(\.kotlinParameters) + .map { $0.renderParameter() } + + TranslatedKotlinDocumentation.printDocumentation( + importedFunc: decl, + translatedDecl: translated, + in: &printer, + ) + printer.printBraceBlock( + """ + \(annotationsStr)fun \(methodName)(\(paramDecls.joined(separator: ", "))): \(returnTy) + """ + ) { printer in + printer.print("TODO(\"Not implemented\")") + } + } +} diff --git a/Sources/JExtractSwiftLib/Kotlin/Swift2KotlinGenerator+KotlinTranslation.swift b/Sources/JExtractSwiftLib/Kotlin/Swift2KotlinGenerator+KotlinTranslation.swift new file mode 100644 index 000000000..34b54b019 --- /dev/null +++ b/Sources/JExtractSwiftLib/Kotlin/Swift2KotlinGenerator+KotlinTranslation.swift @@ -0,0 +1,973 @@ +// +// Swift2KotlinGenerator+KotlinTranslation.swift +// swift-java +// +// Created by Tanish Azad on 30/03/26. +// + +import SwiftJavaJNICore +import SwiftJavaConfigurationShared + +extension Swift2KotlinGenerator { + func translatedDecl( + for decl: ImportedFunc + ) -> TranslatedFunctionDecl? { + if let cached = translatedDecls[decl] { + return cached + } + + let translated: TranslatedFunctionDecl? + do { + let translation = KotlinTranslation( + config: self.config, + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), + javaIdentifiers: self.currentJavaIdentifiers + ) + translated = try translation.translate(decl) + } catch { + self.log.info("Failed to translate: '\(decl.swiftDecl.qualifiedNameForDebug)'; \(error)") + translated = nil + } + + translatedDecls[decl] = translated + return translated + } + + /// Represent a Swift API parameter translated to Kotlin. + struct TranslatedParameter { + /// Kotlin parameter(s) mapped to the Swift parameter. + /// + /// Array because one Swift parameter can be mapped to multiple parameters. + var kotlinParameters: [KotlinParameter] + + /// Whether this parameter requires 32-bit integer overflow checking + var needs32BitIntOverflowCheck: OverflowCheckType = .none + } + + enum OverflowCheckType { + case none + case signedInt // Int: -2147483648 to 2147483647 + case unsignedInt // UInt: 0 to 4294967295 + } + + /// Represent a Swift API result translated to Java. + struct TranslatedResult { + /// Java type that represents the Swift result type. + var javaResultType: JavaType + + /// Java annotations that should be propagated from the result type onto the method + var annotations: [JavaAnnotation] = [] + + /// Required indirect return receivers for receiving the result. + /// + /// 'JavaParameter.name' is the suffix for the receiver variable names. For example + /// + /// var _result_pointer = MemorySegment.allocate(...) + /// var _result_count = MemorySegment.allocate(...) + /// downCall(_result_pointer, _result_count) + /// return constructResult(_result_pointer, _result_count) + /// + /// This case, there're two out parameter, named '_pointer' and '_count'. + var outParameters: [KotlinParameter] + + /// Similar to out parameters, but instead of parameters we "fill in" in native, + /// we create an upcall handle before the downcall and pass it to the downcall. + /// Swift then invokes the upcall in order to populate some data in Java (our callback). + /// + /// After the call is made, we may need to further extact the result from the called-back-into + /// Java function class, for example: + /// + /// var _result_initialize = new $result_initialize.Function(); + /// downCall($result_initialize.toUpcallHandle(_result_initialize, arena)) + /// return _result_initialize.result + /// + var outCallback: OutCallback? + + /// Whether this result requires 32-bit integer overflow checking + var needs32BitIntOverflowCheck: OverflowCheckType = .none + } + + /// Translated Java API representing a Swift API. + /// + /// Since this holds the lowered signature, and the original `SwiftFunctionSignature` + /// in it, this contains all the API information (except the name) to generate the + /// cdecl thunk, Java binding, and the Java wrapper function. + struct TranslatedFunctionDecl { + /// Java function name. + let name: String + + /// Functional interfaces required for the Java method. + let functionTypes: [TranslatedFunctionType] + + /// Function signature. + let translatedSignature: TranslatedFunctionSignature + + /// Cdecl lowered signature. + let loweredSignature: LoweredFunctionSignature + + /// Annotations to include on the Java function declaration + var annotations: [JavaAnnotation] { + self.translatedSignature.annotations + } + } + + /// Function signature for a Java API. + struct TranslatedFunctionSignature { + var selfParameter: TranslatedParameter? + var parameters: [TranslatedParameter] + var result: TranslatedResult + + // if the result type implied any annotations, + // propagate them onto the function the result is returned from + var annotations: [JavaAnnotation] { + self.result.annotations + } + } + + /// Represent a Swift closure type in the user facing Java API. + /// + /// Closures are translated to named functional interfaces in Java. + struct TranslatedFunctionType { + var name: String + var parameters: [TranslatedParameter] + var result: TranslatedResult + var swiftType: SwiftFunctionType + var cdeclType: SwiftFunctionType + } + + // ==== ------------------------------------------------------------------- + // MARK: Java translation + + struct KotlinTranslation { + let config: Configuration + var knownTypes: SwiftKnownTypes + var javaIdentifiers: JavaIdentifierFactory + + init(config: Configuration, knownTypes: SwiftKnownTypes, javaIdentifiers: JavaIdentifierFactory) { + self.config = config + self.knownTypes = knownTypes + self.javaIdentifiers = javaIdentifiers + } + + func translate(_ decl: ImportedFunc) throws -> TranslatedFunctionDecl { + let lowering = CdeclLowering(knownTypes: knownTypes) + let loweredSignature = try lowering.lowerFunctionSignature(decl.functionSignature) + + // Name. + let javaName = javaIdentifiers.makeJavaMethodName(decl) + + // Signature. + let translatedSignature = try translate(loweredFunctionSignature: loweredSignature, methodName: javaName) + + // Closures. + var funcTypes: [TranslatedFunctionType] = [] + for (idx, param) in decl.functionSignature.parameters.enumerated() { + switch param.type { + case .function(let funcTy): + let paramName = param.parameterName ?? "_\(idx)" + guard case .function(let cdeclTy) = loweredSignature.parameters[idx].cdeclParameters[0].type else { + preconditionFailure("closure parameter wasn't lowered to a function type; \(funcTy)") + } + let translatedClosure = try translateFunctionType(name: paramName, swiftType: funcTy, cdeclType: cdeclTy) + funcTypes.append(translatedClosure) + case .tuple: + // Tuple-typed closure parameters are not supported (same as JNI / lowering). + break + default: + break + } + } + + return TranslatedFunctionDecl( + name: javaName, + functionTypes: funcTypes, + translatedSignature: translatedSignature, + loweredSignature: loweredSignature + ) + } + + /// Translate Swift closure type to Java functional interface. + func translateFunctionType( + name: String, + swiftType: SwiftFunctionType, + cdeclType: SwiftFunctionType + ) throws -> TranslatedFunctionType { + var translatedParams: [TranslatedParameter] = [] + + for (i, param) in swiftType.parameters.enumerated() { + let paramName = param.parameterName ?? "_\(i)" + translatedParams.append( + try translateClosureParameter(param.type, convention: param.convention, parameterName: paramName) + ) + } + + guard let resultCType = try? CType(cdeclType: swiftType.resultType) else { + throw JavaTranslationError.unhandledType(.function(swiftType)) + } + + let transltedResult = TranslatedResult( + javaResultType: resultCType.javaType, + outParameters: [] + ) + + return TranslatedFunctionType( + name: name, + parameters: translatedParams, + result: transltedResult, + swiftType: swiftType, + cdeclType: cdeclType + ) + } + + func translateClosureParameter( + _ type: SwiftType, + convention: SwiftParameterConvention, + parameterName: String + ) throws -> TranslatedParameter { + if let cType = try? CType(cdeclType: type) { + return TranslatedParameter( + kotlinParameters: [ + KotlinParameter(name: parameterName, type: cType.javaType) + ] + ) + } + + switch type { + case .nominal(let nominal): + if let knownType = nominal.nominalTypeDecl.knownTypeKind { + switch knownType { + case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer: + return TranslatedParameter( + kotlinParameters: [ + KotlinParameter(name: parameterName, type: .javaForeignMemorySegment) + ] + ) + default: + break + } + } + default: + break + } + throw JavaTranslationError.unhandledType(type) + } + + /// Translate a Swift API signature to the user-facing Java API signature. + /// + /// Note that the result signature is for the high-level Java API, not the + /// low-level FFM down-calling interface. + func translate( + loweredFunctionSignature: LoweredFunctionSignature, + methodName: String + ) throws -> TranslatedFunctionSignature { + let swiftSignature = loweredFunctionSignature.original + + // 'self' + let selfParameter: TranslatedParameter? + if case .instance(let convention, let swiftType) = swiftSignature.selfParameter { + selfParameter = try self.translateParameter( + type: swiftType, + convention: convention, + parameterName: "self", + loweredParam: loweredFunctionSignature.selfParameter!, + methodName: methodName, + genericParameters: swiftSignature.genericParameters, + genericRequirements: swiftSignature.genericRequirements + ) + } else { + selfParameter = nil + } + + // Regular parameters. + let parameters: [TranslatedParameter] = try swiftSignature.parameters.enumerated() + .map { (idx, swiftParam) in + let loweredParam = loweredFunctionSignature.parameters[idx] + let parameterName = swiftParam.parameterName ?? "_\(idx)" + return try self.translateParameter( + type: swiftParam.type, + convention: swiftParam.convention, + parameterName: parameterName, + loweredParam: loweredParam, + methodName: methodName, + genericParameters: swiftSignature.genericParameters, + genericRequirements: swiftSignature.genericRequirements + ) + } + + // Result. + let result = try self.translateResult( + swiftResult: swiftSignature.result, + loweredResult: loweredFunctionSignature.result + ) + + return TranslatedFunctionSignature( + selfParameter: selfParameter, + parameters: parameters, + result: result + ) + } + + /// Translate a Swift API parameter to the user-facing Java API parameter. + func translateParameter( + type swiftType: SwiftType, + convention: SwiftParameterConvention, + parameterName: String, + loweredParam: LoweredParameter, + methodName: String, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] + ) throws -> TranslatedParameter { + // If the result type should cause any annotations on the method, include them here. + let parameterAnnotations: [JavaAnnotation] = getTypeAnnotations(swiftType: swiftType, config: config) + + // If there is a 1:1 mapping between this Swift type and a C type, that can + // be expressed as a Java primitive type. + if let cType = try? CType(cdeclType: swiftType) { + let javaType = cType.javaType + let overflowCheck: OverflowCheckType + if case .integral(.ptrdiff_t) = cType { + overflowCheck = .signedInt + } else if case .integral(.size_t) = cType { + overflowCheck = .unsignedInt + } else { + overflowCheck = .none + } + return TranslatedParameter( + kotlinParameters: [ + KotlinParameter( + name: parameterName, + type: javaType, + annotations: parameterAnnotations + ) + ], + needs32BitIntOverflowCheck: overflowCheck + ) + } + + switch swiftType { + case .metatype: + // Metatype are expressed as 'org.swift.swiftkit.SwiftAnyType' + return TranslatedParameter( + kotlinParameters: [ + KotlinParameter( + name: parameterName, + type: JavaType.class(package: "org.swift.swiftkit.ffm", name: "SwiftAnyType"), + annotations: parameterAnnotations + ) + ] + ) + + case .nominal(let swiftNominalType): + if let knownType = swiftNominalType.nominalTypeDecl.knownTypeKind { + if convention == .inout { + // FIXME: Support non-trivial 'inout' for builtin types. + throw JavaTranslationError.inoutNotSupported(swiftType) + } + switch knownType { + case .unsafePointer, .unsafeMutablePointer: + // FIXME: Implement + throw JavaTranslationError.unhandledType(swiftType) + case .unsafeBufferPointer, .unsafeMutableBufferPointer: + // FIXME: Implement + throw JavaTranslationError.unhandledType(swiftType) + + case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer: + return TranslatedParameter( + kotlinParameters: [ + KotlinParameter(name: parameterName, type: .javaForeignMemorySegment) + ] + ) + + case .optional: + guard let genericArgs = swiftNominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.unhandledType(swiftType) + } + return try translateOptionalParameter( + wrappedType: genericArgs[0], + convention: convention, + parameterName: parameterName, + loweredParam: loweredParam, + methodName: methodName, + genericParameters: genericParameters, + genericRequirements: genericRequirements + ) + + case .string: + return TranslatedParameter( + kotlinParameters: [ + KotlinParameter( + name: parameterName, + type: .javaLangString + ) + ] + ) + + case .foundationData, .essentialsData: + break + + default: + throw JavaTranslationError.unhandledType(swiftType) + } + } + + // Generic types are not supported yet. + guard swiftNominalType.genericArguments == nil else { + throw JavaTranslationError.unhandledType(swiftType) + } + + return TranslatedParameter( + kotlinParameters: [ + KotlinParameter( + name: parameterName, + type: try translate(swiftType: swiftType) + ) + ] + ) + + case .tuple([]): + return TranslatedParameter( + kotlinParameters: [ + KotlinParameter( + name: parameterName, + type: .void, + annotations: parameterAnnotations + ) + ] + ) + + case .tuple(let elements): + return try translateTupleParameter( + elements: elements, + convention: convention, + parameterName: parameterName, + methodName: methodName, + genericParameters: genericParameters, + genericRequirements: genericRequirements + ) + + case .function: + return TranslatedParameter( + kotlinParameters: [ + KotlinParameter( + name: parameterName, + type: JavaType.class(package: nil, name: "\(methodName).\(parameterName)") + ) + ] + ) + + case .existential, .opaque, .genericParameter: + if let concreteTy = swiftType.representativeConcreteTypeIn( + knownTypes: knownTypes, + genericParameters: genericParameters, + genericRequirements: genericRequirements + ) { + return try translateParameter( + type: concreteTy, + convention: convention, + parameterName: parameterName, + loweredParam: loweredParam, + methodName: methodName, + genericParameters: genericParameters, + genericRequirements: genericRequirements + ) + } + + // Otherwise, not supported yet. + throw JavaTranslationError.unhandledType(swiftType) + + case .optional(let wrapped): + return try translateOptionalParameter( + wrappedType: wrapped, + convention: convention, + parameterName: parameterName, + loweredParam: loweredParam, + methodName: methodName, + genericParameters: genericParameters, + genericRequirements: genericRequirements + ) + + case .composite: + throw JavaTranslationError.unhandledType(swiftType) + + case .array(let wrapped) where wrapped == knownTypes.uint8: + return TranslatedParameter( + kotlinParameters: [ + KotlinParameter(name: parameterName, type: .array(.byte), annotations: parameterAnnotations) + ] + ) + + case .array: + throw JavaTranslationError.unhandledType(swiftType) + + case .dictionary: + throw JavaTranslationError.unhandledType(swiftType) + + case .set: + throw JavaTranslationError.unhandledType(swiftType) + } + } + + /// Tuple parameters: one `TupleN<…>` on the Java API; conversion reads `.$0`, `.$1`, … (mirrors JNI). + func translateTupleParameter( + elements: [SwiftTupleElement], + convention: SwiftParameterConvention, + parameterName: String, + methodName: String, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] + ) throws -> TranslatedParameter { + let lowering = CdeclLowering(knownTypes: knownTypes) + var elementJavaTypes: [JavaType] = [] + + for (idx, element) in elements.enumerated() { + let subLowered = try lowering.lowerParameter( + element.type, + convention: convention, + parameterName: "\(parameterName)_\(idx)", + genericParameters: genericParameters, + genericRequirements: genericRequirements + ) + let elementTranslated = try translateParameter( + type: element.type, + convention: convention, + parameterName: "\(parameterName)_\(idx)", + loweredParam: subLowered, + methodName: methodName, + genericParameters: genericParameters, + genericRequirements: genericRequirements + ) + guard elementTranslated.kotlinParameters.count == 1 else { + throw JavaTranslationError.unhandledType(element.type) + } + elementJavaTypes.append(elementTranslated.kotlinParameters[0].type.javaType) + } + + let javaType: JavaType = .tuple(elementTypes: elementJavaTypes) + return TranslatedParameter( + kotlinParameters: [ + KotlinParameter(name: parameterName, type: javaType) + ] + ) + } + + /// Translate an Optional Swift API parameter to the user-facing Java API parameter. + func translateOptionalParameter( + wrappedType swiftType: SwiftType, + convention: SwiftParameterConvention, + parameterName: String, + loweredParam: LoweredParameter, + methodName: String, + genericParameters: [SwiftGenericParameterDeclaration], + genericRequirements: [SwiftGenericRequirement] + ) throws -> TranslatedParameter { + // If there is a 1:1 mapping between this Swift type and a C type, that can + // be expressed as a Java primitive type. + if let cType = try? CType(cdeclType: swiftType) { + let (translatedClass, lowerFunc) = + switch cType.javaType { + case .int: ("Int?", "toOptionalSegmentInt") + case .long: ("Long?", "toOptionalSegmentLong") + case .double: ("Double?", "toOptionalSegmentDouble") + case .boolean: ("Boolean?", "toOptionalSegmentBoolean") + case .byte: ("Byte?", "toOptionalSegmentByte") + case .char: ("Char?", "toOptionalSegmentCharacter") + case .short: ("Short?", "toOptionalSegmentShort") + case .float: ("Float?", "toOptionalSegmentFloat") + default: + throw JavaTranslationError.unhandledType(.optional(swiftType)) + } + return TranslatedParameter( + kotlinParameters: [ + KotlinParameter(name: parameterName, type: JavaType(className: translatedClass)) + ] + ) + } + + switch swiftType { + case .nominal(let nominal): + if let knownType = nominal.nominalTypeDecl.knownTypeKind { + switch knownType { + case .foundationData, .foundationDataProtocol: + break + case .essentialsData, .essentialsDataProtocol: + break + default: + throw JavaTranslationError.unhandledType(.optional(swiftType)) + } + } + + let translatedTy = try self.translate(swiftType: swiftType) + return TranslatedParameter( + kotlinParameters: [ + KotlinParameter(name: parameterName, type: JavaType(className: "\(translatedTy.description)?")) + ] + ) + case .existential, .opaque, .genericParameter: + if let concreteTy = swiftType.representativeConcreteTypeIn( + knownTypes: knownTypes, + genericParameters: genericParameters, + genericRequirements: genericRequirements + ) { + return try translateOptionalParameter( + wrappedType: concreteTy, + convention: convention, + parameterName: parameterName, + loweredParam: loweredParam, + methodName: methodName, + genericParameters: genericParameters, + genericRequirements: genericRequirements + ) + } + throw JavaTranslationError.unhandledType(.optional(swiftType)) + case .tuple(let tuple): + if tuple.count == 1 { + return try translateOptionalParameter( + wrappedType: tuple[0].type, + convention: convention, + parameterName: parameterName, + loweredParam: loweredParam, + methodName: methodName, + genericParameters: genericParameters, + genericRequirements: genericRequirements + ) + } + throw JavaTranslationError.unhandledType(.optional(swiftType)) + default: + throw JavaTranslationError.unhandledType(.optional(swiftType)) + } + } + + /// Translate a Swift API result to the user-facing Java API result. + func translateResult( + swiftResult: SwiftResult, + loweredResult: LoweredResult + ) throws -> TranslatedResult { + let swiftType = swiftResult.type + // If the result type should cause any annotations on the method, include them here. + let resultAnnotations: [JavaAnnotation] = getTypeAnnotations(swiftType: swiftType, config: config) + + // If there is a 1:1 mapping between this Swift type and a C type, that can + // be expressed as a Java primitive type. + if let cType = try? CType(cdeclType: swiftType) { + let javaType = cType.javaType + let overflowCheck: OverflowCheckType + if case .integral(.ptrdiff_t) = cType { + overflowCheck = .signedInt + } else if case .integral(.size_t) = cType { + overflowCheck = .unsignedInt + } else { + overflowCheck = .none + } + return TranslatedResult( + javaResultType: javaType, + annotations: resultAnnotations, + outParameters: [], + needs32BitIntOverflowCheck: overflowCheck + ) + } + + switch swiftType { + case .metatype(_): + // Metatype are expressed as 'org.swift.swiftkit.SwiftAnyType' + let javaType = JavaType.class(package: "org.swift.swiftkit.ffm", name: "SwiftAnyType") + return TranslatedResult( + javaResultType: javaType, + annotations: resultAnnotations, + outParameters: [] + ) + + case .nominal(let swiftNominalType): + if let knownType = swiftNominalType.nominalTypeDecl.knownTypeKind { + switch knownType { + case .unsafeRawBufferPointer, .unsafeMutableRawBufferPointer: + return TranslatedResult( + javaResultType: .javaForeignMemorySegment, + annotations: resultAnnotations, + outParameters: [ + KotlinParameter(name: "pointer", type: .javaForeignMemorySegment), + KotlinParameter(name: "count", type: .long), + ] + ) + + case .foundationData, .essentialsData: + break // Implemented as wrapper + + case .unsafePointer, .unsafeMutablePointer: + // FIXME: Implement + throw JavaTranslationError.unhandledType(swiftType) + case .unsafeBufferPointer, .unsafeMutableBufferPointer: + // FIXME: Implement + throw JavaTranslationError.unhandledType(swiftType) + case .string: + // FIXME: Implement + throw JavaTranslationError.unhandledType(swiftType) + default: + throw JavaTranslationError.unhandledType(swiftType) + } + } + + // Generic types are not supported yet. + guard swiftNominalType.genericArguments == nil else { + throw JavaTranslationError.unhandledType(swiftType) + } + + let javaType: JavaType = .class(package: nil, name: swiftNominalType.nominalTypeDecl.qualifiedName) + return TranslatedResult( + javaResultType: javaType, + annotations: resultAnnotations, + outParameters: [ + KotlinParameter(name: "", type: javaType) + ] + ) + + case .tuple([]): + return TranslatedResult( + javaResultType: .void, + annotations: resultAnnotations, + outParameters: [] + ) + + case .tuple(let elements): + return try translateTupleResult( + elements: elements, + resultAnnotations: resultAnnotations + ) + + case .array(let wrapped) where wrapped == knownTypes.uint8: + return TranslatedResult( + javaResultType: + .array(.byte), + annotations: [.unsigned], + outParameters: [], // no out parameters, but we do an "out" callback + outCallback: OutCallback( + name: "$_result_initialize", + members: [ + "byte[] result = null" + ], + parameters: [ + JavaParameter(name: "pointer", type: .javaForeignMemorySegment), + JavaParameter(name: "count", type: .long), + ], + cFunc: CFunction( + resultType: .void, + name: "apply", + parameters: [ + CParameter(type: .pointer(.void)), + CParameter(type: .integral(.size_t)), + ], + isVariadic: false + ), + body: + "this.result = _0.reinterpret(_1).toArray(ValueLayout.JAVA_BYTE); // copy native Swift array to Java heap array" + ) + ) + + case .genericParameter, .optional, .function, .existential, .opaque, .composite, .array, .dictionary, .set: + throw JavaTranslationError.unhandledType(swiftType) + } + + } + + /// Tuple results: indirect `MemorySegment` per element, then `new TupleN<…>(…)` (mirrors JNI out-arrays). + func translateTupleResult( + elements: [SwiftTupleElement], + resultAnnotations: [JavaAnnotation] + ) throws -> TranslatedResult { + var outParameters: [KotlinParameter] = [] + var tupleElements: [(outParamName: String, elementConversion: JavaConversionStep)] = [] + var elementJavaTypes: [JavaType] = [] + + for (idx, element) in elements.enumerated() { + let (javaType, elementConversion) = try translateTupleElementResult(type: element.type) + outParameters.append(KotlinParameter(name: "\(idx)", type: javaType)) + tupleElements.append((outParamName: "_result_\(idx)", elementConversion: elementConversion)) + elementJavaTypes.append(javaType) + } + + let javaResultType: JavaType = .tuple(elementTypes: elementJavaTypes) + let fullTupleClassName = javaResultType.fullyQualifiedClassName! + + return TranslatedResult( + javaResultType: javaResultType, + annotations: resultAnnotations, + outParameters: outParameters + ) + } + + /// Single tuple element for the Java result (mirrors JNI `translateTupleElementResult`). + private func translateTupleElementResult(type: SwiftType) throws -> (JavaType, JavaConversionStep) { + switch type { + case .nominal(let nominalType): + if nominalType.nominalTypeDecl.knownTypeKind != nil { + if let cType = try? CType(cdeclType: type) { + return (cType.javaType, .readMemorySegment(.placeholder, as: cType.javaType)) + } + throw JavaTranslationError.unhandledType(type) + } + + guard !nominalType.isSwiftJavaWrapper else { + throw JavaTranslationError.unhandledType(type) + } + + let javaType: JavaType = .class(package: nil, name: nominalType.nominalTypeDecl.qualifiedName) + return (javaType, .wrapMemoryAddressUnsafe(.placeholder, javaType)) + + default: + throw JavaTranslationError.unhandledType(type) + } + } + + func translate( + swiftType: SwiftType + ) throws -> JavaType { + guard let nominalName = swiftType.asNominalTypeDeclaration?.name else { + throw JavaTranslationError.unhandledType(swiftType) + } + return .class(package: nil, name: nominalName) + } + } + + /// Describes how to convert values between Java types and FFM types. + enum JavaConversionStep { + /// The input + case placeholder + + /// The "downcall", e.g. `swiftjava_SwiftModule_returnArray.call(...)`. + /// This can be used in combination with aggregate conversion steps to prepare a setup and processing of the downcall. + case placeholderForDowncall + + /// Placeholder for Swift thunk name, e.g. "swiftjava_SwiftModule_returnArray". + /// + /// This is derived from the placeholderForDowncall substitution - could be done more cleanly, + /// however this has the benefit of not needing to pass the name substituion separately. + case placeholderForSwiftThunkName + + /// The temporary `arena$` that is necessary to complete the conversion steps. + /// + /// This is distinct from just a constant 'arena$' string, since it forces the creation of a temporary arena. + case temporaryArena + + /// The input exploded into components. + case explodedName(component: String) + + /// A fixed value + case constant(String) + + /// The result of the function will be initialized with a callback to Java (an upcall). + /// + /// The `extractResult` is used for the actual `return ...` statement, because we need to extract + /// the return value from the called back into class, e.g. `return _result_initialize.result`. + indirect case initializeResultWithUpcall([JavaConversionStep], extractResult: JavaConversionStep) + + /// 'value.$memorySegment()' + indirect case swiftValueSelfSegment(JavaConversionStep) + + /// Call specified function using the placeholder as arguments. + /// + /// The 'base' is if the call should be performed as 'base.function', + /// otherwise the function is assumed to be a free function. + /// + /// If `withArena` is true, `arena$` argument is added. + indirect case call(JavaConversionStep, base: JavaConversionStep?, function: String, withArena: Bool) + + static func call(_ step: JavaConversionStep, function: String, withArena: Bool) -> Self { + .call(step, base: nil, function: function, withArena: withArena) + } + + // TODO: just use make call more powerful and use it instead? + /// Apply a method on the placeholder. + /// If `withArena` is true, `arena$` argument is added. + indirect case method(JavaConversionStep, methodName: String, arguments: [JavaConversionStep] = [], withArena: Bool) + + /// Fetch a property from the placeholder. + /// Similar to 'method', however for a property i.e. without adding the '()' after the name + indirect case property(JavaConversionStep, propertyName: String) + + /// Call 'new \(Type)(\(placeholder), swiftArena)'. + indirect case constructSwiftValue(JavaConversionStep, JavaType) + + /// Construct the type using the placeholder as arguments. + indirect case construct(JavaConversionStep, JavaType) + + /// Call the `MyType.wrapMemoryAddressUnsafe` in order to wrap a memory address using the Java binding type + indirect case wrapMemoryAddressUnsafe(JavaConversionStep, JavaType) + + /// Introduce a local variable, e.g. `var result = new Something()` + indirect case introduceVariable(name: String, initializeWith: JavaConversionStep) + + /// Casting the placeholder to the certain type. + indirect case cast(JavaConversionStep, JavaType) + + /// Prefix the conversion step with a java `new`. + /// + /// This is useful if constructing the value is complex and we use + /// a combination of separated values and constants to do so; Generally prefer using `construct` + /// if you only want to construct a "wrapper" for the current `.placeholder`. + indirect case javaNew(JavaConversionStep) + + /// Convert the results of the inner steps to a comma separated list. + indirect case commaSeparated([JavaConversionStep], separator: String = ", ") + + /// Refer an exploded argument suffixed with `_\(name)`. + indirect case readMemorySegment(JavaConversionStep, as: JavaType) + + /// Use `placeholder` as the root when rendering `inner` (same idea as JNI `replacingPlaceholder`). + indirect case replacingPlaceholder(JavaConversionStep, placeholder: String) + + /// Build `org.swift.swiftkit.core.tuple.TupleN` from indirect `MemorySegment` out params (JNI `tupleFromOutParams`). + case tupleFromOutParams( + tupleClassName: String, + elements: [(outParamName: String, elementConversion: JavaConversionStep)] + ) + + var isPlaceholder: Bool { + if case .placeholder = self { true } else { false } + } + } +} + +extension CType { + /// Map lowered C type to Java type for FFM binding. + var kotlinType: JavaType { + switch self { + case .void: return .void + + case .integral(.bool): return .boolean + case .integral(.signed(bits: 8)): return .byte + case .integral(.signed(bits: 16)): return .short + case .integral(.signed(bits: 32)): return .int + case .integral(.signed(bits: 64)): return .long + case .integral(.unsigned(bits: 8)): return .byte + case .integral(.unsigned(bits: 16)): return .char // char is Java's only unsigned primitive, we can use it! + case .integral(.unsigned(bits: 32)): return .int + case .integral(.unsigned(bits: 64)): return .long + + case .floating(.float): return .float + case .floating(.double): return .double + + // FIXME: 32 bit consideration. + // The 'FunctionDescriptor' uses 'SWIFT_INT' which relies on the running + // machine arch. That means users can't pass Java 'long' values to the + // function without casting. But how do we generate code that runs both + // 32 and 64 bit machine? + case .integral(.ptrdiff_t), .integral(.size_t): + return .long + + case .pointer(_), .function(resultType: _, parameters: _, variadic: _): + return .javaForeignMemorySegment + + case .qualified(const: _, volatile: _, let inner): + return inner.javaType + + case .tag(_): + fatalError("unsupported") + case .integral(.signed(bits: _)), .integral(.unsigned(bits: _)): + fatalError("unreachable") + } + } +} + +enum KotlinTranslationError: Error { + case inoutNotSupported(SwiftType, file: String = #file, line: Int = #line) + case unhandledType(SwiftType, file: String = #file, line: Int = #line) +} diff --git a/Sources/JExtractSwiftLib/Kotlin/Swift2KotlinGenerator.swift b/Sources/JExtractSwiftLib/Kotlin/Swift2KotlinGenerator.swift new file mode 100644 index 000000000..888112266 --- /dev/null +++ b/Sources/JExtractSwiftLib/Kotlin/Swift2KotlinGenerator.swift @@ -0,0 +1,238 @@ +// +// Swift2KotlinGenerator.swift +// swift-java +// +// Created by Tanish Azad on 31/03/26. +// + +import CodePrinting +import SwiftJavaConfigurationShared +import SwiftJavaJNICore +import SwiftSyntax +import SwiftSyntaxBuilder + +import struct Foundation.URL + +package class Swift2KotlinGenerator: Swift2JavaGenerator { + let log: Logger + let config: Configuration + let analysis: AnalysisResult + let swiftModuleName: String + let kotlinPackage: String + let swiftOutputDirectory: String + let kotlinOutputDirectory: String + let lookupContext: SwiftTypeLookupContext + + var kotlinPackagePath: String { + kotlinPackage.replacingOccurrences(of: ".", with: "/") + } + + var thunkNameRegistry: ThunkNameRegistry = ThunkNameRegistry() + + /// Cached Java translation result. 'nil' indicates failed translation. + var translatedDecls: [ImportedFunc: TranslatedFunctionDecl?] = [:] + + /// Duplicate identifier tracking for the current batch of methods being generated. + var currentJavaIdentifiers: JavaIdentifierFactory = JavaIdentifierFactory() + + /// Because we need to write empty files for SwiftPM, keep track which files we didn't write yet, + /// and write an empty file for those. + /// + /// Since Swift files in SwiftPM builds needs to be unique, we use this fact to flatten paths into plain names here. + /// For uniqueness checking "did we write this file already", just checking the name should be sufficient. + var expectedOutputSwiftFileNames: Set + + package init( + config: Configuration, + translator: Swift2JavaTranslator, + kotlinPackage: String, + swiftOutputDirectory: String, + kotlinOutputDirectory: String + ) { + self.log = Logger(label: "kotlin-generator", logLevel: translator.log.logLevel) + self.config = config + self.analysis = translator.result + self.swiftModuleName = translator.swiftModuleName + self.kotlinPackage = kotlinPackage + self.swiftOutputDirectory = swiftOutputDirectory + self.kotlinOutputDirectory = kotlinOutputDirectory + self.lookupContext = translator.lookupContext + + // If we are forced to write empty files, construct the expected outputs. + // It is sufficient to use file names only, since SwiftPM requires names to be unique within a module anyway. + if translator.config.writeEmptyFiles ?? false { + self.expectedOutputSwiftFileNames = Set( + translator.inputs.compactMap { (input) -> String? in + guard let fileName = input.path.split(separator: PATH_SEPARATOR).last else { + return nil + } + guard fileName.hasSuffix(".swift") else { + return nil + } + return String(fileName.replacing(".swift", with: "+SwiftKotlin.swift")) + } + ) + self.expectedOutputSwiftFileNames.insert("\(translator.swiftModuleName)Module+SwiftKotlin.swift") + self.expectedOutputSwiftFileNames.insert("Foundation+SwiftKotlin.swift") + } else { + self.expectedOutputSwiftFileNames = [] + } + } + + func generate() throws { + // try writeSwiftThunkSources() + // log.info("Generated Swift sources (module: '\(self.swiftModuleName)') in: \(swiftOutputDirectory)/") + + try writeExportedJavaSources() + log.info("Generated Kotlin sources (package: '\(kotlinPackage)') in: \(kotlinOutputDirectory)/") + + // try writeSwiftExpectedEmptySources() + } +} + +extension Swift2KotlinGenerator { + package func writeExportedJavaSources() throws { + var printer = CodePrinter() + try writeExportedJavaSources(printer: &printer) + } + + /// Every imported public type becomes a public class in its own file in Java. + package func writeExportedJavaSources(printer: inout CodePrinter) throws { + for (_, ty) in analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) { + let filename = "\(ty.swiftNominal.name).kt" + log.debug("Printing contents: \(filename)") + printImportedNominal(&printer, ty) + + if let outputFile = try printer.writeContents( + outputDirectory: kotlinOutputDirectory, + javaPackagePath: kotlinPackagePath, + filename: filename + ) { + log.info("Generated: \((ty.swiftNominal.name.bold + ".kt").bold) (at \(outputFile.absoluteString))") + } + } + + do { + let filename = "\(self.swiftModuleName).kt" + log.debug("Printing contents: \(filename)") + printModule(&printer) + + if let outputFile = try printer.writeContents( + outputDirectory: kotlinOutputDirectory, + javaPackagePath: kotlinPackagePath, + filename: filename + ) { + log.info("Generated: \((self.swiftModuleName + ".kt").bold) (at \(outputFile.absoluteString))") + } + } + } +} + +// ==== --------------------------------------------------------------------------------------------------------------- +// MARK: Kotlin/text printing + +extension Swift2KotlinGenerator { + /// Render the Java file contents for an imported Swift module. + /// + /// This includes any Swift global functions in that module, and some general type information and helpers. + func printModule(_ printer: inout CodePrinter) { + printHeader(&printer) + printPackage(&printer) + +// self.currentJavaIdentifiers = JavaIdentifierFactory( +// self.analysis.importedGlobalFuncs + self.analysis.importedGlobalVariables +// ) +// +// printModuleClass(&printer) { printer in +// for decl in analysis.importedGlobalVariables { +// self.log.trace("Print imported decl: \(decl)") +// printKotlinBindingPlaceholder(&printer, decl) +// } +// +// for decl in analysis.importedGlobalFuncs { +// self.log.trace("Print imported decl: \(decl)") +// printKotlinBindingPlaceholder(&printer, decl) +// } +// } + } + + func printImportedNominal(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + printHeader(&printer) + printPackage(&printer) + +// self.currentJavaIdentifiers = JavaIdentifierFactory( +// decl.initializers + decl.variables + decl.methods +// ) +// +// printNominal(&printer, decl) { printer in +// // Initializers +// for initDecl in decl.initializers { +// printKotlinBindingPlaceholder(&printer, initDecl) +// } +// +// // Properties +// for accessorDecl in decl.variables { +// printKotlinBindingPlaceholder(&printer, accessorDecl) +// } +// +// // Methods +// for funcDecl in decl.methods { +// printKotlinBindingPlaceholder(&printer, funcDecl) +// } +// +// // Helper methods and default implementations +// printToStringMethod(&printer, decl) +// } + } + + func printHeader(_ printer: inout CodePrinter) { + printer.print( + """ + // Generated by jextract-swift + // Swift module: \(swiftModuleName) + + """ + ) + } + + func printPackage(_ printer: inout CodePrinter) { + printer.print( + """ + package \(kotlinPackage) + + """ + ) + } + + func printNominal( + _ printer: inout CodePrinter, + _ decl: ImportedNominalType, + body: (inout CodePrinter) -> Void + ) { + printer.printBraceBlock( + "class \(decl.swiftNominal.name) private constructor()" + ) { printer in + body(&printer) + } + } + + func printModuleClass(_ printer: inout CodePrinter, body: (inout CodePrinter) -> Void) { + printer.printBraceBlock("class \(swiftModuleName) private constructor()") { printer in + body(&printer) + } + } + + func printToStringMethod( + _ printer: inout CodePrinter, + _ decl: ImportedNominalType + ) { + printer.print( + """ + override fun toString(): String { + TODO("Not implemented") + } + """ + ) + } + +} diff --git a/Sources/JExtractSwiftLib/Kotlin/TranslatedKotlinDocumentation.swift b/Sources/JExtractSwiftLib/Kotlin/TranslatedKotlinDocumentation.swift new file mode 100644 index 000000000..f8834e8f1 --- /dev/null +++ b/Sources/JExtractSwiftLib/Kotlin/TranslatedKotlinDocumentation.swift @@ -0,0 +1,94 @@ +// +// TranslatedDocumnetation.swift +// swift-java +// +// Created by Tanish Azad on 31/03/26. +// + +import CodePrinting +import SwiftSyntax + +enum TranslatedKotlinDocumentation { + static func printDocumentation( + importedFunc: ImportedFunc, + translatedDecl: Swift2KotlinGenerator.TranslatedFunctionDecl, + in printer: inout CodePrinter + ) { + var documentation = SwiftDocumentationParser.parse(importedFunc.swiftDecl) + + printDocumentation(documentation, syntax: importedFunc.swiftDecl, in: &printer) + } + + static func printDocumentation( + importedFunc: ImportedFunc, + translatedDecl: JNISwift2JavaGenerator.TranslatedFunctionDecl, + in printer: inout CodePrinter + ) { + var documentation = SwiftDocumentationParser.parse(importedFunc.swiftDecl) + + if translatedDecl.translatedFunctionSignature.requiresSwiftArena { + documentation?.parameters.append( + SwiftDocumentation.Parameter( + name: "swiftArena", + description: "the arena that the the returned object will be attached to" + ) + ) + } + + printDocumentation(documentation, syntax: importedFunc.swiftDecl, in: &printer) + } + + private static func printDocumentation( + _ parsedDocumentation: SwiftDocumentation?, + syntax: some DeclSyntaxProtocol, + in printer: inout CodePrinter + ) { + var groups = [String]() + if let summary = parsedDocumentation?.summary { + groups.append("\(summary)") + } + + if let discussion = parsedDocumentation?.discussion { + let paragraphs = discussion.split(separator: "\n\n") + for paragraph in paragraphs { + groups.append("

\(paragraph)") + } + } + + groups.append( + """ + \(parsedDocumentation != nil ? "

" : "")Downcall to Swift: + {@snippet lang=swift : + \(syntax.signatureString) + } + """ + ) + + var annotationsGroup = [String]() + + for param in parsedDocumentation?.parameters ?? [] { + annotationsGroup.append("@param \(param.name) \(param.description)") + } + + if let returns = parsedDocumentation?.returns { + annotationsGroup.append("@return \(returns)") + } + + if !annotationsGroup.isEmpty { + groups.append(annotationsGroup.joined(separator: "\n")) + } + + printer.print("/**") + let oldIdentationText = printer.indentationText + printer.indentationText += " * " + for (idx, group) in groups.enumerated() { + printer.print(group) + if idx < groups.count - 1 { + printer.print("") + } + } + printer.indentationText = oldIdentationText + printer.print(" */") + + } +} diff --git a/Sources/JExtractSwiftLib/Swift2Java.swift b/Sources/JExtractSwiftLib/Swift2Java.swift index 4a75f218e..fb9ca5fe6 100644 --- a/Sources/JExtractSwiftLib/Swift2Java.swift +++ b/Sources/JExtractSwiftLib/Swift2Java.swift @@ -86,6 +86,16 @@ public struct SwiftToJava { try translator.analyze() switch config.effectiveMode { + + case .kotlin: + try Swift2KotlinGenerator( + config: self.config, + translator: translator, + kotlinPackage: config.javaPackage ?? "", + swiftOutputDirectory: outputSwiftDirectory, + kotlinOutputDirectory: outputJavaDirectory + ).generate() + case .ffm: let generator = FFMSwift2JavaGenerator( config: self.config, diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift index 8e11d82b0..5546f8613 100644 --- a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift +++ b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift @@ -19,6 +19,9 @@ public enum JExtractGenerationMode: String, Sendable, Codable { /// Java Native Interface case jni + + /// Kotlin stub generator + case kotlin public static var `default`: JExtractGenerationMode { .ffm diff --git a/settings.gradle.kts b/settings.gradle.kts index a360e0786..c7e137d4f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,3 +31,5 @@ if (!(settings.providers.gradleProperty("skipSamples").orNull.toBoolean())) { } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +include("Samples:SwiftJavaExtractKotlinSampleApp") \ No newline at end of file