Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// Without associated values, so Kotlin gets a Java `enum` (`payloadlessEnumsAsJavaEnums`).
public enum OrderStatus: String, CaseIterable, Sendable {
case pending
case filled
case cancelled

public var isFinal: Bool { self != .pending }
}

public struct Order: Sendable {
public var id: String
public var status: OrderStatus

public init(id: String, status: OrderStatus) {
self.id = id
self.status = status
}
}

public enum OrderEvent: Sendable {
case changed(OrderStatus)
}

public enum Orders {
public static func fill(_ status: OrderStatus) -> OrderStatus {
status == .pending ? .filled : status
}

public static func all() -> [OrderStatus] { OrderStatus.allCases }

public static func status(of id: String) -> OrderStatus? { id.isEmpty ? nil : .pending }

public static func byID() -> [String: OrderStatus] { ["a": .filled] }

public static func event(_ status: OrderStatus) -> OrderEvent { .changed(status) }

public static func watch(_ onChange: @escaping @Sendable (OrderStatus) -> Void) {
onChange(.cancelled)
}

public static func settle() async -> OrderStatus { .filled }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"javaPackage": "com.example.kotlin",
"mode": "jni",
"memoryManagementMode": "allowGlobalAutomatic",
"failOnSkippedDeclarations": true
"failOnSkippedDeclarations": true,
"payloadlessEnumsAsJavaEnums": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

package com.example.kotlin

import kotlinx.coroutines.future.await
import kotlinx.coroutines.runBlocking
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertSame
import kotlin.test.assertTrue

class JavaEnumsTest {
@Test
fun exhaustiveWhen() {
val label = when (Orders.fill(OrderStatus.PENDING)) {
OrderStatus.PENDING -> "pending"
OrderStatus.FILLED -> "filled"
OrderStatus.CANCELLED -> "cancelled"
}
assertEquals("filled", label)
}

@Test
fun constantsAreTheValuesSwiftReturns() {
assertSame(OrderStatus.FILLED, Orders.fill(OrderStatus.PENDING))
assertEquals(OrderStatus.entries, Orders.all().toList())
assertEquals("cancelled", OrderStatus.CANCELLED.rawValue)
assertTrue(OrderStatus.CANCELLED.isFinal)
}

@Test
fun everywhereAValueCrosses() = runBlocking {
assertSame(OrderStatus.PENDING, Orders.status("a").get())
assertSame(OrderStatus.FILLED, Orders.byID()["a"])
assertSame(OrderStatus.CANCELLED, Order.init("a", OrderStatus.CANCELLED).status)
assertSame(OrderStatus.FILLED, (Orders.event(OrderStatus.FILLED).case as OrderEvent.Case.Changed).arg0)
var seen: OrderStatus? = null
Orders.watch { seen = it }
assertSame(OrderStatus.CANCELLED, seen)
assertSame(OrderStatus.FILLED, Orders.settle().await())
}

@Test
fun manyRoundTrips() {
repeat(10_000) { assertSame(OrderStatus.FILLED, Orders.fill(OrderStatus.FILLED)) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,10 @@ extension JNISwift2JavaGenerator {

private func printConcreteType(_ printer: inout JavaPrinter, _ decl: ExtractedNominalType) {
printNominal(&printer, decl) { printer in
if self.isJavaEnum(decl) {
printer.print(decl.cases.map { $0.name.uppercased() }.joined(separator: ", ") + ";")
printer.println()
}
printer.print(
"""
static final java.lang.String LIB_NAME = "\(config.nativeLibraryName ?? swiftModuleName)";
Expand Down Expand Up @@ -365,44 +369,48 @@ extension JNISwift2JavaGenerator {
}
let javaName = decl.effectiveJavaSimpleName
let superclass = self.extractedSuperclass(of: decl)
printDesignatedConstructor(
&printer,
javaName: javaName,
pointerParams: swiftPointerParams,
isExtendable: self.isExtendableClass(decl),
hasSuperclass: superclass != nil
)
printer.println()
let genericClause = decl.javaGenericClause
printWrapMemoryAddressUnsafeFactory(
&printer,
javaName: javaName,
genericClause: genericClause,
pointerParams: swiftPointerParams,
docSummary: "Assume that the passed {@code long} represents a memory address of a {@link \(javaName)}.",
subclasses: self.extractedSubclasses(of: decl)
)
if self.isJavaEnum(decl) {
printJavaEnumSwiftValues(&printer, decl)
} else {
printDesignatedConstructor(
&printer,
javaName: javaName,
pointerParams: swiftPointerParams,
isExtendable: self.isExtendableClass(decl),
hasSuperclass: superclass != nil
)
printer.println()
let genericClause = decl.javaGenericClause
printWrapMemoryAddressUnsafeFactory(
&printer,
javaName: javaName,
genericClause: genericClause,
pointerParams: swiftPointerParams,
docSummary: "Assume that the passed {@code long} represents a memory address of a {@link \(javaName)}.",
subclasses: self.extractedSubclasses(of: decl)
)

// A subclass inherits the pointer and cleanup from the root of its hierarchy.
if superclass == nil {
printer.print(
"""
/** Pointer to the "self". */
private final long selfPointer;
// A subclass inherits the pointer and cleanup from the root of its hierarchy.
if superclass == nil {
printer.print(
"""
/** Pointer to the "self". */
private final long selfPointer;

/** Tracks whether this instance has been destroyed; doubles as the destroyed-state holder. */
private final SwiftInstanceCleanup $cleanup;
/** Tracks whether this instance has been destroyed; doubles as the destroyed-state holder. */
private final SwiftInstanceCleanup $cleanup;

public long $memoryAddress() {
return this.selfPointer;
}
public long $memoryAddress() {
return this.selfPointer;
}

@Override
public SwiftInstanceCleanup $cleanup() {
return $cleanup;
}
"""
)
@Override
public SwiftInstanceCleanup $cleanup() {
return $cleanup;
}
"""
)
}
}

if isEffectivelyGeneric {
Expand Down Expand Up @@ -545,21 +553,24 @@ extension JNISwift2JavaGenerator {

/// Prints common Swift object methods such as `equals`, `hashCode` etc.
private func printSwiftInstanceObjectMethods(_ printer: inout JavaPrinter, _ decl: ExtractedNominalType) {
printer.print(
"""
public boolean equals(Object obj) {
if (obj instanceof JNISwiftInstance rhs) {
return SwiftObjects.equals(this.$memoryAddress(), this.$typeMetadataAddress(), rhs.$memoryAddress(), rhs.$typeMetadataAddress());
// A Java enum has one constant per case, so its final `equals` and `hashCode` already match Swift's.
if !self.isJavaEnum(decl) {
printer.print(
"""
public boolean equals(Object obj) {
if (obj instanceof JNISwiftInstance rhs) {
return SwiftObjects.equals(this.$memoryAddress(), this.$typeMetadataAddress(), rhs.$memoryAddress(), rhs.$typeMetadataAddress());
}
return false;
}
return false;
}

public int hashCode() {
return SwiftObjects.hashCode(this.$memoryAddress(), this.$typeMetadataAddress());
}
public int hashCode() {
return SwiftObjects.hashCode(this.$memoryAddress(), this.$typeMetadataAddress());
}

"""
)
"""
)
}
// Error types inherit `Throwable.toString()`, which prints the class name followed by the Swift description.
if !self.isSwiftErrorType(decl) {
printer.print(
Expand Down Expand Up @@ -634,7 +645,8 @@ extension JNISwift2JavaGenerator {
if decl.parent != nil {
modifiers.append("static")
}
if !self.isExtendableClass(decl) {
let isJavaEnum = self.isJavaEnum(decl)
if !self.isExtendableClass(decl) && !isJavaEnum {
modifiers.append("final")
}
let isErrorType = self.isSwiftErrorType(decl)
Expand All @@ -659,12 +671,64 @@ extension JNISwift2JavaGenerator {
// Specialized types are concrete — no generic clause on the Java side
let genericClause = decl.javaGenericClause
printer.printBraceBlock(
"\(modifiers.joined(separator: " ")) class \(decl.effectiveJavaSimpleName)\(genericClause)\(extendsClause)\(implementsClause)"
"\(modifiers.joined(separator: " ")) \(isJavaEnum ? "enum" : "class") \(decl.effectiveJavaSimpleName)\(genericClause)\(extendsClause)\(implementsClause)"
) { printer in
body(&printer)
}
}

/// Whether `decl` is printed as a Java `enum` (`payloadlessEnumsAsJavaEnums`): a non-generic,
/// non-error Swift enum whose cases have no associated values.
func isJavaEnum(_ decl: ExtractedNominalType) -> Bool {
config.effectivePayloadlessEnumsAsJavaEnums && decl.swiftNominal.kind == .enum && !decl.swiftNominal.isGeneric
&& !decl.cases.isEmpty && decl.cases.allSatisfy(\.parameters.isEmpty) && !self.isSwiftErrorType(decl)
}

/// A Java enum constant keeps one Swift value of its case for the life of the process, so passing it
/// to Swift needs no allocation. A Swift value returned to Java is mapped to its case's constant and
/// destroyed.
private func printJavaEnumSwiftValues(_ printer: inout JavaPrinter, _ decl: ExtractedNominalType) {
let javaName = decl.effectiveJavaSimpleName
let caseValues = decl.cases.compactMap { self.translatedDecl(for: $0.caseFunction)?.nativeFunctionName }
printer.print(
"""
/** The Swift value of each case, created on first use and never destroyed. */
private static final class $SwiftValues {
static final long[] ADDRESSES = { \(caseValues.map { "\(javaName).\($0)()" }.joined(separator: ", ")) };
}

private static final SwiftInstanceCleanup $permanent = new SwiftInstanceCleanup() {
public boolean isDestroyed() { return false; }
public void run() {}
};

/**
* The constant for the case of the Swift value at {@code selfPointer}, which is destroyed: the
* constant keeps its own Swift value.
*/
public static \(javaName) wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena) {
return wrapMemoryAddressUnsafe(selfPointer);
}

public static \(javaName) wrapMemoryAddressUnsafe(long selfPointer) {
long typeMetadataAddress = \(javaName).$typeMetadataAddressDowncall();
\(javaName) value = values()[SwiftObjects.getRawDiscriminator(selfPointer, typeMetadataAddress)];
SwiftObjects.destroy(selfPointer, typeMetadataAddress);
return value;
}

public long $memoryAddress() {
return $SwiftValues.ADDRESSES[ordinal()];
}

@Override
public SwiftInstanceCleanup $cleanup() {
return $permanent;
}
"""
)
}

private func printModuleClass(_ printer: inout JavaPrinter, body: (inout JavaPrinter) -> Void) {
printer.printBraceBlock("public final class \(swiftModuleName)") { printer in
body(&printer)
Expand Down
8 changes: 8 additions & 0 deletions Sources/SwiftJavaConfigurationShared/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@ public struct Configuration: Codable {
failOnSkippedDeclarations ?? false
}

/// If true, Swift enums whose cases have no associated values are generated as Java `enum`s,
/// with one constant per case, instead of classes wrapping a Swift value. Only supported in
/// `jni` mode.
public var payloadlessEnumsAsJavaEnums: Bool?
public var effectivePayloadlessEnumsAsJavaEnums: Bool {
payloadlessEnumsAsJavaEnums ?? false
}

/// If specified, JExtract will output to this file a list of paths to all generated Java source files.
public var generatedJavaSourcesListFileOutput: String?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,17 @@ extract or translate, and lists each one. Skipped declarations are always report

---

#### payloadlessEnumsAsJavaEnums

- **Type:** `Bool?`
- **Default:** `false`

If true, Swift enums whose cases have no associated values are generated as Java `enum`s,
with one constant per case, instead of classes wrapping a Swift value. Only supported in
`jni` mode.

---

#### generatedJavaSourcesListFileOutput

- **Type:** `String?`
Expand Down
Loading
Loading