Skip to content

JNI: align inherited protocol requirement names across bindings - #3

Merged
teamhimeh merged 1 commit into
qoncept-forkfrom
codex/fix-jni-protocol-requirement-names
Oct 23, 2026
Merged

teamhimeh merged 1 commit into
qoncept-forkfrom
codex/fix-jni-protocol-requirement-names

Conversation

@teamhimeh

@teamhimeh teamhimeh commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Java interfaces, returned existential boxes, and native JNI thunks can disagree about protocol method names. Extension implementations can incorrectly influence overload detection, and an inherited requirement can be renamed on a child box even though the parent Java interface still declares its original name.

For example, if Parent declares value(number:) and Child adds value(text:), the child box must implement the parent's existing value(long) method and its own valueText(long) method. Emitting valueNumber(long) for the inherited method neither implements the parent interface nor matches its API.

Example 1: a Swift default implementation must not rename a requirement

Consider this Swift API, matching the first runtime regression fixture:

public protocol NamingCallback {
  func message() -> String
}

extension NamingCallback {
  public func message() -> String { "default" }
}

struct DefaultNamingCallback: NamingCallback {}

public func callNamingCallback(_ callback: some NamingCallback) -> String {
  callback.message()
}

public func makeNamingCallback() -> any NamingCallback {
  DefaultNamingCallback()
}

There is one requirement, message(). The extension supplies a default implementation of that same requirement. It does not introduce another method that needs a different Java name.

Before this change, the extension implementation incorrectly participated in name disambiguation. The generated Java interface declared String message_(), while the generated NamingCallbackBox implemented String message(). The box therefore failed to implement the interface's abstract method, causing Java compilation to fail.

After this change, the interface, box, and corresponding JNI thunk consistently use the name message. Both a Java implementation and a Swift value using the default implementation work through the same Java method:

NamingCallback callback = () -> "Java";
String fromJava = MySwiftLibrary.callNamingCallback(callback); // "Java"

try (var arena = SwiftArena.ofConfined()) {
    NamingCallback fromSwift = MySwiftLibrary.makeNamingCallback(arena);
    String fromDefault = fromSwift.message(); // "default"
}

The Swift default implementation remains the implementation for DefaultNamingCallback; the Java lambda supplies its own protocol witness. Generating Java interface default methods that forward to Swift implementations remains separate future work.

Example 2: child overloads must preserve the parent interface's API

Swift argument labels distinguish these two requirements even though both accept an Int64:

public protocol NamingParent {
  var id: Int64 { get }
  func value(number: Int64) -> Int64
}

public protocol NamingChild: NamingParent {
  var tag: Int64 { get }
  func value(text: Int64) -> Int64
}

struct NamingValue: NamingChild {
  var id: Int64 { 1 }
  var tag: Int64 { 2 }
  func value(number: Int64) -> Int64 { number + 10 }
  func value(text: Int64) -> Int64 { text + 20 }
}

public func makeNamingChild() -> any NamingChild { NamingValue() }

Java does not have Swift argument labels. Two value(long) methods cannot represent the two distinct operations on the child interface. The parent interface already exposes value(number:) as value(long), so that inherited API must keep its existing name. The child's additional requirement is named valueText(long).

Before this change, the child box recalculated names using the child's overload set and emitted valueNumber(long) for the inherited requirement. That did not implement the parent's value(long) method. The child interface, box, and native thunks could also use inconsistent names because translation support was checked and cached before overload disambiguation was complete.

After this change, each inherited requirement keeps the name chosen by its declaring interface, and all generated layers agree:

Swift requirement Java interface and box method Box native method
NamingParent.value(number:) value(long) $value
NamingChild.value(text:) valueText(long) $valueText

Java callers can use both operations, including through a parent reference:

try (var arena = SwiftArena.ofConfined()) {
    NamingChild child = MySwiftLibrary.makeNamingChild(arena);
    long fromParent = child.value(3);     // 13: Swift value(number:)
    long fromChild = child.valueText(3);  // 23: Swift value(text:)

    NamingParent parent = child;
    long throughParent = parent.value(3); // 13: the same inherited operation
}

makeNamingChild() returns any NamingChild in Swift. Its generated Java box holds that Swift value and forwards each method through a native JNI thunk to the correct Swift protocol witness. Consistent names are required for the generated Java code to compile and for the Java native declarations to resolve to the matching Swift JNI entry points.

The runtime regressions verify the Java callback override, the Swift default implementation, and both inherited overload results (13 and 23). Generator regressions also check the matching native entry-point names.

Changes

  • Base protocol name disambiguation on actual requirements, including inherited requirements.
  • Use each requirement's declaring interface to name inherited box methods and their JNI dispatch thunks.
  • Probe translation support without caching method names before the complete requirement set has been selected.
  • Add generator and Java runtime tests for default implementations, Java callbacks, and inherited overload dispatch.

Validation

  • The three new generator tests fail before the fix and pass after it.
  • Full Swift test suite: swift test --disable-experimental-prebuilts --disable-sandbox.
  • JNI sample generation, native/Java compilation, and runtime tests: ./gradlew :Samples:SwiftJavaExtractJNISampleApp:test --no-daemon (three new runtime tests).
  • swift-format lint --strict and git diff --check.

Tested with Apple Swift 6.4 and JetBrains Runtime 25.0.2 on macOS.

Dependency

This change builds on the distinction between protocol requirements and extension implementations. That prerequisite is already present in the base branch; this diff contains only the naming changes.

Disambiguate protocol method names using actual requirements, and preserve
the declaring interface's names for inherited box methods and native
thunks. Avoid caching provisional names while probing translation support.

Add regression and Java runtime tests covering default implementations,
callbacks, and overloads introduced by a refined protocol.
@teamhimeh
teamhimeh changed the base branch from codex/fix-jni-protocol-extension-requirements to qoncept-fork October 23, 2026 06:43
@teamhimeh
teamhimeh merged commit 990d2d6 into qoncept-fork Oct 23, 2026
33 of 42 checks passed
@teamhimeh

Copy link
Copy Markdown
Collaborator Author

related: swiftlang#934

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.

1 participant