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,31 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// A class with a shared instance: every Kotlin wrapper of it is equal.
public final class Session: Sendable {
public static let shared: Session = Session(name: "shared")

public let name: String

public init(name: String) {
self.name = name
}

public static func echo(_ session: Session) -> Session { session }
}

public enum Echo {
/// Returns the same instance through its superclass type.
public static func animal(_ animal: Animal) -> Animal { animal }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//===----------------------------------------------------------------------===//
//
// 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 kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertNotEquals
import kotlin.test.assertNotSame

// Kotlin compares objects with `==` (equals) and collections hash them: two wrappers of one Swift
// instance must be equal, as Compose relies on to skip recomposition.
class IdentityTest {
@Test
fun wrappersOfTheSameInstanceAreEqual() {
val first = Session.getShared()
val second = Session.getShared()
assertNotSame(first, second)
assertEquals(first, second)
assertEquals(1, setOf(first, second, Session.echo(first)).size)
assertNotEquals(first, Session.init("other"))
}

@Test
fun sameInstanceThroughItsSuperclassType() {
val dog = Dog.init("Rex")
val back: Animal = Echo.animal(dog)
assertNotSame<Animal>(dog, back)
assertEquals<Animal>(dog, back)
assertEquals(dog.hashCode(), back.hashCode())
assertNotEquals<Animal>(dog, Dog.init("Rex"))
}

@Test
fun enumCaseWithoutAnArena() {
val case = Store.init().route(Item.init("a", 1)).case
assertIs<Route.Case.Detail>(case)
assertEquals("a", case.arg0.id)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,8 @@ public func totalArea(_ shapes: [Shape]) -> Double {
public func makeSquareShape(side: Double) -> Shape {
Square(side: side)
}

/// Returns the same instance: Java gets a new wrapper, which must equal the original.
public func sameShape(_ shape: Shape) -> Shape {
shape
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,16 @@ void returnedInstanceHasItsDynamicClass() {
assertEquals(16, shape.area());
}
}

@Test
void wrappersOfTheSameInstanceAreEqual() {
try (var arena = SwiftArena.ofConfined()) {
Square square = Square.init(2, arena);
Shape same = MySwiftLibrary.sameShape(square, arena);
assertNotSame(square, same);
assertEquals(square, same);
assertEquals(square.hashCode(), same.hashCode());
assertNotEquals(square, Square.init(2, arena));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,14 @@ extension JNISwift2JavaGenerator {
}
}
}

// With a global automatic arena, also offer `getCase()` without an arena, like other members.
if requiresSwiftArena && config.effectiveMemoryManagementMode.requiresGlobalArena {
printer.println()
printer.printBraceBlock("public Case\(caseGenericClause) getCase()") { printer in
printer.print("return getCase(SwiftMemoryManagement.DEFAULT_SWIFT_JAVA_AUTO_ARENA);")
}
}
}

/// Fully qualifies package-less class types whose leading name is shadowed by one of `names`.
Expand Down
37 changes: 34 additions & 3 deletions Sources/SwiftJava/SwiftObjects.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,27 @@ extension SwiftObjects {

@JavaMethod
public static func equals(environment: UnsafeMutablePointer<JNIEnv?>!, lhsPointer: Int64, lhsTypePointer: Int64, rhsPointer: Int64, rhsTypePointer: Int64) -> Bool {
// Fallback for non-equatable types (such as classes)
// Fallback for non-equatable value types: the same memory
let isPointerIdentityEqual = lhsTypePointer == rhsTypePointer && lhsPointer == rhsPointer

guard let lhsType$ = UnsafeRawPointer(bitPattern: Int(lhsTypePointer)) else {
fatalError("lhsType metadata address was null")
}
let lhsMetatype = unsafeBitCast(lhsType$, to: Any.Type.self)

// Each Java wrapper of a class instance holds its own pointer to the reference, so compare the
// objects themselves: wrappers of the same instance are equal, whatever their static types.
if let lhsObject = Self.classInstance(at: lhsPointer, ofType: lhsMetatype),
let rhsType$ = UnsafeRawPointer(bitPattern: Int(rhsTypePointer)),
let rhsObject = Self.classInstance(at: rhsPointer, ofType: unsafeBitCast(rhsType$, to: Any.Type.self))
{
if lhsObject === rhsObject {
return true
}
guard lhsMetatype is any Equatable.Type else {
return false
}
}
guard let lhsMetatype = lhsMetatype as? (any Equatable.Type) else {
return isPointerIdentityEqual
}
Expand Down Expand Up @@ -145,7 +159,15 @@ extension SwiftObjects {
fatalError("selfType metadata address was null")
}
let typeMetadata = unsafeBitCast(selfType$, to: Any.Type.self)
guard let typeMetadata = typeMetadata as? (any Hashable.Type) else {
guard let hashableMetadata = typeMetadata as? (any Hashable.Type) else {
if let object = Self.classInstance(at: selfPointer, ofType: typeMetadata) {
// Consistent with `equals`: an `Equatable` class may equal other instances, so it can only
// hash by type; otherwise equality is identity.
if typeMetadata is any Equatable.Type {
return Int32(truncatingIfNeeded: ObjectIdentifier(typeMetadata).hashValue)
}
return Int32(truncatingIfNeeded: ObjectIdentifier(object).hashValue)
}
// For value types, different instances may return different hash codes even if the values are same.
return Int32(truncatingIfNeeded: selfPointer.hashValue)
}
Expand All @@ -156,7 +178,16 @@ extension SwiftObjects {
}
return Int32(truncatingIfNeeded: self$.pointee.hashValue)
}
return perform(as: typeMetadata)
return perform(as: hashableMetadata)
}

/// The class instance stored at `pointer`, or nil if `type` isn't a class type.
private static func classInstance(at pointer: Int64, ofType type: Any.Type) -> AnyObject? {
guard type is AnyClass, let raw = UnsafeRawPointer(bitPattern: Int(pointer)) else {
return nil
}
// A class reference has the layout of `AnyObject`.
return raw.load(as: AnyObject.self)
}

@JavaMethod
Expand Down
62 changes: 62 additions & 0 deletions Tests/JExtractSwiftTests/JNI/JNIEnumGetCaseArenaTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

import JExtractSwiftLib
import SwiftJavaConfigurationShared
import Testing

/// `getCase()` on an enum with payloads gets an arena-free overload when a global automatic arena exists.
@Suite
struct JNIEnumGetCaseArenaTests {
let source = """
public struct Item {}
public enum Route {
case home
case detail(Item)
}
"""

@Test
func allowGlobalAutomatic_hasArenaFreeGetCase() throws {
var config = Configuration()
config.memoryManagementMode = .allowGlobalAutomatic
try assertOutput(
input: source,
config: config,
.jni,
.java,
detectChunkByInitialLines: 1,
expectedChunks: [
"public Case getCase(SwiftArena swiftArena) {",
"""
public Case getCase() {
return getCase(SwiftMemoryManagement.DEFAULT_SWIFT_JAVA_AUTO_ARENA);
}
""",
]
)
}

@Test
func explicitMode_onlyHasGetCaseWithArena() throws {
try assertOutput(
input: source,
.jni,
.java,
detectChunkByInitialLines: 1,
expectedChunks: ["public Case getCase(SwiftArena swiftArena) {"],
notExpectedChunks: ["public Case getCase() {"]
)
}
}
Loading