Using Swift classes to describe Java classes (instead of structs)

Hey all,

I've put up a pull request to start translating Java classes into Swift classes. With this change, java.lang.BigIntegerimports like this:

@JavaClass("java.math.BigInteger")
open class BigInteger: JavaNumber {
  @JavaMethod
  public convenience init(_ arg0: String, environment: JNIEnvironment? = nil)

  @JavaMethod
  public convenience init(_ arg0: Int32, _ arg1: [Int8], _ arg2: Int32, _ arg3: Int32, environment: JNIEnvironment? = nil)

  @JavaMethod
  public convenience init(_ arg0: [Int8], environment: JNIEnvironment? = nil)

  @JavaMethod
  public convenience init(_ arg0: [Int8], _ arg1: Int32, _ arg2: Int32, environment: JNIEnvironment? = nil)

  @JavaMethod
  public convenience init(_ arg0: String, _ arg1: Int32, environment: JNIEnvironment? = nil)

  @JavaMethod
  public convenience init(_ arg0: Int32, _ arg1: [Int8], environment: JNIEnvironment? = nil)

  @JavaMethod
  open func bitCount() -> Int32

  @JavaMethod
  open override func equals(_ arg0: JavaObject?) -> Bool

  @JavaMethod
  open func toString(_ arg0: Int32) -> String
}

There are a couple of things to notice here:

  • Java class inheritance is mapped into Swift class inheritance, e.g., BigInteger inherits from JavaNumber rather than being buried in an extends: argument of the macro.
  • The classes and methods are all open because they can be subclassed / overridden. We mark overrides as such in Swift.
  • Initializers are convenience because they all end up calling the Java constructor and then wrapping that up in C++.

There are a bunch of advantages to this new approach:

  • Subtyping works! You can pass a BigInteger as a JavaNumber or JavaObject without annoying calls to .as(...).
  • Overrides are nicely documented with the override keyword
  • It (mostly) meets expectations: Swift classes are the closest semantic match for Java classes, and it comes across as odd (and if you're new to Swift--confusing) to have them mapped to structs that have value semantics
  • We no longer need to emit all of the inherited methods into every class, because we get to inherit from the superclass. This reduced the size of the pre-generated sources within the swift-java package by about 2/3.

There are some challenges with this approach, too:

  • The Swift as? and is won't work reliably. They've never worked with swift-java, but the compiler would warn about their use. With this change, they'll work sometimes (if the Swift instance was created with the most-derived type) but not always. We might be able to get this back with some kind of global registry for the Java class -> Swift class mapping.
  • Subclassing a Java class from Swift doesn't work, but this makes it look like it could. We do hope to make this possible in the future.
  • Class methods still don't get any of these benefits, although we might also be able to improve on this going forward.

Overall, I think mapping to Swift classes in the right thing to do, but it's not perfect. We'll need to work on sanding down the rough edges over time.

Thoughts?

Doug

Doug

3 Likes

One (language) option would be an annotation to invoke diagnostics similar to what’s hardcoded for CF types: “downcasts to this type are not implemented, use X instead”. This isn’t perfect, because you could use a Java class as a generic parameter and then the function tries to downcast, but it would at least help a bit, like the struct version did coincidentally.


My micro-optimization brain keeps going “but now you have a heap allocation and indirection to access the Java object!” and I have to remind it that we’re talking about Java here, where everything is a heap allocation and indirection (even if some of it is JITed out).

Yes, we could do that. I'd like us to try to keep the language wish-list really short, but for something where we have a hard-coded capability already and want to generalize it, it's "just" a matter of figuring out the spelling and writing a proposal.

We actually already had a Swift heap allocation with the struct formulation, because we promote the Java object to the "global reference" when it is wrapped up in Swift. We then use the Swift deinit to tell the JVM that we're done with the global reference. The behavior will be the same with the class formulation, just less hidden.

Doug

2 Likes

For what it’s worth this “less hidden class (and allocation)” would be worth doing even it if didn’t help with modeling the inheritance things. It doesn’t give the swift side some false sense of value type semantics when they’re very not. Quite happy about this change :slight_smile:

1 Like