Skip to content

Kotlin Multiplatform: a Java call to a Kotlin object links the commonMain expect instead of the platform actual #3033

Closed

Description

@inth3shadows

What happens

When a Java file calls into a Kotlin Multiplatform object, the call now links to the expect declaration in commonMain instead of the actual in the platform source set. An expect declaration doesn't exist in JVM bytecode, so a Java caller can only ever reach the actual.

// lib/src/commonMain/kotlin/a/Util.kt
package a
expect object Util {
    fun go()
}

// lib/src/androidMain/kotlin/a/Util.kt
package a
actual object Util {
    @JvmStatic
    actual fun go() { println("android") }
}
// app/src/main/java/b/App.java
package b;
import a.Util;
class App { void run() { Util.go(); } }
  • Expected: App.run calls lib/src/androidMain/kotlin/a/Util.kt: Util::go.
  • Actual on main (60b0d7a): App.run calls lib/src/commonMain/kotlin/a/Util.kt: Util::go.

Regression

This changed with #3023, which made a Java call through an import reach the Kotlin class it names. Before that (01e0ac3), the same call linked to the androidMain actual. In a real project, JakeWharton/timber, Timber.plant(...) and Timber.tag(...) from the Java sample app moved from androidMain/.../Timber.kt to the commonMain expect class Timber. So did extends Timber.Tree.

Possible direction: when an import names a class that has both expect and actual declarations, a Java caller (and a platform source set's Kotlin caller) should prefer the actual in a source set that caller can see.

Found by indexing the same repos with the builds before and after the recent JVM resolution fixes and diffing the edges.


Edited 2026-10-29: added the environment, the native-kernel check and the related issues. The repro and the expected and actual results are unchanged.

Environment: Linux (WSL2), Node 22.23.2. Reproduced on main at 60b0d7a both with CODEGRAPH_KERNEL=0 and with the native kernel.

Related: this changed with #3023, which made a Java call through an import reach the Kotlin class it names. #1676 is a separate feature request for a Kotlin Multiplatform bridge.

Activity

  1. johnhldiver-beep commented on Oct 31, 2026

    @johnhldiver-beep

    I reproduced this bug with root cause in src/resolution/import-resolver.ts — resolveJavaImportedReference (the JVM import resolution #3023 added) picks the imported declaration's owner with pickClosestJvmCandidate, which prefers an expect declaration on a directory-proximity tie, so a Java caller of a.Util lands on the commonMain expect instead of the platform actual. I plan to resolve it by making that pick caller-aware — a Java caller (whose JVM bytecode never contains the expect) and a Kotlin caller in a platform source set take the actual, the one in the caller's own source set when there is one, while a commonMain caller keeps the expect — without touching public API. Does this align with the project design?

  2. added a commit that references this issue on Nov 1, 2026
    59bfe6d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions