dotnet / dotnet/android-libraries
Compose UI.Graphics: SolidColor(long) constructor stripped due to Kotlin @JvmInline value class param (sibling of java-interop#1440 for ctors)
- Dominant language
- C#
- Stars
- 317
- Forks
- 73
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 17
Description
## Background
Kotlin `@JvmInline value class` parameters strip the **constructor** of a
binding even when the JVM bytecode signature is plain primitive shape and
the surrounding type is otherwise fully bound. This is a constructor-side
sibling of `dotnet/java-interop#1440` (which targets static methods like
`Button-bWB7cM8` / `Typography(...)`) and the closest analogue is
[#1453](https://github.com/dotnet/android-libraries/issues/1453) — but
that issue focuses on a stripped *static factory function*. Stripped
*constructors* are not currently tracked.
## Concrete repro: `androidx.compose.ui.graphics.SolidColor`
Kotlin source ([1.11.x](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt)):
```kotlin
@Immutable
class SolidColor(val value: Color) : ShaderBrush() { … }
```
`Color` is `@JvmInline value class Color(val value: ULong)`, so the JVM
bytecode for the ctor is plain primitive — `(J)V`. There is **no
mangling on the constructor itself** (Kotlin's hash-mangling only applies
to methods), but the binder still drops it.
`Xamarin.AndroidX.Compose.UI.Graphics.Android` 1.11.2.1 decompiled with
`ilspycmd`:
```csharp
[Register("androidx/compose/ui/graphics/SolidColor", DoNotGenerateAcw = true)]
public sealed class SolidColor : Brush, IInterpolatable, …
{
// value-class-param methods with mangled names ARE kept:
[Register("getValue-0d7_KjU", "()J", "")] public long Value { get; }
[Register("applyTo-Pq9zytI", "(JLandroidx/compose/ui/graphics/Paint;F)V", "")]
public sealed override void ApplyTo(long size, IPaint p, float alpha) { … }
[Register("lerp", "(Ljava/lang/Object;F)Ljava/lang/Object;", "")]
public Object? Lerp(Object? other, float t) { … }
// Only the internal acw ctor survives. The public (J)V ctor is gone:
internal SolidColor(nint javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer) { }
// <-- where is `public SolidColor(long packedColor)` ?
}
```
`source/androidx.compose.ui/ui-graphics-android/PublicAPI/PublicAPI.Unshipped.txt`
confirms the gap — the only `SolidColor` ctor in the public API is the
internal `(nint, JniHandleOwnership)` one. There is no `SolidColor(long)`.
This is *not* unique to `SolidColor`: every Compose class whose primary
ctor takes a value-class parameter has the same problem (`Path`,
`Outline.Rectangle`, several `BrushPainter` shapes, …). The
`androidx.compose.ui.graphics.Color` type itself loses both its `(J)V`
ctor and its `box-impl(J)Color` static factory for the same reason.
## Real-world impact
[`jonathanpeppers/Microsoft.AndroidX.Compose#251`][pr251] adds
`Brush.SolidColor(Color)` and the gradient factories. Because the bound
`SolidColor` has no callable ctor, the PR ships a hand-written JNI
fallback in [`BrushBridges.cs`][bb]:
```csharp
// `new androidx.compose.ui.graphics.SolidColor(Color)` — the ctor
// is stripped because its parameter is a value-class `Color`.
internal static unsafe IntPtr BrushSolidColor(long color)
{
if (s_solidColor_ctor == IntPtr.Zero)
{
s_solidColor_class = Java.Lang.Class.FromType(
typeof(AndroidX.Compose.UI.Graphics.SolidColor)).Handle;
s_solidColor_ctor = JNIEnv.GetMethodID(
s_solidColor_class, "", "(J)V");
}
var args = stackalloc JValue[1];
args[0] = new JValue(color);
return JNIEnv.NewObject(s_solidColor_class, s_solidColor_ctor, args);
}
```
[pr251]: https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/pull/251
[bb]: https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/blob/main/src/Microsoft.AndroidX.Compose/BrushBridges.cs
The same PR also reaches for `Color.box-impl(J)` (a static factory) and
`Brush.Companion` (a static field) directly via JNI — those have other
trackers — but the ctor case is the new one and is what this issue is
about.
## Question: would `` + `` be an acceptable metadata workaround until #1440 lands?
There is precedent in this repo for synthesizing missing
constructors via ``:
- [`source/com.github.bumptech.glide/glide/Transforms/Metadata.xml#L100-L101`](https://github.com/dotnet/android-libraries/blob/main/source/com.github.bumptech.glide/glide/Transforms/Metadata.xml#L100-L101)
injects a public ctor into `GeneratedAppGlideModule`, and the
resulting `Bumptech.Glide.GeneratedAppGlideModule.GeneratedAppGlideModule() -> void`
shows up in `PublicAPI.Unshipped.txt`.
- [`source/com.google.android.gms/play-services-iid/Transforms/Metadata.xml#L26-L28`](https://github.com/dotnet/android-libraries/blob/main/source/com.google.android.gms/play-services-iid/Transforms/Metadata.xml#L26-L28)
injects an `()V` ctor on `InstanceIDListenerService` with an
explicit `jni-signature="()V"`, and that surfaces as
`Android.Gms.Iid.InstanceIDListenerService.InstanceIDListenerService() -> void`.
Applied to `SolidColor`, the proposed metadata would look like:
```xml
```
The bytecode-level ctor `(J)V` already exists — the binder just
isn't emitting it because Kotlin metadata flags the param as a
value class. Manually re-introducing it via `` should cause
the generator to bind it as a normal `public SolidColor(long value)`,
the same way the existing `applyTo-Pq9zytI(JL…/Paint;F)V` and
`getValue-0d7_KjU()J` methods are bound today (those keep their
hash-mangled JNI names and project the value-class params as their
underlying primitives).
**Question for maintainers:** is the `` + `` recipe
above an accepted workaround pattern for value-class-param ctors? If so,
it would unblock several hand-rolled-JNI hotspots in
`Microsoft.AndroidX.Compose` (`SolidColor`, `Color`'s `box-impl`,
several `Painter` shapes) ahead of the eventual #1440 generator fix.
If `` for value-class-param ctors is *not* expected to work
(e.g. there's a downstream fixup that resurfaces the strip), please say
so here and we'll keep the JNI bridges. Either answer is useful.
## Why a separate issue from the existing trackers
| Issue | Scope | Mechanism |
|---|---|---|
| `dotnet/java-interop#1431` (open) | Umbrella: inline-class projection + sibling-collision | Generator |
| `dotnet/java-interop#1432` (merged) | Phase 1 — sibling-collision dedup for hashed *methods* | Generator |
| `dotnet/java-interop#1440` (open) | Phase 2 — project inline classes as `readonly struct`s | Generator |
| `dotnet/android-libraries#1453` (open) | Stripped `TypographyKt.Typography(...)` static factory | Material3 binding + (#1440) |
| **This issue** | Stripped **constructors** of types whose Kotlin primary ctor takes a value class | UI.Graphics binding + Metadata.xml |
`#1432`/`#1440`/`#1431` cover the *method* side (mangled names like
`Button-bWB7cM8`). Constructors aren't mangled (they're always ``),
so the issue presents differently — but the same Kotlin-metadata-driven
strip path drops them. This issue exists so that both halves of the
pattern are tracked, and so the metadata workaround question can be
answered explicitly.
## References
- [`jonathanpeppers/Microsoft.AndroidX.Compose#251`](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/pull/251) — real-world consumer
- [`BrushBridges.cs`](https://github.com/jonathanpeppers/Microsoft.AndroidX.Compose/blob/main/src/Microsoft.AndroidX.Compose/BrushBridges.cs) — the JNI fallback this issue is asking to remove
- [`SolidColor.kt`](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt) — upstream Kotlin source
- [Kotlin inline classes — mangling](https://kotlinlang.org/docs/inline-classes.html#mangling)
Contributor guide
Assessment
This issue has not been assessed yet.