Skip to content

Compose UI.Graphics: SolidColor(long) constructor stripped due to Kotlin @JvmInline value class param (sibling of java-interop#1440 for ctors) #1470

@jonathanpeppers

Description

@jonathanpeppers

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 — 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):

@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 — <init>(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:

[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 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:

// `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, "<init>", "(J)V");
    }
    var args = stackalloc JValue[1];
    args[0] = new JValue(color);
    return JNIEnv.NewObject(s_solidColor_class, s_solidColor_ctor, args);
}

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 <add-node> + <constructor> be an acceptable metadata workaround until #1440 lands?

There is precedent in this repo for synthesizing missing
constructors via <add-node>:

Applied to SolidColor, the proposed metadata would look like:

<add-node path="/api/package[@name='androidx.compose.ui.graphics']/class[@name='SolidColor']">
  <constructor
      deprecated="not deprecated" final="false"
      name="SolidColor" static="false" visibility="public"
      bridge="false" synthetic="false"
      jni-signature="(J)V">
    <parameter name="value" type="long" jni-type="J" />
  </constructor>
</add-node>

The bytecode-level ctor <init>(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 <add-node> 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 <add-node> + <constructor> 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 <add-node> 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 structs 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 <init>),
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

Metadata

Metadata

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions