diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift index e6aed287..72f2fa35 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift @@ -99,3 +99,15 @@ public class MySwiftClass { return self.x + other.longValue() } } + +extension MySwiftClass: CustomStringConvertible { + public var description: String { + "MySwiftClass(x: \(x), y: \(y))" + } +} + +extension MySwiftClass: CustomDebugStringConvertible { + public var debugDescription: String { + "debug: MySwiftClass(x: \(x), y: \(y))" + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java index 860f1641..a1f8a452 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -171,4 +171,20 @@ void getAsyncVariable() throws Exception { assertEquals(42, c1.getGetAsync().get()); } } + + @Test + void toStringTest() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c1 = MySwiftClass.init(20, 10, arena); + assertEquals("MySwiftClass(x: 20, y: 10)", c1.toString()); + } + } + + @Test + void toDebugStringTest() { + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass c1 = MySwiftClass.init(20, 10, arena); + assertEquals("debug: MySwiftClass(x: 20, y: 10)", c1.toDebugString()); + } + } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index c492d439..78cf7d41 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -260,12 +260,38 @@ extension JNISwift2JavaGenerator { printer.println() } + printToStringMethods(&printer, decl) + printer.println() + printTypeMetadataAddressFunction(&printer, decl) printer.println() printDestroyFunction(&printer, decl) } } + + private func printToStringMethods(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + printer.printBraceBlock("public String toString()") { printer in + printer.print( + """ + return $toString(this.$memoryAddress()); + """ + ) + } + printer.print("private static native java.lang.String $toString(long selfPointer);") + + printer.println() + + printer.printBraceBlock("public String toDebugString()") { printer in + printer.print( + """ + return $toDebugString(this.$memoryAddress()); + """ + ) + } + printer.print("private static native java.lang.String $toDebugString(long selfPointer);") + } + private func printHeader(_ printer: inout CodePrinter) { printer.print( """ diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 85fdc640..13955430 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -253,7 +253,7 @@ extension JNISwift2JavaGenerator { printer.println() } - + printToStringMethods(&printer, type) printTypeMetadataAddressThunk(&printer, type) printer.println() printDestroyFunctionThunk(&printer, type) @@ -267,6 +267,48 @@ extension JNISwift2JavaGenerator { try printSwiftInterfaceWrapper(&printer, protocolWrapper) } + private func printToStringMethods(_ printer: inout CodePrinter, _ type: ImportedNominalType) { + let selfPointerParam = JavaParameter(name: "selfPointer", type: .long) + let parentName = type.qualifiedName + + printCDecl( + &printer, + javaMethodName: "$toString", + parentName: type.swiftNominal.qualifiedName, + parameters: [ + selfPointerParam + ], + resultType: .javaLangString + ) { printer in + let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam) + + printer.print( + """ + return String(describing: \(selfVar).pointee).getJNIValue(in: environment) + """ + ) + } + + printer.println() + + printCDecl( + &printer, + javaMethodName: "$toDebugString", + parentName: type.swiftNominal.qualifiedName, + parameters: [ + selfPointerParam + ], + resultType: .javaLangString + ) { printer in + let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam) + + printer.print( + """ + return String(reflecting: \(selfVar).pointee).getJNIValue(in: environment) + """ + ) + } + } private func printEnumDiscriminator(_ printer: inout CodePrinter, _ type: ImportedNominalType) { let selfPointerParam = JavaParameter(name: "selfPointer", type: .long) diff --git a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift index ab1ce32f..cf99d11b 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaVisitor.swift @@ -109,6 +109,12 @@ final class Swift2JavaVisitor { guard let importedNominalType = translator.importedNominalType(node.extendedType) else { return } + + // Add any conforming protocols in the extension + importedNominalType.inheritedTypes += node.inheritanceClause?.inheritedTypes.compactMap { + try? SwiftType($0.type, lookupContext: translator.lookupContext) + } ?? [] + for memberItem in node.memberBlock.members { self.visit(decl: memberItem.decl, in: importedNominalType, sourceFilePath: sourceFilePath) } @@ -374,9 +380,6 @@ final class Swift2JavaVisitor { self.visit(decl: decl, in: imported, sourceFilePath: imported.sourceFilePath) } - // FIXME: why is this un-used - imported.variables.first?.signatureString - if !imported.initializers.contains(where: { $0.functionSignature.parameters.count == 1 && $0.functionSignature.parameters.first?.parameterName == "rawValue" diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift index 33759a2c..4489b4f6 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift @@ -105,6 +105,17 @@ class SwiftTypeLookupContext { typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath) case .protocolDecl(let node): typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath) + case .extensionDecl(let node): + // For extensions, we have to perform a unqualified lookup, + // as the extentedType is just the identifier of the type. + + guard case .identifierType(let id) = Syntax(node.extendedType).as(SyntaxEnum.self), + let lookupResult = try unqualifiedLookup(name: Identifier(id.name)!, from: node) + else { + throw TypeLookupError.notType(Syntax(node)) + } + + typeDecl = lookupResult case .typeAliasDecl: fatalError("typealias not implemented") case .associatedTypeDecl: diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index 849b7f01..7161a26c 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -92,7 +92,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Non-escaping closures with primitive arguments/results: `func callMe(maybe: (Int) -> (Double))` | ✅ | ✅ | | Non-escaping closures with object arguments/results: `func callMe(maybe: (JavaObj) -> (JavaObj))` | ❌ | ❌ | | `@escaping` closures: `func callMe(_: @escaping () -> ())` | ❌ | ❌ | -| Swift type extensions: `extension String { func uppercased() }` | 🟡 | 🟡 | +| Swift type extensions: `extension String { func uppercased() }` | ✅ | ✅ | | Swift macros (maybe) | ❌ | ❌ | | Result builders | ❌ | ❌ | | Automatic Reference Counting of class types / lifetime safety | ✅ | ✅ | diff --git a/Tests/JExtractSwiftTests/JNI/JNIExtensionTests.swift b/Tests/JExtractSwiftTests/JNI/JNIExtensionTests.swift new file mode 100644 index 00000000..e6cb1060 --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIExtensionTests.swift @@ -0,0 +1,65 @@ +//===----------------------------------------------------------------------===// +// +// 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 JNIExtensionTests { + let interfaceFile = + """ + extension MyStruct { + public var variableInExtension: String { get } + public func methodInExtension() {} + } + + public protocol MyProtocol {} + public struct MyStruct {} + extension MyStruct: MyProtocol {} + """ + + @Test("Import extensions: Java methods") + func import_javaMethods() throws { + try assertOutput( + input: interfaceFile, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public final class MyStruct implements JNISwiftInstance, MyProtocol { + ... + public void methodInExtension() { + ... + } + """ + ]) + } + + @Test("Import extensions: Computed variables") + func import_computedVariables() throws { + try assertOutput( + input: interfaceFile, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public final class MyStruct implements JNISwiftInstance, MyProtocol { + ... + public java.lang.String getVariableInExtension() { + ... + } + """ + ]) + } +} diff --git a/Tests/JExtractSwiftTests/JNI/JNIToStringTests.swift b/Tests/JExtractSwiftTests/JNI/JNIToStringTests.swift new file mode 100644 index 00000000..1059002b --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNIToStringTests.swift @@ -0,0 +1,96 @@ +//===----------------------------------------------------------------------===// +// +// 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 +import SwiftJavaConfigurationShared + +@Suite +struct JNIToStringTests { + let source = + """ + public struct MyType {} + """ + + @Test("JNI toString (Java)") + func toString_java() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public String toString() { + return $toString(this.$memoryAddress()); + } + """, + """ + private static native java.lang.String $toString(long selfPointer); + """ + ] + ) + } + + @Test("JNI toString (Swift)") + func toString_swift() throws { + try assertOutput( + input: source, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyType__00024toString__J") + func Java_com_example_swift_MyType__00024toString__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) -> jstring? { + ... + return String(describing: self$.pointee).getJNIValue(in: environment) + } + """, + ] + ) + } + + @Test("JNI toDebugString (Java)") + func toDebugString_java() throws { + try assertOutput( + input: source, + .jni, .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public String toDebugString() { + return $toDebugString(this.$memoryAddress()); + } + """, + ] + ) + } + + @Test("JNI toDebugString (Swift)") + func toDebugString_swift() throws { + try assertOutput( + input: source, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_MyType__00024toDebugString__J") + func Java_com_example_swift_MyType__00024toDebugString__J(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong) -> jstring? { + ... + return String(reflecting: self$.pointee).getJNIValue(in: environment) + } + """, + ] + ) + } +}