Skip to content

Commit 7519b4c

Browse files
authored
extract CustomStringConvertible as toString() and fix extensions (#473)
* fix extensions * add runtime test * update docs * add `toString` and `toDebugString` * fix merge
1 parent 670f640 commit 7519b4c

File tree

9 files changed

+276
-5
lines changed

9 files changed

+276
-5
lines changed

Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/MySwiftClass.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,15 @@ public class MySwiftClass {
9999
return self.x + other.longValue()
100100
}
101101
}
102+
103+
extension MySwiftClass: CustomStringConvertible {
104+
public var description: String {
105+
"MySwiftClass(x: \(x), y: \(y))"
106+
}
107+
}
108+
109+
extension MySwiftClass: CustomDebugStringConvertible {
110+
public var debugDescription: String {
111+
"debug: MySwiftClass(x: \(x), y: \(y))"
112+
}
113+
}

Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/MySwiftClassTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,20 @@ void getAsyncVariable() throws Exception {
171171
assertEquals(42, c1.getGetAsync().get());
172172
}
173173
}
174+
175+
@Test
176+
void toStringTest() {
177+
try (var arena = SwiftArena.ofConfined()) {
178+
MySwiftClass c1 = MySwiftClass.init(20, 10, arena);
179+
assertEquals("MySwiftClass(x: 20, y: 10)", c1.toString());
180+
}
181+
}
182+
183+
@Test
184+
void toDebugStringTest() {
185+
try (var arena = SwiftArena.ofConfined()) {
186+
MySwiftClass c1 = MySwiftClass.init(20, 10, arena);
187+
assertEquals("debug: MySwiftClass(x: 20, y: 10)", c1.toDebugString());
188+
}
189+
}
174190
}

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,12 +260,38 @@ extension JNISwift2JavaGenerator {
260260
printer.println()
261261
}
262262

263+
printToStringMethods(&printer, decl)
264+
printer.println()
265+
263266
printTypeMetadataAddressFunction(&printer, decl)
264267
printer.println()
265268
printDestroyFunction(&printer, decl)
266269
}
267270
}
268271

