Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -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
86 changes: 86 additions & 0 deletions HOW_TO_RUN.md
Original file line number Diff line number Diff line change
@@ -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` |
90 changes: 90 additions & 0 deletions Sources/JExtractSwiftLib/KotlinSwift2JavaGenerator.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
8 changes: 8 additions & 0 deletions Sources/JExtractSwiftLib/Swift2Java.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
80 changes: 80 additions & 0 deletions Tests/JExtractSwiftTests/KotlinGeneratorTests.swift
Original file line number Diff line number Diff line change
@@ -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\")"))
}
}