Skip to content

Commit ad11f85

Browse files
committed
wrap-java: generate genericResult info in @JavaMethods
1 parent 33ed99e commit ad11f85

File tree

3 files changed

+77
-9
lines changed

3 files changed

+77
-9
lines changed

Sources/SwiftJava/Macros.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,22 @@ public macro JavaStaticField(_ javaFieldName: String? = nil, isFinal: Bool = fal
123123
/// ```
124124
///
125125
/// corresponds to the Java constructor `HelloSwift(String name)`.
126+
///
127+
/// ### Generics and type-erasure
128+
/// Swift and Java differ in how they represent generics at runtime.
129+
/// In Java, generics are type-erased and the JVM representation of generic types is erased to `java.lang.Object`.
130+
/// Swift on the other hand, reifies types which means a `Test<T>` in practice will be a specific type with
131+
/// the generic substituted `Test<String>`. This means that at runtime, calling a generic @JavaMethod needs to know
132+
/// which of the parameters (or result type) must be subjected to type-erasure as we form the call into the Java function.
133+
///
134+
/// In order to mark a generic return type you must indicate it to the @JavaMethod macro like this:
135+
/// ```swift
136+
/// // Java: class Test<T> { public <T> get(); }
137+
/// @JavaMethod(genericResult: "T!")
138+
/// func get() -> T!
139+
/// ```
140+
/// This allows the macro to form a call into the get() method, which at runtime, will have an `java.lang.Object`
141+
/// returning method signature, and then, convert the result to the expected `T` type on the Swift side.
126142
@attached(body)
127143
public macro JavaMethod(
128144
genericResult: String? = nil

Sources/SwiftJavaToolLib/JavaClassTranslator.swift

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -643,11 +643,8 @@ extension JavaClassTranslator {
643643
preferValueTypes: true,
644644
outerOptional: .implicitlyUnwrappedOptional
645645
)
646-
// let resultType = try translator.getSwiftTypeNameAsString(
647-
// javaMethod.getGenericReturnType()!,
648-
// preferValueTypes: true,
649-
// outerOptional: .implicitlyUnwrappedOptional
650-
// )
646+
let typeEraseGenericResultType: Bool =
647+
isGenericJavaType(javaMethod.getGenericReturnType())
651648

652649
// FIXME: cleanup the checking here
653650
if resultType != "Void" && resultType != "Swift.Void" {
@@ -659,9 +656,33 @@ extension JavaClassTranslator {
659656
// --- Handle other effects
660657
let throwsStr = javaMethod.throwsCheckedException ? "throws" : ""
661658
let swiftMethodName = javaMethod.getName().escapedSwiftName
662-
let methodAttribute: AttributeSyntax = implementedInSwift
663-
? ""
664-
: javaMethod.isStatic ? "@JavaStaticMethod\n" : "@JavaMethod\n";
659+
660+
// Compute the '@...JavaMethod(...)' details
661+
let methodAttribute: AttributeSyntax
662+
if implementedInSwift {
663+
methodAttribute = ""
664+
} else {
665+
var methodAttributeStr =
666+
if javaMethod.isStatic {
667+
"@JavaStaticMethod"
668+
} else {
669+
"@JavaMethod"
670+
}
671+
// Do we need to record any generic information, in order to enable type-erasure for the upcalls?
672+
var parameters: [String] = []
673+
if typeEraseGenericResultType {
674+
parameters.append("genericResult: \"\(resultType)\"")
675+
}
676+
// TODO: generic parameters?
677+
if !parameters.isEmpty {
678+
methodAttributeStr += "("
679+
methodAttributeStr.append(parameters.joined(separator: ", "))
680+
methodAttributeStr += ")"
681+
}
682+
methodAttributeStr += "\n"
683+
methodAttribute = "\(raw: methodAttributeStr)"
684+
}
685+
665686
let accessModifier = implementedInSwift ? ""
666687
: (javaMethod.isStatic || !translateAsClass) ? "public "
667688
: "open "

Tests/SwiftJavaToolLibTests/WrapJavaTests.swift

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ final class WrapJavaTests: XCTestCase {
227227
)
228228
}
229229

230-
func testGenericSuperclass() async throws {
230+
func testWrapJavaGenericSuperclass() async throws {
231231
return // FIXME: we need this
232232

233233
let classpathURL = try await compileJava(
@@ -270,4 +270,35 @@ final class WrapJavaTests: XCTestCase {
270270
]
271271
)
272272
}
273+
274+
func testWrapJavaGenericMethodTypeErasure_returnType() async throws {
275+
let classpathURL = try await compileJava(
276+
"""
277+
package com.example;
278+
279+
final class Kappa<T> {
280+
public T get() { return null; }
281+
}
282+
""")
283+
284+
try assertWrapJavaOutput(
285+
javaClassNames: [
286+
"com.example.Kappa",
287+
],
288+
classpath: [classpathURL],
289+
expectedChunks: [
290+
"""
291+
import CSwiftJavaJNI
292+
import SwiftJava
293+
""",
294+
"""
295+
@JavaClass("com.example.Kappa")
296+
open class Kappa<T: AnyJavaObject>: JavaObject {
297+
@JavaMethod(genericResult: "T!")
298+
open func get() -> T!
299+
}
300+
"""
301+
]
302+
)
303+
}
273304
}

0 commit comments

Comments
 (0)