Skip to content

jextract/jni: Add basic support for generic types#572

Open
sidepelican wants to merge 24 commits intoswiftlang:mainfrom
sidepelican:generic_call
Open

jextract/jni: Add basic support for generic types#572
sidepelican wants to merge 24 commits intoswiftlang:mainfrom
sidepelican:generic_call

Conversation

@sidepelican
Copy link
Contributor

@sidepelican sidepelican commented Feb 25, 2026

Purpose

This PR introduces basic support for accessing generic Swift types from Java.
As this is the initial step, the functionality is currently limited.

Specifically, the following are not supported in this PR:

  • The Java-side wrappers do not yet have generic signatures.
  • No access to static members. (Since there is no mechanism to specify type parameters from Java yet.)
  • No access to members using type parameters

Implementation

The general strategy follows the direction discussed in this issue comment.

In this implementation, the Swift metatype is stored as a field within the Java wrapper class.
When a function is called, this metatype is passed as an argument to the native function.

Regarding the usage of the metatype, I have adopted a different approach than the one originally mentioned in the issue.
When a type is generic, the generator now produces an "opener protocol".
This is a technique to handle the Self type correctly from a metatype. By conforming the original type to this opener protocol, we can indirectly extract and utilize the type parameters within Swift.

public struct MyID<T> {
  public var rawValue: T
  public var description: String {
    "\(rawValue)"
  }
}
protocol _MySwiftLibrary_MyID_opener {
  static func _get_description(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, self: jlong) -> jstring?
  ...
}

extension MyID: _MySwiftLibrary_MyID_opener {
  static func _get_description(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, self: jlong) -> jstring? {
    assert(self != 0, "self memory address was null")
    let selfBits$ = Int(Int64(fromJNI: self, in: environment))
    let self$ = UnsafeMutablePointer<MyID>(bitPattern: selfBits$)
    guard let self$ else {
      fatalError("self memory address was null in call to \(#function)!")
    }
    return self$.pointee.description.getJNIValue(in: environment)
  }
  ...
}

@_cdecl("Java_com_example_swift_MyID__00024getDescription__JJ")
public func Java_com_example_swift_MyID__00024getDescription__JJ(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, self: jlong, selfType: jlong) -> jstring? {
  let selfTypeBits$ = Int(Int64(fromJNI: selfType, in: environment))
  guard let selfType$ = UnsafeRawPointer(bitPattern: selfTypeBits$) else {
    fatalError("selfType metadata address was null")
  }
  let openerType = unsafeBitCast(selfType$, to: Any.Type.self) as! (any _MySwiftLibrary_MyID_opener.Type)
  return openerType._get_description(environment: environment, thisClass: thisClass, self: self)
}
Why not use the original pattern? This is because the original pattern cannot handle certain cases successfully. It cannot expand when there are multiple type parameters and constraints. 2026-02-16_15 28 39

Others

The following functions cause branching in code generators based on whether they are generic or not:

  • long $typeMetadataAddress()
  • Runnable $createDestroyFunction()
  • Discriminator getDiscriminator()

Similar to the refactoring in #567 for toString, I plan to register these as SynthesizedAPI to remove this redundancy. However, to avoid over-complicating this PR, I have deferred that refactoring to a future update.

@sidepelican sidepelican requested a review from ktoso as a code owner February 25, 2026 02:45
if (CallTraces.TRACE_DOWNCALLS) {
CallTraces.traceDowncall("MyID.$destroy", "self", self$);
}
MyID.$destroy(self$, selfType$);
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

if (CallTraces.TRACE_DOWNCALLS) {
CallTraces.traceDowncall("MyID.$createDestroyFunction",
"this", this,
"self", self$);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's include the selfType$ now as well

expectedChunks: [
"""
protocol _SwiftModule_MyID_opener {
static func _get_description(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, self: jlong) -> jstring?
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
static func _get_description(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, self: jlong) -> jstring?
static func _get_description(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, selfPointer: jlong) -> jstring?

sorry to be annoying about the selfPointer every time 😉

I don't want it to be confusing when we see a self it should be the object and the selfPointer should be the jlong or other pointer repr

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 see. I don't have a strong preference, so I think selfPointer is fine as well.

The variable name self originates from this location:

parameterName: selfParameter.parameterName ?? "self",

Changing this to selfPointer would require updating a large number of existing tests.
Would you still like me to proceed with the change?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah let's bite the bullet, we can do it in a separate PR.

This is looking good, happy to land it after some nitpicks


package org.swift.swiftkit.core;

public final class OutSwiftGenericInstance {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please document what this is for


public final class OutSwiftGenericInstance {
public long selfPointer;
public long selfTypePointer;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
public long selfTypePointer;
public final long selfTypePointer;

If making them public please make them final

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OutSwiftGenericInstance is a temporary container used for receiving multiple return values.

Both selfPointer and selfTypePointer are updated from the Swift side using JNI's SetLongField.
While JNI can technically bypass the final modifier to modify fields, I'm concerned that JIT optimizations might lead to unexpected behavior if the compiler makes incorrect assumptions about these values being constant.

Should I add the final modifier ?

package org.swift.swiftkit.core;

public final class OutSwiftGenericInstance {
public long selfPointer;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
public long selfPointer;
public final long selfPointer;

If making them public please make them final

| Dictionaries: `[String: Int]`, `[K:V]` | ❌ | ❌ |
| Generic type: `struct S<T>` | ❌ | ✅ |
| Functions or properties using generic type param: `struct S<T> { func f(_: T) {} }` | ❌ | ❌ |
| Static functions or properties in generic type | ❌ | ❌ |
Copy link
Collaborator

@ktoso ktoso Feb 25, 2026

Choose a reason for hiding this comment

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

This deserves some more docs about how we import it, since we drop the generic from the signature we should explain this -- but for simple things I thikn we'll be able to pull off doing a generic on the Java side tbh right?

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 wrote documentation to make it clearer what gets exported and what doesn't.

016766b

Copy link
Collaborator

@ktoso ktoso left a comment

Choose a reason for hiding this comment

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

I'm happy to merge this as a first step towards generics support :) Just the few nitpicks please

@ktoso
Copy link
Collaborator

ktoso commented Feb 25, 2026

I just fixed the android CI, a new run should pass

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