Skip to content

jni: Unify common methods using generic implementations#582

Open
sidepelican wants to merge 9 commits intoswiftlang:mainfrom
sidepelican:generic_runtime_func
Open

jni: Unify common methods using generic implementations#582
sidepelican wants to merge 9 commits intoswiftlang:mainfrom
sidepelican:generic_runtime_func

Conversation

@sidepelican
Copy link
Contributor

Motivation

As mentioned in the 'Others' section of PR #572, we currently have branching logic in the generator for common functions like destroy, depending on whether the target type is generic or not.

This branching is undesirable as it increases maintenance costs.
While PR #567 partially addressed this by introducing .synthesizedFunction, I have found a better approach, I propose it and solve the problem.

Solution

Instead of generating type-specific code, providing a single generic implementation within the SwiftJava package for functions like toString and destroy.

Since JNISwiftInstance always including type metadata, I realized that we can utilize the technique proposed in this comment to call a generic implementation.

@_cdecl("Java_org_swift_swiftkit_core_SwiftObjects_toString__JJ")
public func Java_org_swift_swiftkit_core_SwiftObjects_toString__JJ(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, selfPointer: jlong, selfTypePointer: jlong) -> jstring? {
  let selfTypeBits$ = Int(Int64(fromJNI: selfTypePointer, in: environment))
  guard let selfType$ = UnsafeRawPointer(bitPattern: selfTypeBits$) else {
    fatalError("selfType metadata address was null")
  }
  let typeMetadata = unsafeBitCast(selfType$, to: Any.Type.self)

  func perform<T>(as type: T.Type) -> jstring? {
    guard let self$ = UnsafeMutablePointer<T>(bitPattern: selfPointer) else {
      fatalError("self memory address was null")
    }
    return String(describing: self$.pointee).getJNIValue(in: environment)
  }
  return perform(as: typeMetadata)
}

Pros:

  • Significantly simplifies the code generator implementation.
  • Greatly reduces the amount of generated code, leading to smaller binary sizes.
  • Aligns the implementation more closely with the FFM side.

Cons:

  • Increase in runtime overhead:
    • Add calling to runtime functions (SIL's open_existential_metatype).
    • Loss of certain optimizations (e.g., inlining that might skip VWT lookups when individual implementations are generated).

Regarding the downsides, I believe the overhead is negligible compared to the inherent cost of JNI calls.

Changes

Following this new direction, I have replaced the generated code for the following functions with generic implementations:

  • toString
  • toDebugString
  • destroy
  • getDiscriminator

Additionally, I have removed .synthesizedFunction as it is no longer needed.

# Conflicts:
#	Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift
#	Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift
#	Sources/JExtractSwiftLib/Swift2JavaVisitor.swift
#	Tests/JExtractSwiftTests/JNI/JNIClassTests.swift
#	Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift
#	Tests/JExtractSwiftTests/JNI/JNIGenericTypeTests.swift
#	Tests/JExtractSwiftTests/JNI/JNIStructTests.swift
@sidepelican sidepelican requested a review from ktoso as a code owner March 4, 2026 06:43
thisClass: jclass,
selfPointer: jlong,
selfTypePointer: jlong
) -> jint {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to get away with implementing them as

@JavaImplementation("org.swift.swiftkit.core.JavaObjects")
struct SwiftObjects { 
  @JavaStaticMethod
  func getRawDiscriminator(selfPointer: Long, selfTypePointer: Long) -> Int { ... }
}

since SwiftJava depends on the SwiftJavaMacros... I'd like to avoid not-using our own tools in this project, this way it's a good feedback mechanism if something is off or missing, and we avoid the typical mistakes with the cdecls etc.

I forget the exact syntax so sorry if the snippet isn't exactly right above here :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize that we can use SwiftJavaMacros here! This would be better.

exclude: ["swift-java.config"],
swiftSettings: [
.swiftLanguageMode(.v5),
.enableUpcomingFeature("ImplicitOpenExistentials"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

...
return String(reflecting: selfPointer$.pointee).getJNIValue(in: environment)
public String toDebugString() {
return SwiftObjects.toDebugString(this.$memoryAddress(), this.$typeMetadataAddress());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I really like this shape, it's true as you say we take some performance hit here for the opening but perhaps it is worth it anyway.

Would you be able to do a quick benchmark before/after? We have benchmarks set up in the repo, should be quick to add and try.

Overall though I'm supportive of this change, it's a nice pattern we'll likely follow in many more places!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants