From 22f3826cd6ac7234a67b3e04ac4cdb715d92c917 Mon Sep 17 00:00:00 2001 From: Garnik Khroyan Date: Sun, 1 Mar 2026 17:49:06 +0400 Subject: [PATCH 1/9] add Kotlin code generation target --- .../KotlinSwift2JavaGenerator.swift | 95 +++++++++++++++++++ Sources/JExtractSwiftLib/Swift2Java.swift | 8 ++ .../JExtract/JExtractGenerationMode.swift | 3 + 3 files changed, 106 insertions(+) create mode 100644 Sources/JExtractSwiftLib/KotlinSwift2JavaGenerator.swift diff --git a/Sources/JExtractSwiftLib/KotlinSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/KotlinSwift2JavaGenerator.swift new file mode 100644 index 000000000..b81427100 --- /dev/null +++ b/Sources/JExtractSwiftLib/KotlinSwift2JavaGenerator.swift @@ -0,0 +1,95 @@ +//===----------------------------------------------------------------------===// +// 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) { + // DEBUG - add these lines temporarily + print("DEBUG importedGlobalFuncs count: \(analysis.importedGlobalFuncs.count)") + print("DEBUG importedGlobalVariables count: \(analysis.importedGlobalVariables.count)") + print("DEBUG importedTypes count: \(analysis.importedTypes.count)") + + // 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 } From 506bf43f3bbbc29f4d61d8c5bfe703647b123ded Mon Sep 17 00:00:00 2001 From: Garnik Khroyan Date: Sun, 1 Mar 2026 18:06:44 +0400 Subject: [PATCH 2/9] tests added for Kotlin generator --- .../KotlinGeneratorTests.swift | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 Tests/JExtractSwiftTests/KotlinGeneratorTests.swift 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\")")) + } +} From 77e1d82afcde33f62fa30dc9dbf8b69e679c6c98 Mon Sep 17 00:00:00 2001 From: Garnik Khroyan Date: Sun, 1 Mar 2026 18:17:36 +0400 Subject: [PATCH 3/9] design.md file was added with implementation description --- DESIGN.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 DESIGN.md diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 000000000..82a6f5d90 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,48 @@ +# 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 +4. Fix the test environment and verify all tests pass \ No newline at end of file From 26076fa4c85af37b60c5edd435915eb658d96a8c Mon Sep 17 00:00:00 2001 From: Garnik Khroyan Date: Sun, 1 Mar 2026 18:27:26 +0400 Subject: [PATCH 4/9] HOW_TO_RUN.md files was added --- HOW_TO_RUN.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 HOW_TO_RUN.md diff --git a/HOW_TO_RUN.md b/HOW_TO_RUN.md new file mode 100644 index 000000000..e69de29bb From dfe3173176d8b0a4f41957744d346e11e9132fd2 Mon Sep 17 00:00:00 2001 From: Garnik Khroyan Date: Sun, 1 Mar 2026 18:28:19 +0400 Subject: [PATCH 5/9] how to run instructions were added --- HOW_TO_RUN.md | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/HOW_TO_RUN.md b/HOW_TO_RUN.md index e69de29bb..5d9ab52d4 100644 --- a/HOW_TO_RUN.md +++ 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: Long, b: Long): Long = TODO("Not implemented") + fun greet(name: String): String = TODO("Not implemented") + fun isEven(n: Long): Boolean = TODO("Not implemented") +} +``` + +## Type Mappings + +| Swift | Kotlin | +|-------|--------| +| `Int` | `Int` | +| `Int32` | `Int` | +| `Bool` | `Boolean` | +| `Double` | `Double` | +| `String` | `String` | +| `Void` | `Unit` | From 4afc64ac3230a48854af9deaf961ed47caada02f Mon Sep 17 00:00:00 2001 From: Garnik Khroyan Date: Sun, 1 Mar 2026 18:30:51 +0400 Subject: [PATCH 6/9] empty line removed --- DESIGN.md | 1 - 1 file changed, 1 deletion(-) diff --git a/DESIGN.md b/DESIGN.md index 3cf744387..ba2897270 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -45,4 +45,3 @@ object MyModule { 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 - From 12ae48ef3f30aacf4ad8dea571fb1d343d24a3a6 Mon Sep 17 00:00:00 2001 From: Garnik Khroyan Date: Sun, 1 Mar 2026 18:36:29 +0400 Subject: [PATCH 7/9] debug info removed --- HOW_TO_RUN.md | 4 ++-- Sources/JExtractSwiftLib/KotlinSwift2JavaGenerator.swift | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/HOW_TO_RUN.md b/HOW_TO_RUN.md index 5d9ab52d4..b68c66b80 100644 --- a/HOW_TO_RUN.md +++ b/HOW_TO_RUN.md @@ -68,9 +68,9 @@ Expected output: // Swift module: MyModule object MyModule { - fun add(a: Long, b: Long): Long = TODO("Not implemented") + fun add(a: Int, b: Int): Int = TODO("Not implemented") + fun isEven(n: Int): Boolean = TODO("Not implemented") fun greet(name: String): String = TODO("Not implemented") - fun isEven(n: Long): Boolean = TODO("Not implemented") } ``` diff --git a/Sources/JExtractSwiftLib/KotlinSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/KotlinSwift2JavaGenerator.swift index b81427100..c0c6e1c0b 100644 --- a/Sources/JExtractSwiftLib/KotlinSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/KotlinSwift2JavaGenerator.swift @@ -38,11 +38,6 @@ package class KotlinSwift2JavaGenerator: Swift2JavaGenerator { } func printModule(_ printer: inout CodePrinter) { - // DEBUG - add these lines temporarily - print("DEBUG importedGlobalFuncs count: \(analysis.importedGlobalFuncs.count)") - print("DEBUG importedGlobalVariables count: \(analysis.importedGlobalVariables.count)") - print("DEBUG importedTypes count: \(analysis.importedTypes.count)") - // Header printer.print( """ From 7a0076bb77ad503a1406407faf97e65da443128a Mon Sep 17 00:00:00 2001 From: Garnik Khroyan Date: Sun, 1 Mar 2026 18:38:40 +0400 Subject: [PATCH 8/9] functions reordered in how to run --- HOW_TO_RUN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HOW_TO_RUN.md b/HOW_TO_RUN.md index b68c66b80..2942c0914 100644 --- a/HOW_TO_RUN.md +++ b/HOW_TO_RUN.md @@ -69,8 +69,8 @@ Expected output: object MyModule { fun add(a: Int, b: Int): Int = TODO("Not implemented") - fun isEven(n: Int): Boolean = TODO("Not implemented") fun greet(name: String): String = TODO("Not implemented") + fun isEven(n: Int): Boolean = TODO("Not implemented") } ``` From 5deb9a72617f137fbf43b9cfe1aaa1f29eb2ecd7 Mon Sep 17 00:00:00 2001 From: Garnik Khroyan <44695597+Garnik645@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:42:07 +0400 Subject: [PATCH 9/9] Revise DESIGN.md for clarity on implementation details Updated design document to clarify corners cut and ideal solutions. --- DESIGN.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index ba2897270..9c77928a0 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -21,16 +21,12 @@ object MyModule { } ``` ---- - ## 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) @@ -38,8 +34,6 @@ object MyModule { - 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