272+
273+
private func printToStringMethods(_ printer: inout CodePrinter, _ decl: ImportedNominalType) {
274+
printer.printBraceBlock("public String toString()") { printer in
275+
printer.print(
276+
"""
277+
return $toString(this.$memoryAddress());
278+
"""
279+
)
280+
}
281+
printer.print("private static native java.lang.String $toString(long selfPointer);")
282+
283+
printer.println()
284+
285+
printer.printBraceBlock("public String toDebugString()") { printer in
286+
printer.print(
287+
"""
288+
return $toDebugString(this.$memoryAddress());
289+
"""
290+
)
291+
}
292+
printer.print("private static native java.lang.String $toDebugString(long selfPointer);")
293+
}
294+
269295
private func printHeader(_ printer: inout CodePrinter) {
270296
printer.print(
271297
"""

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ extension JNISwift2JavaGenerator {
253253
printer.println()
254254
}
255255

256-
256+
printToStringMethods(&printer, type)
257257
printTypeMetadataAddressThunk(&printer, type)
258258
printer.println()
259259
printDestroyFunctionThunk(&printer, type)
@@ -267,6 +267,48 @@ extension JNISwift2JavaGenerator {
267267
try printSwiftInterfaceWrapper(&printer, protocolWrapper)
268268
}
269269

270+
private func printToStringMethods(_ printer: inout CodePrinter, _ type: ImportedNominalType) {
271+
let selfPointerParam = JavaParameter(name: "selfPointer", type: .long)
272+
let parentName = type.qualifiedName
273+
274+
printCDecl(
275+
&printer,
276+
javaMethodName: "$toString",
277+
parentName: type.swiftNominal.qualifiedName,
278+
parameters: [
279+
selfPointerParam
280+
],
281+
resultType: .javaLangString
282+
) { printer in
283+
let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam)
284+
285+
printer.print(
286+
"""
287+
return String(describing: \(selfVar).pointee).getJNIValue(in: environment)
288+
"""
289+
)
290+
}
291+
292+
printer.println()
293+
294+
printCDecl(
295+
&printer,
296+
javaMethodName: "$toDebugString",
297+
parentName: type.swiftNominal.qualifiedName,
298+
parameters: [
299+
selfPointerParam
300+
],
301+
resultType: .javaLangString
302+
) { printer in
303+
let selfVar = self.printSelfJLongToUnsafeMutablePointer(&printer, swiftParentName: parentName, selfPointerParam)
304+
305+
printer.print(
306+
"""
307+
return String(reflecting: \(selfVar).pointee).getJNIValue(in: environment)
308+
"""
309+
)
310+
}
311+
}
270312

271313
private func printEnumDiscriminator(_ printer: inout CodePrinter, _ type: ImportedNominalType) {
272314
let selfPointerParam = JavaParameter(name: "selfPointer", type: .long)

Sources/JExtractSwiftLib/Swift2JavaVisitor.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ final class Swift2JavaVisitor {
109109
guard let importedNominalType = translator.importedNominalType(node.extendedType) else {
110110
return
111111
}
112+
113+
// Add any conforming protocols in the extension
114+
importedNominalType.inheritedTypes += node.inheritanceClause?.inheritedTypes.compactMap {
115+
try? SwiftType($0.type, lookupContext: translator.lookupContext)
116+
} ?? []
117+
112118
for memberItem in node.memberBlock.members {
113119
self.visit(decl: memberItem.decl, in: importedNominalType, sourceFilePath: sourceFilePath)
114120
}
@@ -374,9 +380,6 @@ final class Swift2JavaVisitor {
374380
self.visit(decl: decl, in: imported, sourceFilePath: imported.sourceFilePath)
375381
}
376382

377-
// FIXME: why is this un-used
378-
imported.variables.first?.signatureString
379-
380383
if !imported.initializers.contains(where: {
381384
$0.functionSignature.parameters.count == 1
382385
&& $0.functionSignature.parameters.first?.parameterName == "rawValue"

Sources/JExtractSwiftLib/SwiftTypes/SwiftTypeLookupContext.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,17 @@ class SwiftTypeLookupContext {
105105
typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath)
106106
case .protocolDecl(let node):
107107
typeDecl = try nominalTypeDeclaration(for: node, sourceFilePath: sourceFilePath)
108+
case .extensionDecl(let node):
109+
// For extensions, we have to perform a unqualified lookup,
110+
// as the extentedType is just the identifier of the type.
111+
112+
guard case .identifierType(let id) = Syntax(node.extendedType).as(SyntaxEnum.self),
113+
let lookupResult = try unqualifiedLookup(name: Identifier(id.name)!, from: node)
114+
else {
115+
throw TypeLookupError.notType(Syntax(node))
116+
}
117+
118+
typeDecl = lookupResult
108119
case .typeAliasDecl:
109120
fatalError("typealias not implemented")
110121
case .associatedTypeDecl:

Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S
9292
| Non-escaping closures with primitive arguments/results: `func callMe(maybe: (Int) -> (Double))` |||
9393
| Non-escaping closures with object arguments/results: `func callMe(maybe: (JavaObj) -> (JavaObj))` |||
9494
| `@escaping` closures: `func callMe(_: @escaping () -> ())` |||
95-
| Swift type extensions: `extension String { func uppercased() }` | 🟡 | 🟡 |
95+
| Swift type extensions: `extension String { func uppercased() }` | | |
9696
| Swift macros (maybe) |||
9797
| Result builders |||
9898
| Automatic Reference Counting of class types / lifetime safety |||
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import JExtractSwiftLib
16+
import Testing
17+
18+
@Suite
19+
struct JNIExtensionTests {
20+
let interfaceFile =
21+
"""
22+
extension MyStruct {
23+
public var variableInExtension: String { get }
24+
public func methodInExtension() {}
25+
}
26+
27+
public protocol MyProtocol {}
28+
public struct MyStruct {}
29+
extension MyStruct: MyProtocol {}
30+
"""
31+
32+
@Test("Import extensions: Java methods")
33+
func import_javaMethods() throws {
34+
try assertOutput(
35+
input: interfaceFile,
36+
.jni, .java,
37+
detectChunkByInitialLines: 1,
38+
expectedChunks: [
39+
"""
40+
public final class MyStruct implements JNISwiftInstance, MyProtocol {
41+
...
42+
public void methodInExtension() {
43+
...
44+
}
45+
"""
46+
])
47+
}
48+
49+
@Test("Import extensions: Computed variables")
50+
func import_computedVariables() throws {
51+
try assertOutput(
52+
input: interfaceFile,
53+
.jni, .java,
54+
detectChunkByInitialLines: 1,
55+
expectedChunks: [
56+
"""
57+
public final class MyStruct implements JNISwiftInstance, MyProtocol {
58+
...
59+
public java.lang.String getVariableInExtension() {
60+
...
61+
}
62+
"""
63+
])
64+
}
65+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import JExtractSwiftLib
16+
import Testing
17+
import SwiftJavaConfigurationShared
18+
19+
@Suite
20+
struct JNIToStringTests {
21+
let source =
22+
"""
23+
public struct MyType {}
24+
"""
25+
26+
@Test("JNI toString (Java)")
27+
func toString_java() throws {
28+
try assertOutput(
29+
input: source,
30+
.jni, .java,
31+
detectChunkByInitialLines: 1,
32+
expectedChunks: [
33+
"""
34+
public String toString() {
35+
return $toString(this.$memoryAddress());
36+
}
37+
""",
38+
"""
39+
private static native java.lang.String $toString(long selfPointer);
40+
"""
41+
]
42+
)
43+
}
44+
45+
@Test("JNI toString (Swift)")
46+
func toString_swift() throws {
47+
try assertOutput(
48+
input: source,
49+
.jni, .swift,
50+
detectChunkByInitialLines: 1,
51+
expectedChunks: [
52+
"""
53+
@_cdecl("Java_com_example_swift_MyType__00024toString__J")
54+
func Java_com_example_swift_MyType__00024toString__J(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, selfPointer: jlong) -> jstring? {
55+
...
56+
return String(describing: self$.pointee).getJNIValue(in: environment)
57+
}
58+
""",
59+
]
60+
)
61+
}
62+
63+
@Test("JNI toDebugString (Java)")
64+
func toDebugString_java() throws {
65+
try assertOutput(
66+
input: source,
67+
.jni, .java,
68+
detectChunkByInitialLines: 1,
69+
expectedChunks: [
70+
"""
71+
public String toDebugString() {
72+
return $toDebugString(this.$memoryAddress());
73+
}
74+
""",
75+
]
76+
)
77+
}
78+
79+
@Test("JNI toDebugString (Swift)")
80+
func toDebugString_swift() throws {
81+
try assertOutput(
82+
input: source,
83+
.jni, .swift,
84+
detectChunkByInitialLines: 1,
85+
expectedChunks: [
86+
"""
87+
@_cdecl("Java_com_example_swift_MyType__00024toDebugString__J")
88+
func Java_com_example_swift_MyType__00024toDebugString__J(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, selfPointer: jlong) -> jstring? {
89+
...
90+
return String(reflecting: self$.pointee).getJNIValue(in: environment)
91+
}
92+
""",
93+
]
94+
)
95+
}
96+
}

0 commit comments

Comments
 (0)