diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 000000000..9c77928a0 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,41 @@ +# Kotlin JVM Code Generation + +## What Was Built + +A new `--mode kotlin-jvm` option for the `jextract` subcommand that generates Kotlin source files from Swift modules. + +**Input Swift:** +```swift +public func add(a: Int, b: Int) -> Int +public func greet(name: String) -> String +``` + +**Output Kotlin:** +```kotlin +// Generated by swift-java +// Swift module: MyModule + +object MyModule { + fun add(a: Int, b: Int): Int = TODO("Not implemented") + fun greet(name: String): String = TODO("Not implemented") +} +``` + +## Corners Cut + +- **Stub bodies only** — no real JNI bridging or Swift thunk generation. The task asked for stubs, so this was intentional. +- **Simplified type mapping** — only the required subset: `Int`, `Int32`, `Bool`, `Double`, `String`, `Void`. Optionals, arrays, generics are not handled. +- **No package support** — no `package` declaration is emitted. Would be easy to add via `--java-package`. + +## What an Ideal Solution Would Look Like + +- Generate real `external fun` declarations with matching Swift `@_cdecl` thunks (like the JNI generator does) +- Full type mapping including optionals, arrays, closures +- Proper `package` declaration and Kotlin-idiomatic output (`@JvmStatic`, nullable types) +- A sample Gradle project demonstrating end-to-end usage + +## What I Would Do Next + +1. Wire up JNI bridging to make the stubs actually callable +2. Expand type coverage (optionals, arrays, simple structs) +3. Add a dedicated `--output-kotlin` CLI flag diff --git a/HOW_TO_RUN.md b/HOW_TO_RUN.md new file mode 100644 index 000000000..2942c0914 --- /dev/null +++ b/HOW_TO_RUN.md @@ -0,0 +1,86 @@ +# How to Run — Kotlin JVM Code Generation + +## Prerequisites + +- Swift 6.2+ +- macOS 13+ or Linux + +## Build the Project + +```bash +git clone https://github.com/Garnik645/swift-java.git +cd swift-java +swift build +``` + +## Prepare Your Swift Module + +The tool requires a compiled Swift module interface. Given a Swift source file: + +```swift +// MyModule.swift +public func add(a: Int, b: Int) -> Int { + return a + b +} + +public func greet(name: String) -> String { + return "Hello, \(name)!" +} + +public func isEven(n: Int) -> Bool { + return n % 2 == 0 +} +``` + +Compile it into a module interface: + +```bash +swiftc MyModule.swift \ + -module-name MyModule \ + -emit-module-interface \ + -enable-library-evolution \ + -emit-module-path /tmp/MyModule.swiftmodule \ + -emit-library \ + -o /tmp/libMyModule.dylib +``` + +## Generate Kotlin Sources + +```bash +swift run swift-java jextract \ + --input-swift /tmp \ + --swift-module MyModule \ + --output-swift /tmp/out-swift \ + --output-java /tmp/out-kotlin \ + --mode kotlin-jvm +``` + +## View the Output + +```bash +cat /tmp/out-kotlin/MyModule.kt +``` + +Expected output: + +```kotlin +// Generated by swift-java +// Swift module: MyModule + +object MyModule { + fun add(a: Int, b: Int): Int = TODO("Not implemented") + fun greet(name: String): String = TODO("Not implemented") + fun isEven(n: Int): Boolean = TODO("Not implemented") +} +``` + +## Type Mappings + +| Swift | Kotlin | +|-------|--------| +| `Int` | `Int` | +| `Int32` | `Int` | +| `Bool` | `Boolean` | +| `Double` | `Double` | +| `String` | `String` | +| `Void` | `Unit` | diff --git a/Sources/JExtractSwiftLib/KotlinSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/KotlinSwift2JavaGenerator.swift new file mode 100644 index 000000000..c0c6e1c0b --- /dev/null +++ b/Sources/JExtractSwiftLib/KotlinSwift2JavaGenerator.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// KotlinSwift2JavaGenerator.swift +// Generates Kotlin JVM stubs from imported Swift declarations. +//===----------------------------------------------------------------------===// + +import SwiftJavaConfigurationShared +import struct Foundation.URL +import Foundation + +package class KotlinSwift2JavaGenerator: Swift2JavaGenerator { + let config: Configuration + let analysis: AnalysisResult + let swiftModuleName: String + let kotlinOutputDirectory: String + + package init( + config: Configuration, + translator: Swift2JavaTranslator, + kotlinOutputDirectory: String + ) { + self.config = config + self.analysis = translator.result + self.swiftModuleName = translator.swiftModuleName + self.kotlinOutputDirectory = kotlinOutputDirectory + } + + package func generate() throws { + var printer = CodePrinter() + printModule(&printer) + + let outputDir = URL(fileURLWithPath: kotlinOutputDirectory) + try FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) + + let outputFile = outputDir.appendingPathComponent("\(swiftModuleName).kt") + try printer.contents.write(to: outputFile, atomically: true, encoding: .utf8) + + print("Generated Kotlin: \(outputFile.path)") + } + + func printModule(_ printer: inout CodePrinter) { + // Header + printer.print( + """ + // Generated by swift-java + // Swift module: \(swiftModuleName) + + """ + ) + + // Module object + printer.printBraceBlock("object \(swiftModuleName)") { printer in + for decl in analysis.importedGlobalFuncs { + printFunction(&printer, decl) + } + } + } + + func printFunction(_ printer: inout CodePrinter, _ decl: ImportedFunc) { + let params = decl.functionSignature.parameters + .enumerated() + .map { (i, param) in + let name = param.parameterName ?? "_\(i)" + let type = mapSwiftTypeToKotlin(param.type) + return "\(name): \(type)" + } + .joined(separator: ", ") + + let returnType = mapSwiftTypeToKotlin(decl.functionSignature.result.type) + let returnClause = returnType == "Unit" ? "" : ": \(returnType)" + + printer.print("fun \(decl.name)(\(params))\(returnClause) = TODO(\"Not implemented\")") + } + + func mapSwiftTypeToKotlin(_ type: SwiftType) -> String { + // Get the type name as a string and map it + let typeName = "\(type)" + + switch typeName { + case "Int": return "Int" + case "Int64": return "Long" + case "Int32": return "Int" + case "Bool": return "Boolean" + case "Double": return "Double" + case "Float": return "Float" + case "String": return "String" + case "Void", "()": return "Unit" + default: return typeName + } + } +} diff --git a/Sources/JExtractSwiftLib/Swift2Java.swift b/Sources/JExtractSwiftLib/Swift2Java.swift index 4a75f218e..9c6e0e93b 100644 --- a/Sources/JExtractSwiftLib/Swift2Java.swift +++ b/Sources/JExtractSwiftLib/Swift2Java.swift @@ -108,6 +108,14 @@ public struct SwiftToJava { ) try generator.generate() + + case .kotlinJvm: + let generator = KotlinSwift2JavaGenerator( + config: self.config, + translator: translator, + kotlinOutputDirectory: outputJavaDirectory + ) + try generator.generate() } print("[swift-java] Imported Swift module '\(swiftModule)': " + "done.".green) diff --git a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift index 8e11d82b0..f4fa61a76 100644 --- a/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift +++ b/Sources/SwiftJavaConfigurationShared/JExtract/JExtractGenerationMode.swift @@ -20,6 +20,9 @@ public enum JExtractGenerationMode: String, Sendable, Codable { /// Java Native Interface case jni + /// Kotlin JVM + case kotlinJvm = "kotlin-jvm" + public static var `default`: JExtractGenerationMode { .ffm } diff --git a/Tests/JExtractSwiftTests/KotlinGeneratorTests.swift b/Tests/JExtractSwiftTests/KotlinGeneratorTests.swift new file mode 100644 index 000000000..dfdc6165f --- /dev/null +++ b/Tests/JExtractSwiftTests/KotlinGeneratorTests.swift @@ -0,0 +1,80 @@ +//===----------------------------------------------------------------------===// +// KotlinGeneratorTests.swift +//===----------------------------------------------------------------------===// + +import Testing +@testable import JExtractSwiftLib + +@Suite("Kotlin Generator Tests") +struct KotlinGeneratorTests { + + // Helper to run the Kotlin generator and return output as a string + func generateKotlin(input: String) throws -> String { + var config = Configuration() + config.swiftModule = "MyModule" + + let translator = Swift2JavaTranslator(config: config) + translator.log.logLevel = .error + try translator.analyze(path: "Fake.swift", text: input) + + let generator = KotlinSwift2JavaGenerator( + config: config, + translator: translator, + kotlinOutputDirectory: "/fake" + ) + + return CodePrinter.toString { printer in + generator.printModule(&printer) + } + } + + @Test("Generates object wrapper for module") + func generatesModuleObject() throws { + let output = try generateKotlin(input: "public func hello()") + #expect(output.contains("object MyModule")) + } + + @Test("Generates header comment") + func generatesHeader() throws { + let output = try generateKotlin(input: "public func hello()") + #expect(output.contains("// Generated by swift-java")) + #expect(output.contains("// Swift module: MyModule")) + } + + @Test("Maps Int to Kotlin Int") + func mapsIntType() throws { + let output = try generateKotlin(input: "public func add(a: Int, b: Int) -> Int") + #expect(output.contains("fun add(a: Int, b: Int): Int")) + } + + @Test("Maps Bool to Kotlin Boolean") + func mapsBoolType() throws { + let output = try generateKotlin(input: "public func isEven(n: Int) -> Bool") + #expect(output.contains("Boolean")) + } + + @Test("Maps Double to Kotlin Double") + func mapsDoubleType() throws { + let output = try generateKotlin(input: "public func area(r: Double) -> Double") + #expect(output.contains("fun area(r: Double): Double")) + } + + @Test("Maps String to Kotlin String") + func mapsStringType() throws { + let output = try generateKotlin(input: "public func greet(name: String) -> String") + #expect(output.contains("fun greet(name: String): String")) + } + + @Test("Maps Void return type to Unit") + func mapsVoidToUnit() throws { + let output = try generateKotlin(input: "public func doNothing()") + #expect(output.contains("fun doNothing()")) + #expect(!output.contains(": Unit")) // Unit return is implicit in Kotlin + } + + @Test("Generates TODO stub body") + func generatesTodoStub() throws { + let output = try generateKotlin(input: "public func add(a: Int, b: Int) -> Int") + #expect(output.contains("TODO(\"Not implemented\")")) + } +}