Skip to content

JavaKit: Macro and translator support for mapping Java classes to Swift classes #140

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a6780be
JavaKit: Macro and translator support for mapping Java classes to Swi…
DougGregor Oct 30, 2024
6440a3c
Java2Swift: Mark instance methods as "override" when needed
DougGregor Oct 30, 2024
83b0e1a
Java2Swift: Always use the nearest imported superclass for the superc…
DougGregor Oct 30, 2024
819dd27
Java2Swift: Regenerate generated mappings
DougGregor Oct 30, 2024
2a7d841
Java2Swift: Drop the "as(superclass)" function when generating classes
DougGregor Oct 30, 2024
4d833cf
Java2Swift: all initializers in "class mode" are convenience inits
DougGregor Oct 30, 2024
1baa095
Java2Swift: Fix enum initializer for class-based generation.
DougGregor Oct 31, 2024
1715af8
Java2Swift: Don't use Self in an invariant position in enum convenien…
DougGregor Oct 31, 2024
b55cd4d
Java2Swift: fullJavaClassName needs to be "open class" in class-gener…
DougGregor Oct 31, 2024
45d195c
Java2Swift: Correctly determine if a Swift method will be an override.
DougGregor Oct 31, 2024
9f5fbbf
Java2Swift: Java static methods need to use "public", not "open"
DougGregor Oct 31, 2024
ea6cfaa
Java2Swift: Include protected methods in generated classes
DougGregor Oct 31, 2024
c3337b6
Import java.util.zip.ZipEntry and regenerate JavaKit sources
DougGregor Oct 31, 2024
4b045b2
Java2Swift: Don't translate Java interfaces into Swift classes
DougGregor Oct 31, 2024
374c847
JavaField macro: within a class, don't generate "nonmutating" modifier
DougGregor Oct 31, 2024
7346359
Java2Swift: Account for differences in covariant overrides between Ja…
DougGregor Oct 31, 2024
0e3fc53
Java2Swift: Use `@_nonoverride` on generated convenience initializers
DougGregor Oct 31, 2024
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
3 changes: 2 additions & 1 deletion Sources/Java2Swift/JavaToSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ struct JavaToSwift: ParsableCommand {
) throws {
let translator = JavaTranslator(
swiftModuleName: moduleName,
environment: environment
environment: environment,
translateAsClass: false
)

// Keep track of all of the Java classes that will have
Expand Down
254 changes: 238 additions & 16 deletions Sources/Java2SwiftLib/JavaClassTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ struct JavaClassTranslator {
/// The Java class (or interface) being translated.
let javaClass: JavaClass<JavaObject>

/// Whether to translate this Java class into a Swift class.
///
/// This will be false for Java interfaces.
let translateAsClass: Bool

/// The type parameters to the Java class or interface.
let javaTypeParameters: [TypeVariable<JavaClass<JavaObject>>]

Expand All @@ -37,6 +42,10 @@ struct JavaClassTranslator {
/// class.
let swiftTypeName: String

/// The effective Java superclass object, which is the nearest
/// superclass that has been mapped into Swift.
let effectiveJavaSuperclass: JavaClass<JavaObject>?

/// The Swift name of the superclass.
let swiftSuperclass: String?

Expand Down Expand Up @@ -103,6 +112,7 @@ struct JavaClassTranslator {
let fullName = javaClass.getName()
self.javaClass = javaClass
self.translator = translator
self.translateAsClass = translator.translateAsClass && !javaClass.isInterface()
self.swiftTypeName = try translator.getSwiftTypeNameFromJavaClassName(
fullName,
preferValueTypes: false,
Expand All @@ -114,14 +124,24 @@ struct JavaClassTranslator {
self.nestedClasses = translator.nestedClasses[fullName] ?? []

// Superclass.
if !javaClass.isInterface(), let javaSuperclass = javaClass.getSuperclass() {
do {
self.swiftSuperclass = try translator.getSwiftTypeName(javaSuperclass, preferValueTypes: false).swiftName
} catch {
translator.logUntranslated("Unable to translate '\(fullName)' superclass: \(error)")
self.swiftSuperclass = nil
if !javaClass.isInterface() {
var javaSuperclass = javaClass.getSuperclass()
var swiftSuperclass: String? = nil
while let javaSuperclassNonOpt = javaSuperclass {
do {
swiftSuperclass = try translator.getSwiftTypeName(javaSuperclassNonOpt, preferValueTypes: false).swiftName
break
} catch {
translator.logUntranslated("Unable to translate '\(fullName)' superclass: \(error)")
}

javaSuperclass = javaSuperclassNonOpt.getSuperclass()
}

self.effectiveJavaSuperclass = javaSuperclass
self.swiftSuperclass = swiftSuperclass
} else {
self.effectiveJavaSuperclass = nil
self.swiftSuperclass = nil
}

Expand Down Expand Up @@ -161,9 +181,15 @@ struct JavaClassTranslator {
}

// Gather methods.
for method in javaClass.getMethods() {
let methods = translateAsClass
? javaClass.getDeclaredMethods()
: javaClass.getMethods()
for method in methods {
guard let method else { continue }

// Only look at public and protected methods here.
guard method.isPublic || method.isProtected else { continue }

// Skip any methods that are expected to be implemented in Swift. We will
// visit them in the second pass, over the *declared* methods, because
// we want to see non-public methods as well.
Expand All @@ -178,6 +204,7 @@ struct JavaClassTranslator {
}

if translator.swiftNativeImplementations.contains(javaClass.getName()) {
// Gather the native methods we're going to implement.
for method in javaClass.getDeclaredMethods() {
guard let method else { continue }

Expand Down Expand Up @@ -209,6 +236,12 @@ extension JavaClassTranslator {
return
}

// Don't include inherited fields when translating to a class.
if translateAsClass &&
!field.getDeclaringClass()!.equals(javaClass.as(JavaObject.self)!) {
return
}

fields.append(field)
}

Expand Down Expand Up @@ -283,8 +316,18 @@ extension JavaClassTranslator {
// Collect all of the members of this type.
let members = properties + enumDecls + initializers + instanceMethods

// Compute the "extends" clause for the superclass.
let extends = swiftSuperclass.map { ", extends: \($0).self" } ?? ""
// Compute the "extends" clause for the superclass (of the struct
// formulation) or the inheritance clause (for the class
// formulation).
let extends: String
let inheritanceClause: String
if translateAsClass {
extends = ""
inheritanceClause = swiftSuperclass.map { ": \($0)" } ?? ""
} else {
extends = swiftSuperclass.map { ", extends: \($0).self" } ?? ""
inheritanceClause = ""
}

// Compute the string to capture all of the interfaces.
let interfacesStr: String
Expand All @@ -297,10 +340,11 @@ extension JavaClassTranslator {

// Emit the struct declaration describing the java class.
let classOrInterface: String = isInterface ? "JavaInterface" : "JavaClass";
let introducer = translateAsClass ? "open class" : "public struct"
var classDecl: DeclSyntax =
"""
@\(raw: classOrInterface)(\(literal: javaClass.getName())\(raw: extends)\(raw: interfacesStr))
public struct \(raw: swiftInnermostTypeName)\(raw: genericParameterClause) {
\(raw: introducer) \(raw: swiftInnermostTypeName)\(raw: genericParameterClause)\(raw: inheritanceClause) {
\(raw: members.map { $0.description }.joined(separator: "\n\n"))
}
"""
Expand Down Expand Up @@ -447,9 +491,11 @@ extension JavaClassTranslator {
let parametersStr = parameters.map { $0.description }.joined(separator: ", ")
let throwsStr = javaConstructor.throwsCheckedException ? "throws" : ""
let accessModifier = javaConstructor.isPublic ? "public " : ""
let convenienceModifier = translateAsClass ? "convenience " : ""
let nonoverrideAttribute = translateAsClass ? "@_nonoverride " : ""
return """
@JavaMethod
\(raw: accessModifier)init(\(raw: parametersStr))\(raw: throwsStr)
\(raw: nonoverrideAttribute)\(raw: accessModifier)\(raw: convenienceModifier)init(\(raw: parametersStr))\(raw: throwsStr)
"""
}

Expand Down Expand Up @@ -483,9 +529,14 @@ extension JavaClassTranslator {
let methodAttribute: AttributeSyntax = implementedInSwift
? ""
: javaMethod.isStatic ? "@JavaStaticMethod\n" : "@JavaMethod\n";
let accessModifier = implementedInSwift ? "" : "public "
let accessModifier = implementedInSwift ? ""
: (javaMethod.isStatic || !translateAsClass) ? "public "
: "open "
let overrideOpt = (translateAsClass && !javaMethod.isStatic && isOverride(javaMethod))
? "override "
: ""
return """
\(methodAttribute)\(raw: accessModifier)func \(raw: swiftMethodName)\(raw: genericParameterClause)(\(raw: parametersStr))\(raw: throwsStr)\(raw: resultTypeStr)\(raw: whereClause)
\(methodAttribute)\(raw: accessModifier)\(raw: overrideOpt)func \(raw: swiftMethodName)\(raw: genericParameterClause)(\(raw: parametersStr))\(raw: throwsStr)\(raw: resultTypeStr)\(raw: whereClause)
"""
}

Expand Down Expand Up @@ -532,20 +583,23 @@ extension JavaClassTranslator {
}
"""

let convenienceModifier = translateAsClass ? "convenience " : ""
let initSyntax: DeclSyntax = """
public init(_ enumValue: \(raw: name), environment: JNIEnvironment? = nil) {
public \(raw: convenienceModifier)init(_ enumValue: \(raw: name), environment: JNIEnvironment? = nil) {
let _environment = if let environment {
environment
} else {
try! JavaVirtualMachine.shared().environment()
}
let classObj = try! JavaClass<Self>(environment: _environment)
let classObj = try! JavaClass<\(raw: swiftInnermostTypeName)>(environment: _environment)
switch enumValue {
\(raw: enumConstants.map {
return """
case .\($0.getName()):
if let \($0.getName()) = classObj.\($0.getName()) {
self = \($0.getName())
\(translateAsClass
? "self.init(javaHolder: \($0.getName()).javaHolder)"
: "self = \($0.getName())")
} else {
fatalError("Enum value \($0.getName()) was unexpectedly nil, please re-run Java2Swift on the most updated Java class")
}
Expand Down Expand Up @@ -611,3 +665,171 @@ struct MethodCollector {
methods.append(method)
}
}

// MARK: Utility functions
extension JavaClassTranslator {
/// Determine whether this method is an override of another Java
/// method.
func isOverride(_ method: Method) -> Bool {
var currentSuperclass = effectiveJavaSuperclass
while let currentSuperclassNonOpt = currentSuperclass {
// Set the loop up for the next run.
defer {
currentSuperclass = currentSuperclassNonOpt.getSuperclass()
}

do {
// If this class didn't get translated into Swift, skip it.
if translator.translatedClasses[currentSuperclassNonOpt.getName()] == nil {
continue
}

// If this superclass declares a method with the same parameter types,
// we have an override.
guard let overriddenMethod = try currentSuperclassNonOpt
.getDeclaredMethod(method.getName(), method.getParameterTypes()) else {
continue
}

// Ignore non-public, non-protected methods because they would not
// have been render into the Swift superclass.
if !overriddenMethod.isPublic && !overriddenMethod.isProtected {
continue
}

// We know that Java considers this method an override. However, it is
// possible that Swift will not consider it an override, because Java
// has subtyping relations that Swift does not.
if method.getGenericReturnType().isEqualToOrSubtypeOf(overriddenMethod.getGenericReturnType()) {
return true
}
} catch {
}
}

return false
}
}

extension [Type?] {
/// Determine whether the types in the array match the other array.
func allTypesEqual(_ other: [Type?]) -> Bool {
if self.count != other.count {
return false
}

for (selfType, otherType) in zip(self, other) {
if !selfType!.isEqualTo(otherType!) {
return false
}
}

return true
}
}

extension Type {
/// Adjust the given type to use its bounds, mirroring what we do in
/// mapping Java types into Swift.
func adjustToJavaBounds(adjusted: inout Bool) -> Type {
if let typeVariable = self.as(TypeVariable<GenericDeclaration>.self),
typeVariable.getBounds().count == 1,
let bound = typeVariable.getBounds()[0] {
adjusted = true
return bound
}

if let wildcardType = self.as(WildcardType.self),
wildcardType.getUpperBounds().count == 1,
let bound = wildcardType.getUpperBounds()[0] {
adjusted = true
return bound
}

return self
}

/// Determine whether this type is equivalent to or a subtype of the other
/// type.
func isEqualTo(_ other: Type) -> Bool {
// First, adjust types to their bounds, if we need to.
var anyAdjusted: Bool = false
let adjustedSelf = self.adjustToJavaBounds(adjusted: &anyAdjusted)
let adjustedOther = other.adjustToJavaBounds(adjusted: &anyAdjusted)
if anyAdjusted {
return adjustedSelf.isEqualTo(adjustedOther)
}

// If both are classes, check for equivalence.
if let selfClass = self.as(JavaClass<JavaObject>.self),
let otherClass = other.as(JavaClass<JavaObject>.self) {
return selfClass.equals(otherClass.as(JavaObject.self))
}

// If both are arrays, check that their component types are equivalent.
if let selfArray = self.as(GenericArrayType.self),
let otherArray = other.as(GenericArrayType.self) {
return selfArray.getGenericComponentType().isEqualTo(otherArray.getGenericComponentType())
}

// If both are parameterized types, check their raw type and type
// arguments for equivalence.
if let selfParameterizedType = self.as(ParameterizedType.self),
let otherParameterizedType = other.as(ParameterizedType.self) {
if !selfParameterizedType.getRawType().isEqualTo(otherParameterizedType.getRawType()) {
return false
}

return selfParameterizedType.getActualTypeArguments()
.allTypesEqual(otherParameterizedType.getActualTypeArguments())
}

// If both are type variables, compare their bounds.
// FIXME: This is a hack.
if let selfTypeVariable = self.as(TypeVariable<GenericDeclaration>.self),
let otherTypeVariable = other.as(TypeVariable<GenericDeclaration>.self) {
return selfTypeVariable.getBounds().allTypesEqual(otherTypeVariable.getBounds())
}

// If both are wildcards, compare their upper and lower bounds.
if let selfWildcard = self.as(WildcardType.self),
let otherWildcard = other.as(WildcardType.self) {
return selfWildcard.getUpperBounds().allTypesEqual(otherWildcard.getUpperBounds())
&& selfWildcard.getLowerBounds().allTypesEqual(otherWildcard.getLowerBounds())
}

return false
}

/// Determine whether this type is equivalent to or a subtype of the
/// other type.
func isEqualToOrSubtypeOf(_ other: Type) -> Bool {
// First, adjust types to their bounds, if we need to.
var anyAdjusted: Bool = false
let adjustedSelf = self.adjustToJavaBounds(adjusted: &anyAdjusted)
let adjustedOther = other.adjustToJavaBounds(adjusted: &anyAdjusted)
if anyAdjusted {
return adjustedSelf.isEqualToOrSubtypeOf(adjustedOther)
}

if isEqualTo(other) {
return true
}

// If both are classes, check for subclassing.
if let selfClass = self.as(JavaClass<JavaObject>.self),
let otherClass = other.as(JavaClass<JavaObject>.self) {
return selfClass.isSubclass(of: otherClass)
}

// Anything object-like is a subclass of java.lang.Object
if let otherClass = other.as(JavaClass<JavaObject>.self),
otherClass.getName() == "java.lang.Object" {
if self.is(GenericArrayType.self) || self.is(ParameterizedType.self) ||
self.is(WildcardType.self) || self.is(TypeVariable<GenericDeclaration>.self) {
return true
}
}
return false
}
}
6 changes: 6 additions & 0 deletions Sources/Java2SwiftLib/JavaTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ package class JavaTranslator {
let swiftModuleName: String

let environment: JNIEnvironment

/// Whether to translate Java classes into classes (rather than structs).
let translateAsClass: Bool

let format: BasicFormat

/// A mapping from the name of each known Java class to the corresponding
Expand Down Expand Up @@ -61,10 +65,12 @@ package class JavaTranslator {
package init(
swiftModuleName: String,
environment: JNIEnvironment,
translateAsClass: Bool = false,
format: BasicFormat = JavaTranslator.defaultFormat
) {
self.swiftModuleName = swiftModuleName
self.environment = environment
self.translateAsClass = translateAsClass
self.format = format
}

Expand Down
Loading