Commit 6898c1c
[generator] Drop Kotlin hash-mangled siblings that collide on the C# side (#1432)
Context: step (1) of #1431.
When Kotlin compiles a function whose parameter is an `@JvmInline value
class`, the JVM-level name is mangled with a 7-char hash suffix, e.g.
`tint-Rn_QMJI(J)V`. `KotlinFixups` already strips that suffix so the C#
binding sees `Tint(long)` instead of an unspellable name.
The problem: two distinct Kotlin overloads that take different inline
classes erase to the *same* JVM signature, differing only in the hash.
Jetpack Compose hits this constantly (`Color`, `TextUnit`, `Dp` all wrap
`ULong`/`Long`/`Float`). For example:
@JvmInline value class MyColor(val value: ULong)
@JvmInline value class MyAlpha(val value: ULong)
object Widgets {
fun tint(color: MyColor) { }
fun tint(alpha: MyAlpha) { }
}
`javap` shows two hash-distinct siblings with identical erased shape:
public static void tint-Rn_QMJI(long); // MyColor
public static void tint-uzYZ1wI(long); // MyAlpha
After the existing rename both become `Tint(long)`, and `generator`
emits something like:
// Before: CS0111, build broken
public static void Tint (long color) { ... }
public static void Tint (long alpha) { ... }
This change detects post-rename collisions inside `KotlinFixups`, keeps
the first method deterministically, drops the rest, and emits a new
`BG8C02` warning so the user can override the choice via Metadata.xml:
// After: one overload kept, warning emitted
public static void Tint (long color) { ... }
// warning BG8C02: For type 'Widgets', the Kotlin name-mangled method
// 'Tint' (originally 'tint-uzYZ1wI') has multiple hash-suffixed
// siblings that erase to the same C# signature. Only the first will
// be emitted; remove the duplicate via Metadata.xml to suppress this
// warning.
Non-colliding hash-mangled siblings (different arity, or different
underlying primitive) still survive — e.g. `pad(MyDp)` and
`pad(MyDp, MyDp)` both bind, and `tint(MyDp)` survives alongside one of
the `tint(long)` overloads because they differ in raw type (`F` vs `J`).
Step (2) of #1431 — projecting inline-class parameters as their own
strongly-typed wrappers so all overloads can coexist — is left for a
follow-up.
Real Kotlin/Gradle fixture
--------------------------
To exercise this against real Kotlin compiler output (not hand-written
`.class` files), add a small Gradle project under
`tests/Xamarin.Android.Tools.Bytecode-Tests/kotlin-gradle/`:
* `build.gradle.kts` pins Kotlin 2.0.21 / JVM 17 and emits `.class`
files into `$rootDir/classes`.
* `src/main/kotlin/InlineClassCollisions.kt` declares the value
classes and `Widgets` object shown above.
* A new MSBuild target `BuildKotlinGradleProject` invokes the
existing `build-tools/gradle/gradlew(.bat)` (no duplicate wrapper)
with `-p kotlin-gradle classes` before `BeforeCompile`. The
produced `.class` files are then embedded as test resources.
* The `classes/` and `build/` outputs stay out of git via
`kotlin-gradle/.gitignore`.
A `.gitattributes` entry forces `gradlew`, `*.properties`, `*.kt`, and
`*.kts` to LF so the shared wrapper script keeps working on Unix.
Tests
-----
* `KotlinFixupsTests`: 3 new unit tests cover the collision /
no-collision / mixed cases with a `WarningCapture` helper that
asserts on BG8C02.
* `KotlinInlineClassCollisionTests`: 3 new tests load the freshly
Gradle-built `Widgets.class` / `MyColor.class` and assert the
expected JVM-level mangling, so we'd notice if Kotlin ever changed
the scheme.
All 5 `KotlinFixupsTests`, all 78 Bytecode tests, and all 453 generator
tests pass.
### Address PR review feedback
- KotlinFixups.RemoveCollidingSiblings: collide mangled methods against
every other method on the type (not only other mangled siblings), so a
mangled tint(MyInlineLong) collapsing to tint(long) collides with a
pre-existing non-mangled tint(long) too. Mangled methods are still the
only thing ever dropped.
- Add MangledMethod_CollidesWithNonMangledOverload test for that case.
- Mark the BG8C02-asserting tests [NonParallelizable] because they mutate
the global Report.OutputDelegate, matching the convention used by other
WarningCapture-style tests in the suite.
- Bytecode-Tests.targets: use shared $(GradleWPath) / $(GradleArgs)/
EnvironmentVariables=JAVA_HOME pattern from Directory.Build.props,
matching tools/java-source-utils. Removes the ad-hoc OS-switch wrapper
selection and makes the Gradle invocation work in environments without
java on PATH.
- build.gradle.kts: drop jvmToolchain(17) so Gradle uses the JDK the
caller already configured via JAVA_HOME (auto-provisioning fails in CI
environments without download repositories).
### Keep first colliding sibling, not last
The previous fix in 8e8d7a6 walked all of gen.Methods when looking
for a conflict and removed the *current* mangled method whenever any
other method matched -- including ones that appeared LATER in source.
For two mangled siblings A and B, iterating A would find B (later) and
remove A, leaving B as the survivor. That contradicted the warning text
('Only the first will be emitted') and was non-deterministic w.r.t. the
intended Metadata.xml escape hatch.
Fix: only treat the current mangled method as the duplicate when a
matching method appears EARLIER in source order (TakeWhile(m => m !=
method)). This guarantees the first-declared overload always wins,
whether the earlier method is mangled or non-mangled.
Strengthened CollidingHashSiblings_AreDeduplicated to assert the kept
method's JavaName is �dd-AAAAAAA (not just any method named Add).
### Drop mangled when non-mangled overload matches in any order
When a mangled method appears BEFORE a non-mangled overload with the
same erased signature, the previous TakeWhile-only logic kept both
(neither was earlier than itself, and non-mangled methods were never
in 'renamed'), producing CS0111.
Always prefer dropping the mangled method whenever ANY non-mangled
match exists regardless of source order. Fall back to the
first-declared-wins rule only when the only collisions are between
mangled siblings.
Adds MangledMethod_CollidesWithNonMangledOverload_ReversedOrder test
covering the previously-broken ordering.
### Compile Kotlin in-process to fix macOS CI failure
On macOS the external Kotlin compile daemon fails to start on attempt
#1 and prints 'e : The daemon has terminated unexpectedly...' to
stderr. Kotlin retries successfully (BUILD SUCCESSFUL), but the stray
'e :' line plus the daemon JVM shutdown causes the gradlew process to
exit -1, failing the dotnet build.
Setting kotlin.compiler.execution.strategy=in-process makes Kotlin
compile inside the Gradle daemon process itself, avoiding the extra
JVM and the macOS flake entirely.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>1 parent e7c502f commit 6898c1c
14 files changed
Lines changed: 382 additions & 3 deletions
File tree
- src
- Java.Interop.Localization
- Java.Interop.Tools.Generator/Utilities
- tests
- Xamarin.Android.Tools.Bytecode-Tests
- kotlin-gradle
- src/main/kotlin
- generator-Tests/Unit-Tests
- tools/generator/Java.Interop.Tools.Generator.Transformation
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
308 | 308 | | |
309 | 309 | | |
310 | 310 | | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
311 | 315 | | |
312 | 316 | | |
313 | 317 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
| 77 | + | |
77 | 78 | | |
78 | 79 | | |
79 | 80 | | |
| |||
Lines changed: 70 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
Lines changed: 7 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
29 | 35 | | |
30 | 36 | | |
31 | 37 | | |
| |||
Lines changed: 25 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
8 | 14 | | |
9 | 15 | | |
10 | 16 | | |
| |||
34 | 40 | | |
35 | 41 | | |
36 | 42 | | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
37 | 62 | | |
38 | 63 | | |
39 | 64 | | |
| |||
Lines changed: 4 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
Lines changed: 21 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
Lines changed: 5 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
0 commit comments