diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/NestedTypes.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/NestedTypes.swift new file mode 100644 index 00000000..b7edefa9 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/NestedTypes.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 SwiftJava + +public class A { + public init() {} + + public class B { + public init() {} + + public struct C { + public init() {} + + public func g(a: A, b: B, bbc: BB.C) {} + } + } + + public class BB { + public init() {} + + public struct C { + public init() {} + } + } + + public func f(a: A, b: A.B, c: A.B.C, bb: BB, bbc: BB.C) {} +} + +public enum NestedEnum { + case one(OneStruct) + + public struct OneStruct { + public init() {} + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/NestedTypesTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/NestedTypesTest.java new file mode 100644 index 00000000..b006ea91 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/NestedTypesTest.java @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 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.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.SwiftArena; + +import static org.junit.jupiter.api.Assertions.*; + +public class NestedTypesTest { + @Test + void testClassesAndStructs() { + try (var arena = SwiftArena.ofConfined()) { + var a = A.init(arena); + var b = A.B.init(arena); + var c = A.B.C.init(arena); + var bb = A.BB.init(arena); + var abbc = A.BB.C.init(arena); + + a.f(a, b, c, bb, abbc); + c.g(a, b, abbc); + } + } + + @Test + void testStructInEnum() { + try (var arena = SwiftArena.ofConfined()) { + var obj = NestedEnum.one(NestedEnum.OneStruct.init(arena), arena); + var one = obj.getAsOne(arena); + assertTrue(one.isPresent()); + } + } +} \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/JExtractSwiftLib/ImportedDecls.swift index 84cc43c0..9ba1cd6f 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/JExtractSwiftLib/ImportedDecls.swift @@ -41,12 +41,14 @@ package final class ImportedNominalType: ImportedDecl { package var variables: [ImportedFunc] = [] package var cases: [ImportedEnumCase] = [] var inheritedTypes: [SwiftType] + package var parent: SwiftNominalTypeDeclaration? init(swiftNominal: SwiftNominalTypeDeclaration, lookupContext: SwiftTypeLookupContext) throws { self.swiftNominal = swiftNominal self.inheritedTypes = swiftNominal.inheritanceTypes?.compactMap { try? SwiftType($0.type, lookupContext: lookupContext) } ?? [] + self.parent = swiftNominal.parent } var swiftType: SwiftType { diff --git a/Sources/JExtractSwiftLib/JNI/JNICaching.swift b/Sources/JExtractSwiftLib/JNI/JNICaching.swift index cdb13e3f..b0521946 100644 --- a/Sources/JExtractSwiftLib/JNI/JNICaching.swift +++ b/Sources/JExtractSwiftLib/JNI/JNICaching.swift @@ -14,15 +14,15 @@ enum JNICaching { static func cacheName(for type: ImportedNominalType) -> String { - cacheName(for: type.swiftNominal.name) + cacheName(for: type.swiftNominal.qualifiedName) } static func cacheName(for type: SwiftNominalType) -> String { - cacheName(for: type.nominalTypeDecl.name) + cacheName(for: type.nominalTypeDecl.qualifiedName) } - private static func cacheName(for name: String) -> String { - "_JNI_\(name)" + private static func cacheName(for qualifiedName: String) -> String { + "_JNI_\(qualifiedName.replacingOccurrences(of: ".", with: "_"))" } static func cacheMemberName(for enumCase: ImportedEnumCase) -> String { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index ce6ec18e..1e59c196 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -40,7 +40,9 @@ extension JNISwift2JavaGenerator { package func writeExportedJavaSources(_ printer: inout CodePrinter) throws { let importedTypes = analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) - for (_, ty) in importedTypes { + // Each parent type goes into its own file + // any nested types are printed inside the body as `static class` + for (_, ty) in importedTypes.filter({ _, type in type.parent == nil }) { let filename = "\(ty.swiftNominal.name).java" logger.debug("Printing contents: \(filename)") printImportedNominal(&printer, ty) @@ -145,6 +147,15 @@ extension JNISwift2JavaGenerator { """ ) + let nestedTypes = self.analysis.importedTypes.filter { _, type in + type.parent == decl.swiftNominal + } + + for nestedType in nestedTypes { + printConcreteType(&printer, nestedType.value) + printer.println() + } + printer.print( """ /** @@ -255,13 +266,18 @@ extension JNISwift2JavaGenerator { if decl.swiftNominal.isSendable { printer.print("@ThreadSafe // Sendable") } + var modifiers = ["public"] + if decl.parent != nil { + modifiers.append("static") + } + modifiers.append("final") var implements = ["JNISwiftInstance"] implements += decl.inheritedTypes .compactMap(\.asNominalTypeDeclaration) .filter { $0.kind == .protocol } .map(\.name) let implementsClause = implements.joined(separator: ", ") - printer.printBraceBlock("public final class \(decl.swiftNominal.name) implements \(implementsClause)") { printer in + printer.printBraceBlock("\(modifiers.joined(separator: " ")) class \(decl.swiftNominal.name) implements \(implementsClause)") { printer in body(&printer) } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 56174e3e..3512da07 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -307,9 +307,7 @@ extension JNISwift2JavaGenerator { genericParameters: [SwiftGenericParameterDeclaration], genericRequirements: [SwiftGenericRequirement] ) throws -> [TranslatedParameter] { - try parameters.enumerated().map { - idx, - param in + try parameters.enumerated().map { idx, param in let parameterName = param.name ?? "arg\(idx)" return try translateParameter( swiftType: param.type, @@ -373,7 +371,7 @@ extension JNISwift2JavaGenerator { switch swiftType { case .nominal(let nominalType): - let nominalTypeName = nominalType.nominalTypeDecl.name + let nominalTypeName = nominalType.nominalTypeDecl.qualifiedName if let knownType = nominalType.nominalTypeDecl.knownTypeKind { switch knownType { diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index c9f8af9e..91a0b0e7 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -430,7 +430,7 @@ extension JNISwift2JavaGenerator { let cName = "Java_" + self.javaPackage.replacingOccurrences(of: ".", with: "_") - + "_\(parentName.escapedJNIIdentifier)_" + + "_\(parentName.replacingOccurrences(of: ".", with: "$").escapedJNIIdentifier)_" + javaMethodName.escapedJNIIdentifier + "__" + jniSignature.escapedJNIIdentifier @@ -474,7 +474,7 @@ extension JNISwift2JavaGenerator { printCDecl( &printer, javaMethodName: "$typeMetadataAddressDowncall", - parentName: type.swiftNominal.name, + parentName: type.swiftNominal.qualifiedName, parameters: [], resultType: .long ) { printer in @@ -493,7 +493,7 @@ extension JNISwift2JavaGenerator { printCDecl( &printer, javaMethodName: "$destroy", - parentName: type.swiftNominal.name, + parentName: type.swiftNominal.qualifiedName, parameters: [ selfPointerParam ], diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift index 6328045f..2db19928 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftModuleSymbolTable.swift @@ -56,4 +56,4 @@ extension SwiftModuleSymbolTable { /// Names of modules which are alternative for currently checked module. let moduleNames: Set } -} \ No newline at end of file +} diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift index 13f42cfc..33759a2c 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift @@ -119,7 +119,15 @@ class SwiftTypeLookupContext { /// Create a nominal type declaration instance for the specified syntax node. private func nominalTypeDeclaration(for node: NominalTypeDeclSyntaxNode, sourceFilePath: String) throws -> SwiftNominalTypeDeclaration { - SwiftNominalTypeDeclaration( + + if let symbolTableDeclaration = self.symbolTable.lookupType( + node.name.text, + parent: try parentTypeDecl(for: node) + ) { + return symbolTableDeclaration + } + + return SwiftNominalTypeDeclaration( sourceFilePath: sourceFilePath, moduleName: self.symbolTable.moduleName, parent: try parentTypeDecl(for: node), diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index 4bbaee40..3178f60d 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -86,7 +86,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Subscripts: `subscript()` | ❌ | ❌ | | Equatable | ❌ | ❌ | | Pointers: `UnsafeRawPointer`, UnsafeBufferPointer (?) | 🟡 | ❌ | -| Nested types: `struct Hello { struct World {} }` | ❌ | ❌ | +| Nested types: `struct Hello { struct World {} }` | ❌ | ✅ | | Inheritance: `class Caplin: Capybara` | ❌ | ❌ | | Non-escaping `Void` closures: `func callMe(maybe: () -> ())` | ✅ | ✅ | | Non-escaping closures with primitive arguments/results: `func callMe(maybe: (Int) -> (Double))` | ✅ | ✅ | diff --git a/Tests/JExtractSwiftTests/JNI/JNINestedTypesTests.swift b/Tests/JExtractSwiftTests/JNI/JNINestedTypesTests.swift new file mode 100644 index 00000000..50a0ae26 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNINestedTypesTests.swift @@ -0,0 +1,137 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 Testing + +@Suite +struct JNINestedTypesTests { + let source1 = """ + public class A { + public class B { + public func g(c: C) {} + + public struct C { + public func h(b: B) {} + } + } + } + + public func f(a: A, b: A.B, c: A.B.C) {} + """ + + @Test("Import: class and struct A.B.C (Java)") + func nestedClassesAndStructs_java() throws { + try assertOutput( + input: source1, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public final class A implements JNISwiftInstance { + ... + public static final class B implements JNISwiftInstance { + ... + public static final class C implements JNISwiftInstance { + ... + public void h(A.B b) { + ... + } + ... + public void g(A.B.C c) { + ... + } + ... + } + """, + """ + public static void f(A a, A.B b, A.B.C c) { + ... + } + ... + """ + ] + ) + } + + @Test("Import: class and struct A.B.C (Swift)") + func nestedClassesAndStructs_swift() throws { + try assertOutput( + input: source1, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_A__00024destroy__J") + func Java_com_example_swift_A__00024destroy__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) { + ... + } + """, + """ + @_cdecl("Java_com_example_swift_A_00024B__00024destroy__J") + func Java_com_example_swift_A_00024B__00024destroy__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) { + ... + } + """, + """ + @_cdecl("Java_com_example_swift_A_00024B__00024destroy__J") + func Java_com_example_swift_A_00024B__00024destroy__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) { + ... + } + """, + """ + @_cdecl("Java_com_example_swift_A_00024B_00024C__00024h__JJ") + func Java_com_example_swift_A_00024B_00024C__00024h__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, b: jlong, self: jlong) { + ... + } + """ + ] + ) + } + + @Test("Import: nested in enum") + func nestedEnums_java() throws { + try assertOutput( + input: """ + public enum MyError { + case text(TextMessage) + + public struct TextMessage {} + } + + public func f(text: MyError.TextMessage) {} + """, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public final class MyError implements JNISwiftInstance { + ... + public static final class TextMessage implements JNISwiftInstance { + ... + } + ... + public static MyError text(MyError.TextMessage arg0, SwiftArena swiftArena$) { + ... + } + """, + """ + public static void f(MyError.TextMessage text) { + ... + } + """ + ] + ) + } +}